Compare commits
163 commits
Author | SHA1 | Date | |
---|---|---|---|
|
9df1115380 | ||
|
f22e360cbb | ||
|
67769af6f4 | ||
|
b1a9a1d6d9 | ||
|
cf0606ecb7 | ||
|
7287edcd6f | ||
|
e0d26203dd | ||
|
7e3a85e9da | ||
|
5a0fed9c93 | ||
|
1f1e743912 | ||
|
b4301ed0d5 | ||
|
b5391560fc | ||
|
718989cbcf | ||
|
d0aee76962 | ||
|
fb08af96bd | ||
|
510a0c5e70 | ||
|
5fe2be031f | ||
|
2ba3aaba47 | ||
|
ad8903991c | ||
|
3e5624c570 | ||
|
f5ee6f3537 | ||
|
afc77e7adc | ||
|
024f42fce6 | ||
|
8a5f12b97c | ||
|
954b21cf39 | ||
|
74d095774d | ||
|
17a2722e6d | ||
|
c843bddbfe | ||
|
3f22a49755 | ||
|
7af2ffcebf | ||
|
de90c959e0 | ||
|
9987dc1eb4 | ||
|
3efd575dd2 | ||
|
f4c7b17a87 | ||
|
16d80718cb | ||
|
ad228d53b7 | ||
|
15ee1e531b | ||
|
1c8fb3392a | ||
|
8647866a32 | ||
|
23351c4f1c | ||
|
1367c304cf | ||
|
26d6bcb074 | ||
|
b0d651ece1 | ||
|
b6d50ba6a4 | ||
|
b3ab6a9166 | ||
|
f39a5ac9c2 | ||
|
38a9a9240d | ||
|
241b22a465 | ||
|
741abc0366 | ||
|
7854775de5 | ||
|
e62eaa6b4b | ||
|
b4cce23ef4 | ||
|
2bcaf90fc8 | ||
|
96ffbe2f84 | ||
|
6f5acee1c3 | ||
|
54e491d8bf | ||
|
ab6374e278 | ||
|
2fda4c9f67 | ||
|
5138a45b0f | ||
|
b224d4fa8a | ||
|
a552e44483 | ||
|
0cf3bba118 | ||
|
2c48ea3508 | ||
|
b9b6212b75 | ||
|
b978aaec21 | ||
|
af704681d9 | ||
|
1443ddfe6c | ||
|
54457a3e1b | ||
|
bf180e6a2c | ||
|
864a5820c9 | ||
|
4d3ca49c3f | ||
|
c49c3cf7f0 | ||
|
5d5ab57469 | ||
|
31978d8de0 | ||
|
e8eb68bf24 | ||
|
9ea08f4fed | ||
|
fe078a5c5b | ||
|
61933954f3 | ||
|
4c243638cb | ||
|
02ba04b5d8 | ||
|
4f158a4829 | ||
|
177a22df59 | ||
|
6b0ca2966e | ||
|
aadfaf7150 | ||
|
b307b9a66b | ||
|
6c1ab6002d | ||
|
9638eefc91 | ||
|
9e9c4ad587 | ||
|
ce231431b9 | ||
|
06e1e14e02 | ||
|
416e7884f5 | ||
|
d579222007 | ||
|
30243c84cd | ||
|
3557a77645 | ||
|
97be28638b | ||
|
aba0826c38 | ||
|
f032228d0e | ||
|
6cf174c5ed | ||
|
c2109d245f | ||
|
6a9745171e | ||
|
f9a68e8b23 | ||
|
6e391df5ee | ||
|
f5edca94d3 | ||
|
60046abec3 | ||
|
cafc2060b8 | ||
|
b1f45752cf | ||
|
ed17551170 | ||
|
ef5adab638 | ||
|
fb653ff99d | ||
|
78fc47a9c4 | ||
|
2a124cea61 | ||
|
4c7cc563dc | ||
|
6114af4f93 | ||
|
8c31629655 | ||
|
03c8a8edb2 | ||
|
3eeaee154f | ||
|
8cf8fa7c80 | ||
|
6b4f6fc71e | ||
|
30c2680b6f | ||
|
fb7b1800cc | ||
|
ff573bf377 | ||
|
0a33bb861e | ||
|
728756289b | ||
|
56ccd3a0ac | ||
|
66f3f0ba07 | ||
|
af5e0d589e | ||
|
533dc99e7d | ||
|
fc5ca965ba | ||
|
9c4a46bcdb | ||
|
52658886e7 | ||
|
8174ab7616 | ||
|
2b6acedae1 | ||
|
d00fe9c5f4 | ||
|
88aa270728 | ||
|
4ae409c7f4 | ||
|
9a29c9abdd | ||
|
66d93ea037 | ||
|
6c0066dbfb | ||
|
4fde644733 | ||
|
e7841c60df | ||
|
94f647b24a | ||
|
630249d22a | ||
|
db99b4cb54 | ||
|
c77db23586 | ||
|
daf66bcec4 | ||
|
8caf36349f | ||
|
6934de58e5 | ||
|
54a5007c01 | ||
|
e25a455698 | ||
|
ab429dfeb6 | ||
|
c5289dc0e8 | ||
|
d191877002 | ||
|
4d979160c2 | ||
|
d00e8f6e19 | ||
|
91b77e5237 | ||
|
5b8c246d53 | ||
|
b374b9b91c | ||
|
403717117e | ||
|
027295d995 | ||
|
c9b7eccbc1 | ||
|
2b6d9348cd | ||
|
692f8c8454 | ||
|
6783355c4d |
92 changed files with 3930 additions and 1958 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.
|
61
.github/workflows/alpha.yml
vendored
61
.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,35 +32,13 @@ jobs:
|
|||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000
|
||||
yarn run check
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Delete current release assets
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
@ -79,6 +53,18 @@ jobs:
|
|||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
*.exe
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check --force
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
|
@ -91,14 +77,13 @@ jobs:
|
|||
releaseName: "Clash Verge Alpha"
|
||||
releaseBody: "Alpha Version (include debug)"
|
||||
releaseDraft: false
|
||||
includeDebug: ${{ github.event.inputs.debug }}
|
||||
prerelease: true
|
||||
args: -f default-meta
|
||||
includeDebug: ${{ github.event.inputs.debug }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
yarn build -f default-meta
|
||||
yarn build
|
||||
yarn run portable
|
||||
env:
|
||||
TAG_NAME: alpha
|
||||
|
|
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
|
||||
|
|
35
.github/workflows/meta.yml
vendored
35
.github/workflows/meta.yml
vendored
|
@ -39,6 +39,23 @@ jobs:
|
|||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Delete current release assets
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: meta
|
||||
fail-if-no-assets: false
|
||||
fail-if-no-release: false
|
||||
assets: |
|
||||
*.zip
|
||||
*.gz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
|
@ -63,22 +80,6 @@ jobs:
|
|||
yarn install --network-timeout 1000000
|
||||
yarn run check
|
||||
|
||||
- name: Delete current release assets
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: meta
|
||||
fail-if-no-assets: false
|
||||
fail-if-no-release: false
|
||||
assets: |
|
||||
*.zip
|
||||
*.gz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
|
@ -99,7 +100,7 @@ jobs:
|
|||
yarn build -f default-meta
|
||||
yarn run portable
|
||||
env:
|
||||
TAG_NAME: alpha
|
||||
TAG_NAME: meta
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
|
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
|
||||
|
|
60
README.md
60
README.md
|
@ -17,17 +17,69 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
|||
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
|
||||
- System proxy setting and guard.
|
||||
|
||||
## Promotion
|
||||
|
||||
[狗狗加速 —— 技术流机场 Doggygo VPN](https://dg1.top)
|
||||
|
||||
- High-performance overseas VPN, free trial, discounted packages, unlock streaming media, the world's first to support Hysteria protocol.
|
||||
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
|
||||
- 使用 Clash Verge 专属邀请链接注册送 15 天,每天 1G 流量免费试用:https://panel.dg1.top/#/register?code=sFCDayZf
|
||||
|
||||
<details>
|
||||
<summary>Promotion Detail</summary>
|
||||
|
||||
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
|
||||
- 优惠套餐每月仅需 15.8 元,160G 流量,年付 8 折
|
||||
- 海外团队,无跑路风险,高达 50% 返佣
|
||||
- 集群负载均衡设计,高速专线(兼容老客户端),极低延迟,无视晚高峰,4K 秒开
|
||||
- 全球首家 Hysteria 协议机场,将在今年 10 月上线更快的 `tuic` 协议(Clash Verge 客户端最佳搭配)
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 官网:https://dg1.top
|
||||
|
||||
</details>
|
||||
|
||||
<br />
|
||||
|
||||
[EEVPN —— 海外运营机场 ※ 支持 ChatGPT](https://www.eejsq.net/#/register?code=yRr6qBO3)
|
||||
|
||||
- 年付低至 9.99 元,价格低,速度不减
|
||||
|
||||
<details>
|
||||
<summary>Promotion Detail</summary>
|
||||
|
||||
- 中国大陆 BGP 网络接入
|
||||
- IEPL 专线网络
|
||||
- 最高 2500Mbps 速率可用
|
||||
- 不限制在线客户端
|
||||
- 解锁流媒体及 ChatGPT
|
||||
- 海外运营 数据安全
|
||||
|
||||
</details>
|
||||
|
||||
## Install
|
||||
|
||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
|
||||
- [Windows x64](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64_en-US.msi)
|
||||
- [macOS intel](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_x64.dmg)
|
||||
- [macOS arm](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/Clash.Verge_1.3.8_aarch64.dmg)
|
||||
- [Linux AppImage](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.AppImage)
|
||||
- [Linux deb](https://github.com/zzzgydi/clash-verge/releases/download/v1.3.8/clash-verge_1.3.8_amd64.deb)
|
||||
- [Fedora Linux](https://github.com/zzzgydi/clash-verge/issues/352)
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
|
||||
|
||||
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app`
|
||||
|
||||
## Development
|
||||
|
||||
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
|
||||
```shell
|
||||
yarn install
|
||||
|
@ -36,6 +88,9 @@ yarn install
|
|||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
||||
|
||||
```shell
|
||||
# force update to latest version
|
||||
# yarn run check --force
|
||||
|
||||
yarn run check
|
||||
```
|
||||
|
||||
|
@ -43,6 +98,9 @@ Then run
|
|||
|
||||
```shell
|
||||
yarn dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
yarn dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
|
150
UPDATELOG.md
150
UPDATELOG.md
|
@ -1,3 +1,153 @@
|
|||
## v1.3.8
|
||||
|
||||
### Features
|
||||
|
||||
- update clash meta core
|
||||
- add default valid keys
|
||||
- adjust the delay display interval and color
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix connections page undefined exception
|
||||
|
||||
---
|
||||
|
||||
## v1.3.7
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
- profiles page add paste button
|
||||
- subscriptions url textfield use multi lines
|
||||
- set min window size
|
||||
- add check for updates buttons
|
||||
- add open dashboard to the hotkey list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix profiles page undefined exception
|
||||
|
||||
---
|
||||
|
||||
## v1.3.6
|
||||
|
||||
### Features
|
||||
|
||||
- add russian translation
|
||||
- support to show connection detail
|
||||
- support clash meta memory usage display
|
||||
- support proxy provider update ui
|
||||
- update geo data file from meta repo
|
||||
- adjust setting page
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- center the window when it is out of screen
|
||||
- use `sudo` when `pkexec` not found (Linux)
|
||||
- reconnect websocket when window focus
|
||||
|
||||
### Notes
|
||||
|
||||
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
|
||||
|
||||
---
|
||||
|
||||
## v1.3.5
|
||||
|
||||
### Features
|
||||
|
||||
- update clash core
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix blurry system tray icon (Windows)
|
||||
- fix v1.3.4 wintun.dll not found (Windows)
|
||||
- fix v1.3.4 clash core not found (macOS, Linux)
|
||||
|
||||
---
|
||||
|
||||
## v1.3.4
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
- optimize traffic graph high CPU usage when window hidden
|
||||
- use polkit to elevate permission (Linux)
|
||||
- support app log level setting
|
||||
- support copy environment variable
|
||||
- overwrite resource file according to file modified
|
||||
- save window size and position
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove fallback group select status
|
||||
- enable context menu on editable element (Windows)
|
||||
|
||||
---
|
||||
|
||||
## v1.3.3
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
- show tray icon variants in different system proxy status (Windows)
|
||||
- close all connections when mode changed
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- encode controller secret into uri
|
||||
- error boundary for each page
|
||||
|
||||
---
|
||||
|
||||
## v1.3.2
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix import url issue
|
||||
- fix profile undefined issue
|
||||
|
||||
---
|
||||
|
||||
## v1.3.1
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix open url issue
|
||||
- fix appimage path panic
|
||||
- fix grant root permission in macOS
|
||||
- fix linux system proxy default bypass
|
||||
|
||||
---
|
||||
|
||||
## v1.3.0
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta
|
||||
- support opening dir on tray
|
||||
- support updating all profiles with one click
|
||||
- support granting root permission to clash core(Linux, macOS)
|
||||
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- deb add openssl depend(Linux)
|
||||
- fix the AppImage auto launch path(Linux)
|
||||
- fix get the default network service(macOS)
|
||||
- remove the esc key listener in macOS, cmd+w instead(macOS)
|
||||
- fix infinite retry when websocket error
|
||||
|
||||
---
|
||||
|
||||
## v1.2.3
|
||||
|
||||
### Features
|
||||
|
|
17
package.json
17
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "clash-verge",
|
||||
"version": "1.2.3",
|
||||
"version": "1.3.8",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
|
@ -24,14 +24,15 @@
|
|||
"@mui/icons-material": "^5.10.9",
|
||||
"@mui/material": "^5.10.13",
|
||||
"@mui/x-data-grid": "^5.17.11",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"ahooks": "^3.7.2",
|
||||
"axios": "^1.1.3",
|
||||
"dayjs": "1.11.5",
|
||||
"i18next": "^22.0.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-i18next": "^12.0.0",
|
||||
|
@ -43,12 +44,12 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.0.3",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.0.1",
|
||||
"adm-zip": "^0.5.9",
|
||||
"cross-env": "^7.0.3",
|
||||
|
@ -60,7 +61,7 @@
|
|||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.54.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^3.0.9",
|
||||
"vite": "^3.2.5",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^2.2.1"
|
||||
},
|
||||
|
|
|
@ -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.01.29";
|
||||
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.1";
|
||||
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`);
|
||||
}
|
||||
|
|
2105
src-tauri/Cargo.lock
generated
2105
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -16,37 +16,45 @@ tauri-build = { version = "1", features = [] }
|
|||
warp = "0.3"
|
||||
which = "4.2.2"
|
||||
anyhow = "1.0"
|
||||
dirs = "4.0.0"
|
||||
open = "3.2.0"
|
||||
dirs = "5.0.0"
|
||||
open = "4.0.1"
|
||||
log = "0.4.14"
|
||||
ctrlc = "3.2.3"
|
||||
dunce = "1.0.2"
|
||||
log4rs = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
chrono = "0.4.19"
|
||||
sysinfo = "0.27"
|
||||
sysproxy = "0.2"
|
||||
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 |
|
@ -175,6 +175,15 @@ pub async fn restart_sidecar() -> CmdResult {
|
|||
wrap_err!(CoreManager::global().run_core().await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn grant_permission(core: String) -> CmdResult {
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
return wrap_err!(manager::grant_permission(core));
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
return Err("Unsupported target".into());
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
|
@ -220,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()
|
||||
}
|
||||
|
@ -138,9 +138,9 @@ impl IProfiles {
|
|||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.context(format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.context(format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
|
@ -155,7 +155,7 @@ impl IProfiles {
|
|||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
|
@ -189,7 +189,7 @@ impl IProfiles {
|
|||
if let Some(items) = self.items.as_mut() {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
|
@ -207,9 +207,9 @@ impl IProfiles {
|
|||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.context(format!("failed to create file \"{}\"", file))?
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.context(format!("failed to write to file \"{}\"", file))?;
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
|
@ -8,6 +9,10 @@ pub struct IVerge {
|
|||
/// app listening port for app singleton
|
||||
pub app_singleton_port: Option<u16>,
|
||||
|
||||
/// app log level
|
||||
/// silent | error | warn | info | debug | trace
|
||||
pub app_log_level: Option<String>,
|
||||
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
|
@ -21,6 +26,9 @@ pub struct IVerge {
|
|||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// show memory info (only for Clash Meta)
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// clash tun mode
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
|
||||
|
@ -66,11 +74,22 @@ pub struct IVerge {
|
|||
/// 默认的延迟测试连接
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为真
|
||||
pub enable_clash_fields: Option<bool>,
|
||||
|
||||
/// 是否使用内部的脚本支持,默认为真
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
|
||||
/// 日志清理
|
||||
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
|
||||
pub auto_log_clean: Option<i32>,
|
||||
|
||||
/// window size and position
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_size_position: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -113,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),
|
||||
|
@ -120,6 +140,8 @@ impl IVerge {
|
|||
proxy_guard_duration: Some(30),
|
||||
auto_close_connection: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
enable_clash_fields: Some(true),
|
||||
auto_log_clean: Some(3),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
@ -140,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);
|
||||
|
@ -163,6 +187,9 @@ impl IVerge {
|
|||
patch!(default_latency_test);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(enable_clash_fields);
|
||||
patch!(auto_log_clean);
|
||||
patch!(window_size_position);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
|
@ -177,4 +204,21 @@ impl IVerge {
|
|||
Err(_) => SERVER_PORT, // 这里就不log错误了
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取日志等级
|
||||
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}");
|
||||
}
|
||||
|
@ -136,6 +140,7 @@ impl CoreManager {
|
|||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("clash".into());
|
||||
let is_clash = clash_core == "clash";
|
||||
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
|
||||
|
@ -167,8 +172,12 @@ impl CoreManager {
|
|||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
let stdout = clash_api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {stdout}");
|
||||
if is_clash {
|
||||
let stdout = clash_api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {stdout}");
|
||||
} else {
|
||||
log::info!(target: "app", "[clash]: {line}");
|
||||
};
|
||||
Logger::global().set_log(line);
|
||||
}
|
||||
CommandEvent::Stderr(err) => {
|
||||
|
|
|
@ -4,7 +4,7 @@ use once_cell::sync::OnceCell;
|
|||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{AppHandle, GlobalShortcutManager};
|
||||
use tauri_runtime_wry::wry::application::accelerator::Accelerator;
|
||||
use wry::application::accelerator::Accelerator;
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
|
@ -76,6 +76,7 @@ impl Hotkey {
|
|||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_dashboard" => || feat::open_dashboard(),
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
|
|
82
src-tauri/src/core/manager.rs
Normal file
82
src-tauri/src/core/manager.rs
Normal file
|
@ -0,0 +1,82 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
/// 给clash内核的tun模式授权
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
||||
use std::process::Command;
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let path = current_exe()?.with_file_name(core).canonicalize()?;
|
||||
let path = path.display().to_string();
|
||||
|
||||
log::debug!("grant_permission path: {path}");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let output = {
|
||||
// the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash
|
||||
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
|
||||
// let path = escape(&path);
|
||||
let path = path.replace(' ', "\\\\ ");
|
||||
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
||||
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
||||
Command::new("osascript")
|
||||
.args(vec!["-e", &command])
|
||||
.output()?
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let output = {
|
||||
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
||||
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
|
||||
|
||||
let sudo = match Command::new("which").arg("pkexec").output() {
|
||||
Ok(output) => {
|
||||
if output.stdout.is_empty() {
|
||||
"sudo"
|
||||
} else {
|
||||
"pkexec"
|
||||
}
|
||||
}
|
||||
Err(_) => "sudo",
|
||||
};
|
||||
|
||||
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
|
||||
anyhow::bail!("{stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn escape<'a>(text: &'a str) -> Cow<'a, str> {
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
let mut owned = None;
|
||||
|
||||
for pos in 0..bytes.len() {
|
||||
let special = match bytes[pos] {
|
||||
b' ' => Some(b' '),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(s) = special {
|
||||
if owned.is_none() {
|
||||
owned = Some(bytes[0..pos].to_owned());
|
||||
}
|
||||
owned.as_mut().unwrap().push(b'\\');
|
||||
owned.as_mut().unwrap().push(b'\\');
|
||||
owned.as_mut().unwrap().push(s);
|
||||
} else if let Some(owned) = owned.as_mut() {
|
||||
owned.push(bytes[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(owned) = owned {
|
||||
unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) }
|
||||
} else {
|
||||
unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) }
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ mod core;
|
|||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod logger;
|
||||
pub mod manager;
|
||||
pub mod sysopt;
|
||||
pub mod timer;
|
||||
pub mod tray;
|
||||
|
|
|
@ -25,7 +25,7 @@ pub struct Sysopt {
|
|||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,<local>";
|
||||
|
||||
|
@ -171,6 +171,24 @@ impl Sysopt {
|
|||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
// fix #403
|
||||
#[cfg(target_os = "linux")]
|
||||
let app_path = {
|
||||
use crate::core::handle::Handle;
|
||||
use tauri::Manager;
|
||||
|
||||
let handle = Handle::global();
|
||||
match handle.app_handle.lock().as_ref() {
|
||||
Some(app_handle) => {
|
||||
let appimage = app_handle.env().appimage;
|
||||
appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(app_path)
|
||||
}
|
||||
None => app_path,
|
||||
}
|
||||
};
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(&app_path)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{config::Config, feat, utils::resolve};
|
||||
use crate::{cmds, config::Config, feat, utils::resolve};
|
||||
use anyhow::Result;
|
||||
use tauri::{
|
||||
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
|
@ -13,53 +13,81 @@ impl Tray {
|
|||
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
|
||||
if zh {
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "打开面板"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("rule_mode", "规则模式"))
|
||||
.add_item(CustomMenuItem::new("global_mode", "全局模式"))
|
||||
.add_item(CustomMenuItem::new("direct_mode", "直连模式"))
|
||||
.add_item(CustomMenuItem::new("script_mode", "脚本模式"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("system_proxy", "系统代理"))
|
||||
.add_item(CustomMenuItem::new("tun_mode", "TUN 模式"))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"更多",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart_clash", "重启 Clash"))
|
||||
.add_item(CustomMenuItem::new("restart_app", "重启应用"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}"))
|
||||
.disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q"))
|
||||
} else {
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "Dashboard"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("rule_mode", "Rule Mode"))
|
||||
.add_item(CustomMenuItem::new("global_mode", "Global Mode"))
|
||||
.add_item(CustomMenuItem::new("direct_mode", "Direct Mode"))
|
||||
.add_item(CustomMenuItem::new("script_mode", "Script Mode"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
|
||||
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
"More",
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
|
||||
.add_item(CustomMenuItem::new("restart_app", "Restart App"))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}"))
|
||||
.disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"))
|
||||
macro_rules! t {
|
||||
($en: expr, $zh: expr) => {
|
||||
if zh {
|
||||
$zh
|
||||
} else {
|
||||
$en
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_window",
|
||||
t!("Dashboard", "打开面板"),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"rule_mode",
|
||||
t!("Rule Mode", "规则模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"global_mode",
|
||||
t!("Global Mode", "全局模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"direct_mode",
|
||||
t!("Direct Mode", "直连模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"script_mode",
|
||||
t!("Script Mode", "脚本模式"),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"system_proxy",
|
||||
t!("System Proxy", "系统代理"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env",
|
||||
t!("Copy Env", "复制环境变量"),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("Open Dir", "打开目录"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_app_dir",
|
||||
t!("App Dir", "应用目录"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_core_dir",
|
||||
t!("Core Dir", "内核目录"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_logs_dir",
|
||||
t!("Logs Dir", "日志目录"),
|
||||
)),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("More", "更多"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_clash",
|
||||
t!("Restart Clash", "重启 Clash"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_app",
|
||||
t!("Restart App", "重启应用"),
|
||||
))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
|
||||
}
|
||||
|
||||
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
||||
|
@ -93,6 +121,17 @@ impl Tray {
|
|||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let indication_icon = if *system_proxy {
|
||||
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
|
||||
} else {
|
||||
include_bytes!("../../icons/win-tray-icon.png").to_vec()
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
|
||||
}
|
||||
|
||||
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
|
||||
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
|
||||
|
||||
|
@ -110,9 +149,15 @@ impl Tray {
|
|||
"open_window" => resolve::create_window(app_handle),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env" => feat::copy_clash_env(),
|
||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => api::process::restart(&app_handle.env()),
|
||||
"quit" => {
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||
"mode",
|
||||
|
@ -20,7 +21,7 @@ pub const DEFAULT_FIELDS: [&str; 5] = [
|
|||
"rule-providers",
|
||||
];
|
||||
|
||||
pub const OTHERS_FIELDS: [&str; 25] = [
|
||||
pub const OTHERS_FIELDS: [&str; 30] = [
|
||||
"dns",
|
||||
"tun",
|
||||
"ebpf",
|
||||
|
@ -39,13 +40,18 @@ pub const OTHERS_FIELDS: [&str; 25] = [
|
|||
"external-ui",
|
||||
"bind-address",
|
||||
"authentication",
|
||||
"tls", // meta
|
||||
"sniffer", // meta
|
||||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"tls", // meta
|
||||
"sniffer", // meta
|
||||
"geox-url", // meta
|
||||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
"external-controller-tls", // meta
|
||||
"global-client-fingerprint", // meta
|
||||
];
|
||||
|
||||
pub fn use_clash_fields() -> Vec<String> {
|
||||
|
@ -68,7 +74,11 @@ pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
|
||||
pub fn use_filter(config: Mapping, filter: &Vec<String>, enable: bool) -> Mapping {
|
||||
if !enable {
|
||||
return config;
|
||||
}
|
||||
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
|
@ -94,7 +104,7 @@ pub fn use_lowercase(config: Mapping) -> Mapping {
|
|||
ret
|
||||
}
|
||||
|
||||
pub fn use_sort(config: Mapping) -> Mapping {
|
||||
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
HANDLE_FIELDS
|
||||
|
@ -107,6 +117,28 @@ pub fn use_sort(config: Mapping) -> Mapping {
|
|||
ret.insert(key, value.clone());
|
||||
});
|
||||
});
|
||||
|
||||
if !enable_filter {
|
||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS
|
||||
.into_iter()
|
||||
.chain(OTHERS_FIELDS)
|
||||
.chain(DEFAULT_FIELDS)
|
||||
.collect();
|
||||
|
||||
let config_keys: HashSet<&str> = config
|
||||
.keys()
|
||||
.filter_map(|e| e.as_str())
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||
let key = Value::from(key);
|
||||
config.get(&key).map(|value| {
|
||||
ret.insert(key, value.clone());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use super::{use_filter, use_lowercase};
|
||||
use serde_yaml::{self, Mapping, Sequence, Value};
|
||||
|
||||
#[allow(unused)]
|
||||
const MERGE_FIELDS: [&str; 6] = [
|
||||
"prepend-rules",
|
||||
"append-rules",
|
||||
|
@ -20,7 +19,7 @@ pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
|||
});
|
||||
|
||||
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
||||
let merge = use_filter(merge, &merge_list.collect());
|
||||
let merge = use_filter(merge, &merge_list.collect(), true);
|
||||
|
||||
["rules", "proxies", "proxy-groups"]
|
||||
.iter()
|
||||
|
|
|
@ -23,13 +23,14 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
// config.yaml 的配置
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
|
||||
let (clash_core, tun_mode, enable_builtin) = {
|
||||
let (clash_core, enable_tun, enable_builtin, enable_filter) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.clash_core.clone(),
|
||||
verge.enable_tun_mode.clone(),
|
||||
verge.enable_builtin_enhanced.clone(),
|
||||
verge.enable_tun_mode.clone().unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.clone().unwrap_or(true),
|
||||
verge.enable_clash_fields.clone().unwrap_or(true),
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -58,14 +59,14 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||
|
||||
let valid = use_valid_fields(valid);
|
||||
config = use_filter(config, &valid);
|
||||
config = use_filter(config, &valid, enable_filter);
|
||||
|
||||
// 处理用户的profile
|
||||
chain.into_iter().for_each(|item| match item.data {
|
||||
ChainType::Merge(merge) => {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
config = use_merge(merge, config.to_owned());
|
||||
config = use_filter(config.to_owned(), &valid);
|
||||
config = use_filter(config.to_owned(), &valid, enable_filter);
|
||||
}
|
||||
ChainType::Script(script) => {
|
||||
let mut logs = vec![];
|
||||
|
@ -73,7 +74,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
config = use_filter(res_config, &valid);
|
||||
config = use_filter(res_config, &valid, enable_filter);
|
||||
logs.extend(res_logs);
|
||||
}
|
||||
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||
|
@ -91,7 +92,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
let clash_fields = use_clash_fields();
|
||||
|
||||
// 内建脚本最后跑
|
||||
if enable_builtin.unwrap_or(true) {
|
||||
if enable_builtin {
|
||||
ChainItem::builtin()
|
||||
.into_iter()
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
|
@ -102,7 +103,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
match item.data {
|
||||
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, _)) => {
|
||||
config = use_filter(res_config, &clash_fields);
|
||||
config = use_filter(res_config, &clash_fields, enable_filter);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "builtin script error `{err}`");
|
||||
|
@ -113,9 +114,9 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
|||
});
|
||||
}
|
||||
|
||||
config = use_filter(config, &clash_fields);
|
||||
config = use_tun(config, tun_mode.unwrap_or(false));
|
||||
config = use_sort(config);
|
||||
config = use_filter(config, &clash_fields, enable_filter);
|
||||
config = use_tun(config, enable_tun);
|
||||
config = use_sort(config, enable_filter);
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
|
||||
|
|
|
@ -7,8 +7,19 @@
|
|||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use wry::application::clipboard::Clipboard;
|
||||
|
||||
// 打开面板
|
||||
pub fn open_dashboard() {
|
||||
let handle = handle::Handle::global();
|
||||
let app_handle = handle.app_handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
resolve::create_window(app_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// 重启clash
|
||||
pub fn restart_clash_core() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ fn main() -> std::io::Result<()> {
|
|||
cmds::open_core_dir,
|
||||
// cmds::kill_sidecar,
|
||||
cmds::restart_sidecar,
|
||||
cmds::grant_permission,
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::get_clash_logs,
|
||||
|
@ -65,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")]
|
||||
|
@ -108,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();
|
||||
});
|
||||
|
@ -116,6 +121,20 @@ fn main() -> std::io::Result<()> {
|
|||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
}
|
||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
|
|||
use nanoid::nanoid;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{fs, path::PathBuf, process::Command, str::FromStr};
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
|
@ -11,19 +11,21 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
|||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(&path)
|
||||
.context(format!("failed to read the file \"{}\"", path.display()))?;
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
|
||||
serde_yaml::from_str::<T>(&yaml_str).context(format!(
|
||||
"failed to read the file with yaml format \"{}\"",
|
||||
path.display()
|
||||
))
|
||||
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||
format!(
|
||||
"failed to read the file with yaml format \"{}\"",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// read mapping from yaml fix #165
|
||||
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
let mut val: Value = read_yaml(path)?;
|
||||
val.apply_merge()
|
||||
.context(format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
Ok(val
|
||||
.as_mapping()
|
||||
|
@ -45,7 +47,8 @@ pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -
|
|||
};
|
||||
|
||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||
fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
|
||||
fs::write(path, yaml_str.as_bytes())
|
||||
.with_context(|| format!("failed to save file \"{path_str}\""))
|
||||
}
|
||||
|
||||
const ALPHABET: [char; 62] = [
|
||||
|
@ -79,29 +82,18 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
|||
/// open file
|
||||
/// use vscode by default
|
||||
pub fn open_file(path: PathBuf) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let code = "Visual Studio Code";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let code = "code";
|
||||
|
||||
// use vscode first
|
||||
if let Ok(code) = which::which("code") {
|
||||
let mut command = Command::new(&code);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
if let Err(err) = command.creation_flags(0x08000000).arg(&path).spawn() {
|
||||
log::error!(target: "app", "failed to open with VScode `{err}`");
|
||||
open::that(path)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err(err) = command.arg(&path).spawn() {
|
||||
log::error!(target: "app", "failed to open with VScode `{err}`");
|
||||
open::that(path)?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
if let Err(err) = open::with(&path, code) {
|
||||
log::error!(target: "app", "failed to open file with VScode `{err}`");
|
||||
// default open
|
||||
open::that(path)?;
|
||||
}
|
||||
|
||||
open::that(path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -127,6 +119,15 @@ macro_rules! log_err {
|
|||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use crate::config::*;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use chrono::{DateTime, Local};
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Logger, Root};
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs;
|
||||
use std::fs::{self, DirEntry};
|
||||
use std::str::FromStr;
|
||||
use tauri::PackageInfo;
|
||||
|
||||
/// initialize this instance's log file
|
||||
|
@ -17,42 +18,126 @@ fn init_log() -> Result<()> {
|
|||
let _ = fs::create_dir_all(&log_dir);
|
||||
}
|
||||
|
||||
let log_level = Config::verge().data().get_log_level();
|
||||
if log_level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {M} {m}{n}";
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}";
|
||||
let log_pattern = match log_level {
|
||||
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
|
||||
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
||||
};
|
||||
|
||||
let encode = Box::new(PatternEncoder::new(time_format));
|
||||
let encode = Box::new(PatternEncoder::new(log_pattern));
|
||||
|
||||
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
|
||||
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
let level = LevelFilter::Debug;
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
let level = LevelFilter::Info;
|
||||
let mut logger_builder = Logger::builder();
|
||||
let mut root_builder = Root::builder();
|
||||
|
||||
let config = Config::builder()
|
||||
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
{
|
||||
logger_builder = logger_builder.appenders(["file", "stdout"]);
|
||||
if log_more {
|
||||
root_builder = root_builder.appenders(["file", "stdout"]);
|
||||
} else {
|
||||
root_builder = root_builder.appenders(["stdout"]);
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
{
|
||||
logger_builder = logger_builder.appenders(["file"]);
|
||||
if log_more {
|
||||
root_builder = root_builder.appenders(["file"]);
|
||||
}
|
||||
}
|
||||
|
||||
let (config, _) = log4rs::config::Config::builder()
|
||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||
.logger(
|
||||
Logger::builder()
|
||||
.appenders(["file", "stdout"])
|
||||
.additive(false)
|
||||
.build("app", level),
|
||||
)
|
||||
.build(Root::builder().appender("stdout").build(LevelFilter::Info))?;
|
||||
.logger(logger_builder.additive(false).build("app", log_level))
|
||||
.build_lossy(root_builder.build(log_level));
|
||||
|
||||
log4rs::init_config(config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the files from resources
|
||||
/// 删除log文件
|
||||
pub fn delete_log() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
if !log_dir.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auto_log_clean = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.data();
|
||||
verge.auto_log_clean.clone().unwrap_or(0)
|
||||
};
|
||||
|
||||
let day = match auto_log_clean {
|
||||
1 => 7,
|
||||
2 => 30,
|
||||
3 => 90,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "try to delete log files, day: {day}");
|
||||
|
||||
// %Y-%m-%d to NaiveDateTime
|
||||
let parse_time_str = |s: &str| {
|
||||
let sa: Vec<&str> = s.split('-').collect();
|
||||
if sa.len() != 4 {
|
||||
return Err(anyhow::anyhow!("invalid time str"));
|
||||
}
|
||||
|
||||
let year = i32::from_str(sa[0])?;
|
||||
let month = u32::from_str(sa[1])?;
|
||||
let day = u32::from_str(sa[2])?;
|
||||
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
|
||||
.ok_or(anyhow::anyhow!("invalid time str"))?
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.ok_or(anyhow::anyhow!("invalid time str"))?;
|
||||
Ok(time)
|
||||
};
|
||||
|
||||
let process_file = |file: DirEntry| -> Result<()> {
|
||||
let file_name = file.file_name();
|
||||
let file_name = file_name.to_str().unwrap_or_default();
|
||||
|
||||
if file_name.ends_with(".log") {
|
||||
let now = Local::now();
|
||||
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
|
||||
let file_time = DateTime::<Local>::from_local(created_time, now.offset().clone());
|
||||
|
||||
let duration = now.signed_duration_since(file_time);
|
||||
if duration.num_days() > day {
|
||||
let file_path = file.path();
|
||||
let _ = fs::remove_file(file_path);
|
||||
log::info!(target: "app", "delete log file: {file_name}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
for file in fs::read_dir(&log_dir)? {
|
||||
if let Ok(file) = file {
|
||||
let _ = process_file(file);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
|
@ -60,6 +145,7 @@ pub fn init_config() -> Result<()> {
|
|||
}
|
||||
|
||||
let _ = init_log();
|
||||
let _ = delete_log();
|
||||
|
||||
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||
if !app_dir.exists() {
|
||||
|
@ -97,7 +183,8 @@ pub fn init_config() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize app
|
||||
/// initialize app resources
|
||||
/// after tauri setup
|
||||
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir(package_info)?;
|
||||
|
@ -109,13 +196,47 @@ pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
|||
let _ = fs::create_dir_all(&res_dir);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||
|
||||
// copy the resource file
|
||||
for file in ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"].iter() {
|
||||
// if the source file is newer than the destination file, copy it over
|
||||
for file in file_list.iter() {
|
||||
let src_path = res_dir.join(file);
|
||||
let target_path = app_dir.join(file);
|
||||
if src_path.exists() {
|
||||
let _ = fs::copy(src_path, target_path);
|
||||
let dest_path = app_dir.join(file);
|
||||
|
||||
let handle_copy = || {
|
||||
match fs::copy(&src_path, &dest_path) {
|
||||
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "failed to copy resources '{file}', {err}")
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if src_path.exists() && !dest_path.exists() {
|
||||
handle_copy();
|
||||
continue;
|
||||
}
|
||||
|
||||
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
||||
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
||||
|
||||
match (src_modified, dest_modified) {
|
||||
(Ok(src_modified), Ok(dest_modified)) => {
|
||||
if src_modified > dest_modified {
|
||||
handle_copy();
|
||||
} else {
|
||||
log::debug!(target: "app", "skipping resource copy '{file}'");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::debug!(target: "app", "failed to get modified '{file}'");
|
||||
handle_copy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::config::Config;
|
||||
use crate::log_err;
|
||||
use crate::{core::*, utils::init, utils::server};
|
||||
use crate::{config::Config, core::*, utils::init, utils::server};
|
||||
use crate::{log_err, trace_err};
|
||||
use anyhow::Result;
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
|
||||
/// handle something when start app
|
||||
|
@ -13,12 +13,17 @@ pub fn resolve_setup(app: &mut App) {
|
|||
log_err!(init::init_resources(app.package_info()));
|
||||
|
||||
// 启动核心
|
||||
log::trace!("init config");
|
||||
log_err!(Config::init_config());
|
||||
|
||||
log::trace!("launch core");
|
||||
log_err!(CoreManager::global().init());
|
||||
|
||||
// setup a simple http server for singleton
|
||||
log::trace!("launch embed server");
|
||||
server::embed_server(app.app_handle());
|
||||
|
||||
log::trace!("init system tray");
|
||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||
|
||||
let silent_start = { Config::verge().data().enable_silent_start.clone() };
|
||||
|
@ -43,22 +48,47 @@ pub fn resolve_reset() {
|
|||
/// create main window
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
let _ = window.unminimize();
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
return;
|
||||
}
|
||||
|
||||
let builder = tauri::window::WindowBuilder::new(
|
||||
let mut builder = tauri::window::WindowBuilder::new(
|
||||
app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WindowUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Verge")
|
||||
.center()
|
||||
.fullscreen(false)
|
||||
.min_inner_size(600.0, 520.0);
|
||||
|
||||
match Config::verge().latest().window_size_position.clone() {
|
||||
Some(size_pos) if size_pos.len() == 4 => {
|
||||
let size = (size_pos[0], size_pos[1]);
|
||||
let pos = (size_pos[2], size_pos[3]);
|
||||
let w = size.0.clamp(600.0, f64::INFINITY);
|
||||
let h = size.1.clamp(520.0, f64::INFINITY);
|
||||
builder = builder.inner_size(w, h).position(pos.0, pos.1);
|
||||
}
|
||||
_ => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 636.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::time::Duration;
|
||||
|
@ -68,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.2.3"
|
||||
"version": "1.3.8"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
|
@ -32,7 +32,7 @@
|
|||
"shortDescription": "A Clash GUI based on tauri.",
|
||||
"longDescription": "A Clash GUI based on tauri.",
|
||||
"deb": {
|
||||
"depends": []
|
||||
"depends": ["openssl"]
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
|
@ -46,15 +46,15 @@
|
|||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US"]
|
||||
"language": ["zh-CN", "en-US", "ru-RU"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
|
||||
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
|
||||
"https://ghproxy.com/https://github.com/zzzgydi/clash-verge/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { forwardRef, ReactNode, useImperativeHandle, useState } from "react";
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
|
@ -18,6 +18,7 @@ interface Props {
|
|||
disableCancel?: boolean;
|
||||
disableFooter?: boolean;
|
||||
contentSx?: SxProps<Theme>;
|
||||
children?: ReactNode;
|
||||
onOk?: () => void;
|
||||
onCancel?: () => void;
|
||||
onClose?: () => void;
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import { ReactNode } from "react";
|
||||
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
|
||||
|
||||
function ErrorFallback({ error }: FallbackProps) {
|
||||
return (
|
||||
<div role="alert">
|
||||
<p>Something went wrong:(</p>
|
||||
<div role="alert" style={{ padding: 16 }}>
|
||||
<h4>Something went wrong:(</h4>
|
||||
|
||||
<pre>{error.message}</pre>
|
||||
|
||||
<details title="Error Stack">
|
||||
<summary>Error Stack</summary>
|
||||
<pre>{error.stack}</pre>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const BaseErrorBoundary: React.FC = (props) => {
|
||||
interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const BaseErrorBoundary = (props: Props) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{props.children}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
|
||||
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material";
|
||||
|
@ -77,13 +77,14 @@ export const Notice: NoticeInstance = (props) => {
|
|||
|
||||
const container = document.createElement("div");
|
||||
parent.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
const onUnmount = () => {
|
||||
const result = ReactDOM.unmountComponentAtNode(container);
|
||||
if (result && parent) setTimeout(() => parent.removeChild(container), 500);
|
||||
root.unmount();
|
||||
if (parent) setTimeout(() => parent.removeChild(container), 500);
|
||||
};
|
||||
|
||||
ReactDOM.render(<NoticeInner {...props} onClose={onUnmount} />, container);
|
||||
root.render(<NoticeInner {...props} onClose={onUnmount} />);
|
||||
};
|
||||
|
||||
(["info", "error", "success"] as const).forEach((type) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Typography } from "@mui/material";
|
||||
import { BaseErrorBoundary } from "./base-error-boundary";
|
||||
|
||||
|
@ -6,6 +6,7 @@ interface Props {
|
|||
title?: React.ReactNode; // the page title
|
||||
header?: React.ReactNode; // something behind title
|
||||
contentStyle?: React.CSSProperties;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const BasePage: React.FC<Props> = (props) => {
|
||||
|
|
104
src/components/connection/connection-detail.tsx
Normal file
104
src/components/connection/connection-detail.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import dayjs from "dayjs";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Snackbar } from "@mui/material";
|
||||
import { deleteConnection } from "@/services/api";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
export interface ConnectionDetailRef {
|
||||
open: (detail: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
|
||||
(props, ref) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<IConnectionsItem>(null!);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: (detail: IConnectionsItem) => {
|
||||
if (open) return;
|
||||
setOpen(true);
|
||||
setDetail(detail);
|
||||
},
|
||||
}));
|
||||
|
||||
const onClose = () => setOpen(false);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
message={
|
||||
detail ? (
|
||||
<InnerConnectionDetail data={detail} onClose={onClose} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface InnerProps {
|
||||
data: IConnectionsItem;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
|
||||
const { metadata, rulePayload } = data;
|
||||
const chains = [...data.chains].reverse().join(" / ");
|
||||
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
|
||||
const host = metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`;
|
||||
|
||||
const information = [
|
||||
{ label: "Host", value: host },
|
||||
{ label: "Download", value: parseTraffic(data.download).join(" ") },
|
||||
{ label: "Upload", value: parseTraffic(data.upload).join(" ") },
|
||||
{
|
||||
label: "DL Speed",
|
||||
value: parseTraffic(data.curDownload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
label: "UL Speed",
|
||||
value: parseTraffic(data.curUpload ?? -1).join(" ") + "/s",
|
||||
},
|
||||
{ label: "Chains", value: chains },
|
||||
{ label: "Rule", value: rule },
|
||||
{
|
||||
label: "Process",
|
||||
value: truncateStr(metadata.process || metadata.processPath),
|
||||
},
|
||||
{ label: "Time", value: dayjs(data.start).fromNow() },
|
||||
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
|
||||
{ label: "Destination IP", value: metadata.destinationIP },
|
||||
{ label: "Type", value: `${metadata.type}(${metadata.network})` },
|
||||
];
|
||||
|
||||
const onDelete = useLockFn(async () => deleteConnection(data.id));
|
||||
|
||||
return (
|
||||
<Box sx={{ userSelect: "text" }}>
|
||||
{information.map((each) => (
|
||||
<div key={each.label}>
|
||||
<b>{each.label}</b>: <span>{each.value}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Box sx={{ textAlign: "right" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
title="Close Connection"
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -24,10 +24,11 @@ const Tag = styled("span")(({ theme }) => ({
|
|||
|
||||
interface Props {
|
||||
value: IConnectionsItem;
|
||||
onShowDetail?: () => void;
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
const { value } = props;
|
||||
export const ConnectionItem = (props: Props) => {
|
||||
const { value, onShowDetail } = props;
|
||||
|
||||
const { id, metadata, chains, start, curUpload, curDownload } = value;
|
||||
|
||||
|
@ -44,8 +45,9 @@ const ConnectionItem = (props: Props) => {
|
|||
}
|
||||
>
|
||||
<ListItemText
|
||||
sx={{ userSelect: "text" }}
|
||||
sx={{ userSelect: "text", cursor: "pointer" }}
|
||||
primary={metadata.host || metadata.destinationIP}
|
||||
onClick={onShowDetail}
|
||||
secondary={
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap" }}>
|
||||
<Tag sx={{ textTransform: "uppercase", color: "success" }}>
|
||||
|
@ -56,7 +58,7 @@ const ConnectionItem = (props: Props) => {
|
|||
|
||||
{!!metadata.process && <Tag>{metadata.process}</Tag>}
|
||||
|
||||
{chains.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
{chains?.length > 0 && <Tag>{chains[value.chains.length - 1]}</Tag>}
|
||||
|
||||
<Tag>{dayjs(start).fromNow()}</Tag>
|
||||
|
||||
|
@ -71,5 +73,3 @@ const ConnectionItem = (props: Props) => {
|
|||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionItem;
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
import dayjs from "dayjs";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DataGrid, GridColDef } from "@mui/x-data-grid";
|
||||
import { truncateStr } from "@/utils/truncate-str";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
|
||||
interface Props {
|
||||
connections: IConnectionsItem[];
|
||||
onShowDetail: (data: IConnectionsItem) => void;
|
||||
}
|
||||
|
||||
const ConnectionTable = (props: Props) => {
|
||||
const { connections } = props;
|
||||
export const ConnectionTable = (props: Props) => {
|
||||
const { connections, onShowDetail } = props;
|
||||
|
||||
const [columnVisible, setColumnVisible] = useState<
|
||||
Partial<Record<keyof IConnectionsItem, boolean>>
|
||||
>({});
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: "host",
|
||||
headerName: "Host",
|
||||
flex: 200,
|
||||
minWidth: 200,
|
||||
resizable: false,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "host", headerName: "Host", flex: 220, minWidth: 220 },
|
||||
{
|
||||
field: "download",
|
||||
headerName: "Download",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "upload",
|
||||
|
@ -34,18 +31,13 @@ const ConnectionTable = (props: Props) => {
|
|||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) => parseTraffic(params.value).join(" "),
|
||||
},
|
||||
{
|
||||
field: "dlSpeed",
|
||||
headerName: "DL Speed",
|
||||
align: "right",
|
||||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "ulSpeed",
|
||||
|
@ -53,55 +45,26 @@ const ConnectionTable = (props: Props) => {
|
|||
width: 88,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params: any) =>
|
||||
parseTraffic(params.value).join(" ") + "/s",
|
||||
},
|
||||
{
|
||||
field: "chains",
|
||||
headerName: "Chains",
|
||||
width: 360,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "rule",
|
||||
headerName: "Rule",
|
||||
width: 225,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "process",
|
||||
headerName: "Process",
|
||||
width: 120,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "chains", headerName: "Chains", flex: 360, minWidth: 360 },
|
||||
{ field: "rule", headerName: "Rule", flex: 300, minWidth: 250 },
|
||||
{ field: "process", headerName: "Process", flex: 480, minWidth: 480 },
|
||||
{
|
||||
field: "time",
|
||||
headerName: "Time",
|
||||
width: 120,
|
||||
flex: 120,
|
||||
minWidth: 100,
|
||||
align: "right",
|
||||
headerAlign: "right",
|
||||
disableColumnMenu: true,
|
||||
valueFormatter: (params) => dayjs(params.value).fromNow(),
|
||||
},
|
||||
{
|
||||
field: "source",
|
||||
headerName: "Source",
|
||||
width: 150,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{ field: "source", headerName: "Source", flex: 200, minWidth: 130 },
|
||||
{
|
||||
field: "destinationIP",
|
||||
headerName: "Destination IP",
|
||||
width: 125,
|
||||
disableColumnMenu: true,
|
||||
},
|
||||
{
|
||||
field: "type",
|
||||
headerName: "Type",
|
||||
width: 160,
|
||||
disableColumnMenu: true,
|
||||
flex: 200,
|
||||
minWidth: 130,
|
||||
},
|
||||
{ field: "type", headerName: "Type", flex: 160, minWidth: 100 },
|
||||
];
|
||||
|
||||
const connRows = useMemo(() => {
|
||||
|
@ -115,30 +78,33 @@ const ConnectionTable = (props: Props) => {
|
|||
host: metadata.host
|
||||
? `${metadata.host}:${metadata.destinationPort}`
|
||||
: `${metadata.destinationIP}:${metadata.destinationPort}`,
|
||||
download: each.download,
|
||||
upload: each.upload,
|
||||
dlSpeed: each.curDownload,
|
||||
ulSpeed: each.curUpload,
|
||||
download: parseTraffic(each.download).join(" "),
|
||||
upload: parseTraffic(each.upload).join(" "),
|
||||
dlSpeed: parseTraffic(each.curDownload).join(" ") + "/s",
|
||||
ulSpeed: parseTraffic(each.curUpload).join(" ") + "/s",
|
||||
chains,
|
||||
rule,
|
||||
process: metadata.process || metadata.processPath,
|
||||
time: each.start,
|
||||
process: truncateStr(metadata.process || metadata.processPath),
|
||||
time: dayjs(each.start).fromNow(),
|
||||
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
|
||||
destinationIP: metadata.destinationIP,
|
||||
type: `${metadata.type}(${metadata.network})`,
|
||||
|
||||
connectionData: each,
|
||||
};
|
||||
});
|
||||
}, [connections]);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
hideFooter
|
||||
rows={connRows}
|
||||
columns={columns}
|
||||
onRowClick={(e) => onShowDetail(e.row.connectionData)}
|
||||
density="compact"
|
||||
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
|
||||
hideFooter
|
||||
columnVisibilityModel={columnVisible}
|
||||
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionTable;
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
HorizontalRuleRounded,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const LayoutControl = () => {
|
||||
export const LayoutControl = () => {
|
||||
const minWidth = 40;
|
||||
|
||||
return (
|
||||
|
@ -37,5 +37,3 @@ const LayoutControl = () => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutControl;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { alpha, ListItem, ListItemButton, ListItemText } from "@mui/material";
|
|||
import { useMatch, useResolvedPath, useNavigate } from "react-router-dom";
|
||||
import type { LinkProps } from "react-router-dom";
|
||||
|
||||
const LayoutItem = (props: LinkProps) => {
|
||||
export const LayoutItem = (props: LinkProps) => {
|
||||
const { to, children } = props;
|
||||
|
||||
const resolved = useResolvedPath(to);
|
||||
|
@ -40,5 +40,3 @@ const LayoutItem = (props: LinkProps) => {
|
|||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutItem;
|
||||
|
|
|
@ -1,93 +1,83 @@
|
|||
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 wsRef = useRef<WebSocket | null>(null);
|
||||
const [refresh, setRefresh] = useState({});
|
||||
const [memory, setMemory] = useState({ inuse: 0 });
|
||||
const pageVisible = useVisibility();
|
||||
|
||||
// setup log ws during layout
|
||||
useLogSetup();
|
||||
|
||||
const { connect, disconnect } = useWebsocket((event) => {
|
||||
const data = JSON.parse(event.data) as ITrafficItem;
|
||||
trafficRef.current?.appendData(data);
|
||||
setTraffic(data);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
if (!clashInfo || !pageVisible) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
const ws = new WebSocket(`ws://${server}/traffic?token=${secret}`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const data = JSON.parse(event.data) as ITrafficItem;
|
||||
trafficRef.current?.appendData(data);
|
||||
setTraffic(data);
|
||||
});
|
||||
|
||||
ws.addEventListener("error", () => {
|
||||
setTimeout(() => {
|
||||
if (document.visibilityState === "visible") {
|
||||
setRefresh({});
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
setTimeout(() => {
|
||||
if (document.visibilityState === "visible") {
|
||||
setRefresh({});
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
wsRef.current = ws;
|
||||
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
return () => {
|
||||
ws?.close();
|
||||
wsRef.current = null;
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo, refresh]);
|
||||
}, [clashInfo, pageVisible]);
|
||||
|
||||
/* --------- meta memory information --------- */
|
||||
const isMetaCore = verge?.clash_core === "clash-meta";
|
||||
const displayMemory = isMetaCore && (verge?.enable_memory_usage ?? true);
|
||||
|
||||
const memoryWs = useWebsocket(
|
||||
(event) => {
|
||||
setMemory(JSON.parse(event.data));
|
||||
},
|
||||
{ onError: () => setMemory({ inuse: 0 }) }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleVisibleChange = () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
// reconnect websocket
|
||||
if (
|
||||
wsRef.current &&
|
||||
wsRef.current.readyState !== WebSocket.CONNECTING
|
||||
) {
|
||||
setRefresh({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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",
|
||||
|
@ -103,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 };
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import dayjs from "dayjs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import { getClashLogs } from "@/services/cmds";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { atomEnableLog, atomLogData } from "@/services/states";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
|
||||
const MAX_LOG_NUM = 1000;
|
||||
|
||||
|
@ -14,7 +15,14 @@ export const useLogSetup = () => {
|
|||
const enableLog = useRecoilValue(atomEnableLog);
|
||||
const setLogData = useSetRecoilState(atomLogData);
|
||||
|
||||
const [refresh, setRefresh] = useState({});
|
||||
const { connect, disconnect } = useWebsocket((event) => {
|
||||
const data = JSON.parse(event.data) as ILogItem;
|
||||
const time = dayjs().format("MM-DD HH:mm:ss");
|
||||
setLogData((l) => {
|
||||
if (l.length >= MAX_LOG_NUM) l.shift();
|
||||
return [...l, { ...data, time }];
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableLog || !clashInfo) return;
|
||||
|
@ -22,21 +30,10 @@ export const useLogSetup = () => {
|
|||
getClashLogs().then(setLogData);
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
const ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||
connect(`ws://${server}/logs?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const data = JSON.parse(event.data) as ILogItem;
|
||||
const time = dayjs().format("MM-DD HH:mm:ss");
|
||||
setLogData((l) => {
|
||||
if (l.length >= MAX_LOG_NUM) l.shift();
|
||||
return [...l, { ...data, time }];
|
||||
});
|
||||
});
|
||||
|
||||
ws.addEventListener("error", () => {
|
||||
setTimeout(() => setRefresh({}), 1000);
|
||||
});
|
||||
|
||||
return () => ws?.close();
|
||||
}, [clashInfo, enableLog, refresh]);
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo, enableLog]);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||
<ListItemText
|
||||
primary={group.name}
|
||||
secondary={
|
||||
<Box
|
||||
<ListItemTextChild
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
|
@ -50,7 +50,7 @@ export const ProxyRender = (props: RenderProps) => {
|
|||
>
|
||||
<StyledTypeBox>{group.type}</StyledTypeBox>
|
||||
<StyledSubtitle>{group.now}</StyledSubtitle>
|
||||
</Box>
|
||||
</ListItemTextChild>
|
||||
}
|
||||
secondaryTypographyProps={{
|
||||
sx: { display: "flex", alignItems: "center" },
|
||||
|
@ -142,7 +142,11 @@ const StyledSubtitle = styled("span")`
|
|||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledTypeBox = styled(Box)(({ theme }) => ({
|
||||
const ListItemTextChild = styled("span")`
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
|
||||
display: "inline-block",
|
||||
border: "1px solid #ccc",
|
||||
borderColor: alpha(theme.palette.primary.main, 0.5),
|
||||
|
|
132
src/components/setting/mods/clash-core-viewer.tsx
Normal file
132
src/components/setting/mods/clash-core-viewer.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { mutate } from "swr";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Lock } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
IconButton,
|
||||
List,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { changeClashCore, restartSidecar } from "@/services/cmds";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { grantPermission } from "@/services/cmds";
|
||||
import getSystem from "@/utils/get-system";
|
||||
|
||||
const VALID_CORE = [
|
||||
{ name: "Clash", core: "clash" },
|
||||
{ name: "Clash Meta", core: "clash-meta" },
|
||||
];
|
||||
|
||||
const OS = getSystem();
|
||||
|
||||
export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { verge, mutateVerge } = useVerge();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const { clash_core = "clash" } = verge ?? {};
|
||||
|
||||
const onCoreChange = useLockFn(async (core: string) => {
|
||||
if (core === clash_core) return;
|
||||
|
||||
try {
|
||||
closeAllConnections();
|
||||
await changeClashCore(core);
|
||||
mutateVerge();
|
||||
setTimeout(() => {
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion");
|
||||
}, 100);
|
||||
Notice.success(`Successfully switch to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const onGrant = useLockFn(async (core: string) => {
|
||||
try {
|
||||
await grantPermission(core);
|
||||
// 自动重启
|
||||
if (core === clash_core) await restartSidecar();
|
||||
Notice.success(`Successfully grant permission to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
const onRestart = useLockFn(async () => {
|
||||
try {
|
||||
await restartSidecar();
|
||||
Notice.success(`Successfully restart core`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
{t("Clash Core")}
|
||||
|
||||
<Button variant="contained" size="small" onClick={onRestart}>
|
||||
{t("Restart")}
|
||||
</Button>
|
||||
</Box>
|
||||
}
|
||||
contentSx={{
|
||||
pb: 0,
|
||||
width: 320,
|
||||
height: 200,
|
||||
overflowY: "auto",
|
||||
userSelect: "text",
|
||||
marginTop: "-8px",
|
||||
}}
|
||||
disableOk
|
||||
cancelBtn={t("Back")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List component="nav">
|
||||
{VALID_CORE.map((each) => (
|
||||
<ListItemButton
|
||||
key={each.core}
|
||||
selected={each.core === clash_core}
|
||||
onClick={() => onCoreChange(each.core)}
|
||||
>
|
||||
<ListItemText primary={each.name} secondary={`/${each.core}`} />
|
||||
|
||||
{(OS === "macos" || OS === "linux") && (
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
edge="end"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onGrant(each.core);
|
||||
}}
|
||||
>
|
||||
<Lock fontSize="inherit" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
|
@ -64,7 +64,7 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
|
||||
</>
|
||||
}
|
||||
contentSx={{ width: 520, pb: 1 }}
|
||||
contentSx={{ width: 520, pb: 1, userSelect: "text" }}
|
||||
cancelBtn={t("Back")}
|
||||
disableOk
|
||||
onClose={() => setOpen(false)}
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
import { mutate } from "swr";
|
||||
import { useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { IconButton, Menu, MenuItem } from "@mui/material";
|
||||
import { Settings } from "@mui/icons-material";
|
||||
import { changeClashCore } from "@/services/cmds";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
const VALID_CORE = [
|
||||
{ name: "Clash", core: "clash" },
|
||||
{ name: "Clash Meta", core: "clash-meta" },
|
||||
];
|
||||
|
||||
export const CoreSwitch = () => {
|
||||
const { verge, mutateVerge } = useVerge();
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||
const [position, setPosition] = useState({ left: 0, top: 0 });
|
||||
|
||||
const { clash_core = "clash" } = verge ?? {};
|
||||
|
||||
const onCoreChange = useLockFn(async (core: string) => {
|
||||
if (core === clash_core) return;
|
||||
|
||||
try {
|
||||
closeAllConnections();
|
||||
await changeClashCore(core);
|
||||
mutateVerge();
|
||||
setTimeout(() => {
|
||||
mutate("getClashConfig");
|
||||
mutate("getVersion");
|
||||
}, 100);
|
||||
setAnchorEl(null);
|
||||
Notice.success(`Successfully switch to ${core}`, 1000);
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={(event) => {
|
||||
const { clientX, clientY } = event;
|
||||
setPosition({ top: clientY, left: clientX });
|
||||
setAnchorEl(event.currentTarget);
|
||||
}}
|
||||
>
|
||||
<Settings
|
||||
fontSize="inherit"
|
||||
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||
/>
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
open={!!anchorEl}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => setAnchorEl(null)}
|
||||
anchorPosition={position}
|
||||
anchorReference="anchorPosition"
|
||||
transitionDuration={225}
|
||||
onContextMenu={(e) => {
|
||||
setAnchorEl(null);
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
{VALID_CORE.map((each) => (
|
||||
<MenuItem
|
||||
key={each.core}
|
||||
sx={{ minWidth: 125 }}
|
||||
selected={each.core === clash_core}
|
||||
onClick={() => onCoreChange(each.core)}
|
||||
>
|
||||
{each.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -14,6 +14,7 @@ const ItemWrapper = styled("div")`
|
|||
`;
|
||||
|
||||
const HOTKEY_FUNC = [
|
||||
"open_dashboard",
|
||||
"clash_mode_rule",
|
||||
"clash_mode_global",
|
||||
"clash_mode_direct",
|
||||
|
|
80
src/components/setting/mods/layout-viewer.tsx
Normal file
80
src/components/setting/mods/layout-viewer.tsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { List, Switch } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { SettingItem } from "./setting-comp";
|
||||
import { GuardState } from "./guard-state";
|
||||
|
||||
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { verge, patchVerge, mutateVerge } = useVerge();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onError = (err: any) => {
|
||||
Notice.error(err.message || err.toString());
|
||||
};
|
||||
const onChangeData = (patch: Partial<IVergeConfig>) => {
|
||||
mutateVerge({ ...verge, ...patch }, false);
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("Layout Setting")}
|
||||
contentSx={{ width: 450 }}
|
||||
disableOk
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
<SettingItem label={t("Theme Blur")}>
|
||||
<GuardState
|
||||
value={verge?.theme_blur ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ theme_blur: e })}
|
||||
onGuard={(e) => patchVerge({ theme_blur: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Traffic Graph")}>
|
||||
<GuardState
|
||||
value={verge?.traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ traffic_graph: e })}
|
||||
onGuard={(e) => patchVerge({ traffic_graph: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Memory Usage")}>
|
||||
<GuardState
|
||||
value={verge?.enable_memory_usage ?? true}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_memory_usage: e })}
|
||||
onGuard={(e) => patchVerge({ enable_memory_usage: e })}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
});
|
|
@ -19,20 +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),
|
||||
|
@ -41,10 +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) {
|
||||
|
@ -65,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}
|
||||
|
@ -76,7 +106,18 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary="Enable Builtin Enhanced" />
|
||||
<ListItemText primary={t("Enable Clash Fields Filter")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableClashFields}
|
||||
onChange={(_, c) =>
|
||||
setValues((v) => ({ ...v, enableClashFields: c }))
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{ padding: "5px 2px" }}>
|
||||
<ListItemText primary={t("Enable Builtin Enhanced")} />
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={values.enableBuiltinEnhanced}
|
||||
|
@ -87,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) => ({
|
||||
|
@ -111,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"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { ReactNode } from "react";
|
||||
import {
|
||||
Box,
|
||||
List,
|
||||
|
@ -8,12 +8,14 @@ import {
|
|||
} from "@mui/material";
|
||||
|
||||
interface ItemProps {
|
||||
label: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
label: ReactNode;
|
||||
extra?: ReactNode;
|
||||
children?: ReactNode;
|
||||
secondary?: ReactNode;
|
||||
}
|
||||
|
||||
export const SettingItem: React.FC<ItemProps> = (props) => {
|
||||
const { label, extra, children } = props;
|
||||
const { label, extra, children, secondary } = props;
|
||||
|
||||
const primary = !extra ? (
|
||||
label
|
||||
|
@ -26,13 +28,16 @@ export const SettingItem: React.FC<ItemProps> = (props) => {
|
|||
|
||||
return (
|
||||
<ListItem sx={{ pt: "5px", pb: "5px" }}>
|
||||
<ListItemText primary={primary} />
|
||||
<ListItemText primary={primary} secondary={secondary} />
|
||||
{children}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingList: React.FC<{ title: string }> = (props) => (
|
||||
export const SettingList: React.FC<{
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}> = (props) => (
|
||||
<List>
|
||||
<ListSubheader sx={{ background: "transparent" }} disableSticky>
|
||||
{props.title}
|
||||
|
|
|
@ -1,43 +1,45 @@
|
|||
import useSWR from "swr";
|
||||
import snarkdown from "snarkdown";
|
||||
import { useMemo } from "react";
|
||||
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, styled } from "@mui/material";
|
||||
import { useRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
styled,
|
||||
} from "@mui/material";
|
||||
import { relaunch } from "@tauri-apps/api/process";
|
||||
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
|
||||
import { BaseDialog, DialogRef, Notice } from "@/components/base";
|
||||
import { atomUpdateState } from "@/services/states";
|
||||
import { Notice } from "@/components/base";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UpdateLog = styled(Box)(() => ({
|
||||
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
|
||||
}));
|
||||
|
||||
const UpdateDialog = (props: Props) => {
|
||||
const { open, onClose } = props;
|
||||
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||
|
||||
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
|
||||
errorRetryCount: 2,
|
||||
revalidateIfStale: false,
|
||||
focusThrottleInterval: 36e5, // 1 hour
|
||||
});
|
||||
|
||||
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => setOpen(true),
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const onUpdate = async () => {
|
||||
// markdown parser
|
||||
const parseContent = useMemo(() => {
|
||||
if (!updateInfo?.manifest?.body) {
|
||||
return "New Version is available";
|
||||
}
|
||||
return snarkdown(updateInfo?.manifest?.body);
|
||||
}, [updateInfo]);
|
||||
|
||||
const onUpdate = useLockFn(async () => {
|
||||
if (updateState) return;
|
||||
setUpdateState(true);
|
||||
|
||||
|
@ -49,39 +51,20 @@ const UpdateDialog = (props: Props) => {
|
|||
} finally {
|
||||
setUpdateState(false);
|
||||
}
|
||||
};
|
||||
|
||||
// markdown parser
|
||||
const parseContent = useMemo(() => {
|
||||
if (!updateInfo?.manifest?.body) {
|
||||
return "New Version is available";
|
||||
}
|
||||
return snarkdown(updateInfo?.manifest?.body);
|
||||
}, [updateInfo]);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>New Version v{updateInfo?.manifest?.version}</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}>
|
||||
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button variant="outlined" onClick={onClose}>
|
||||
{t("Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
autoFocus
|
||||
variant="contained"
|
||||
disabled={updateState}
|
||||
onClick={onUpdate}
|
||||
>
|
||||
{t("Update")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={`New Version v${updateInfo?.manifest?.version}`}
|
||||
contentSx={{ minWidth: 360, maxWidth: 400, maxHeight: "50vh" }}
|
||||
okBtn={t("Update")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={onUpdate}
|
||||
>
|
||||
<UpdateLog dangerouslySetInnerHTML={{ __html: parseContent }} />
|
||||
</BaseDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateDialog;
|
||||
});
|
|
@ -60,7 +60,10 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
|
|||
.trim();
|
||||
|
||||
url = url.replaceAll("%port", port || "9090");
|
||||
url = url.replaceAll("%secret", clashInfo.secret || "");
|
||||
url = url.replaceAll(
|
||||
"%secret",
|
||||
encodeURIComponent(clashInfo.secret || "")
|
||||
);
|
||||
}
|
||||
|
||||
await openWebUrl(url);
|
||||
|
|
|
@ -8,16 +8,16 @@ import {
|
|||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import { ArrowForward } from "@mui/icons-material";
|
||||
import { ArrowForward, Settings } from "@mui/icons-material";
|
||||
import { DialogRef } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { GuardState } from "./mods/guard-state";
|
||||
import { CoreSwitch } from "./mods/core-switch";
|
||||
import { WebUIViewer } from "./mods/web-ui-viewer";
|
||||
import { ClashFieldViewer } from "./mods/clash-field-viewer";
|
||||
import { ClashPortViewer } from "./mods/clash-port-viewer";
|
||||
import { ControllerViewer } from "./mods/controller-viewer";
|
||||
import { SettingList, SettingItem } from "./mods/setting-comp";
|
||||
import { ClashCoreViewer } from "./mods/clash-core-viewer";
|
||||
|
||||
interface Props {
|
||||
onError: (err: Error) => void;
|
||||
|
@ -39,6 +39,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||
const fieldRef = useRef<DialogRef>(null);
|
||||
const portRef = useRef<DialogRef>(null);
|
||||
const ctrlRef = useRef<DialogRef>(null);
|
||||
const coreRef = useRef<DialogRef>(null);
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<IConfigData>) => {
|
||||
|
@ -51,6 +52,7 @@ const SettingClash = ({ onError }: Props) => {
|
|||
<ClashFieldViewer ref={fieldRef} />
|
||||
<ClashPortViewer ref={portRef} />
|
||||
<ControllerViewer ref={ctrlRef} />
|
||||
<ClashCoreViewer ref={coreRef} />
|
||||
|
||||
<SettingItem label={t("Allow Lan")}>
|
||||
<GuardState
|
||||
|
@ -143,7 +145,21 @@ const SettingClash = ({ onError }: Props) => {
|
|||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t("Clash Core")} extra={<CoreSwitch />}>
|
||||
<SettingItem
|
||||
label={t("Clash Core")}
|
||||
extra={
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
onClick={() => coreRef.current?.open()}
|
||||
>
|
||||
<Settings
|
||||
fontSize="inherit"
|
||||
style={{ cursor: "pointer", opacity: 0.75 }}
|
||||
/>
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography>
|
||||
</SettingItem>
|
||||
</SettingList>
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import { useRef } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
IconButton,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { 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;
|
||||
};
|
53
src/hooks/use-websocket.ts
Normal file
53
src/hooks/use-websocket.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { useRef } from "react";
|
||||
|
||||
export type WsMsgFn = (event: MessageEvent<any>) => void;
|
||||
|
||||
export interface WsOptions {
|
||||
errorCount?: number; // default is 5
|
||||
retryInterval?: number; // default is 2500
|
||||
onError?: () => void;
|
||||
}
|
||||
|
||||
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const timerRef = useRef<any>(null);
|
||||
|
||||
const disconnect = () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
const connect = (url: string) => {
|
||||
let errorCount = options?.errorCount ?? 5;
|
||||
|
||||
if (!url) return;
|
||||
|
||||
const connectHelper = () => {
|
||||
disconnect();
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.addEventListener("message", onMessage);
|
||||
ws.addEventListener("error", () => {
|
||||
errorCount -= 1;
|
||||
|
||||
if (errorCount >= 0) {
|
||||
timerRef.current = setTimeout(connectHelper, 2500);
|
||||
} else {
|
||||
disconnect();
|
||||
options?.onError?.();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
connectHelper();
|
||||
};
|
||||
|
||||
return { connect, disconnect };
|
||||
};
|
|
@ -15,6 +15,7 @@
|
|||
"global": "global",
|
||||
"direct": "direct",
|
||||
"script": "script",
|
||||
|
||||
"Profiles": "Profiles",
|
||||
"Profile URL": "Profile URL",
|
||||
"Import": "Import",
|
||||
|
@ -34,6 +35,9 @@
|
|||
"Refresh": "Refresh",
|
||||
"To Top": "To Top",
|
||||
"To End": "To End",
|
||||
"Update All Profiles": "Update All Profiles",
|
||||
"View Runtime Config": "View Runtime Config",
|
||||
"Reactivate Profiles": "Reactivate Profiles",
|
||||
|
||||
"Location": "Location",
|
||||
"Delay check": "Delay check",
|
||||
|
@ -51,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",
|
||||
|
@ -60,6 +66,7 @@
|
|||
"IPv6": "IPv6",
|
||||
"Log Level": "Log Level",
|
||||
"Mixed Port": "Mixed Port",
|
||||
"External": "External",
|
||||
"Clash Core": "Clash Core",
|
||||
"Tun Mode": "Tun Mode",
|
||||
"Service Mode": "Service Mode",
|
||||
|
@ -74,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",
|
||||
|
@ -87,11 +98,17 @@
|
|||
"Clash Field": "Clash Field",
|
||||
"Runtime Config": "Runtime Config",
|
||||
"ReadOnly": "ReadOnly",
|
||||
"Restart": "Restart",
|
||||
|
||||
"Back": "Back",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
|
||||
"Default": "Default",
|
||||
"Download Speed": "Download Speed",
|
||||
"Upload Speed": "Upload Speed",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Rule Mode",
|
||||
"clash_mode_global": "Global Mode",
|
||||
"clash_mode_direct": "Direct Mode",
|
||||
|
@ -101,5 +118,18 @@
|
|||
"disable_system_proxy": "Disable System Proxy",
|
||||
"toggle_tun_mode": "Toggle Tun Mode",
|
||||
"enable_tun_mode": "Enable Tun Mode",
|
||||
"disable_tun_mode": "Disable Tun Mode"
|
||||
"disable_tun_mode": "Disable Tun Mode",
|
||||
|
||||
"App Log Level": "App Log Level",
|
||||
"Auto Close Connections": "Auto Close Connections",
|
||||
"Enable Clash Fields Filter": "Enable Clash Fields Filter",
|
||||
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
|
||||
"Proxy Layout Column": "Proxy Layout Column",
|
||||
"Default Latency Test": "Default Latency Test",
|
||||
|
||||
"Auto Log Clean": "Auto Log Clean",
|
||||
"Never Clean": "Never Clean",
|
||||
"Retain 7 Days": "Retain 7 Days",
|
||||
"Retain 30 Days": "Retain 30 Days",
|
||||
"Retain 90 Days": "Retain 90 Days"
|
||||
}
|
||||
|
|
111
src/locales/ru.json
Normal file
111
src/locales/ru.json
Normal file
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"Label-Proxies": "Прокси",
|
||||
"Label-Profiles": "Профили",
|
||||
"Label-Connections": "Соединения",
|
||||
"Label-Logs": "Логи",
|
||||
"Label-Rules": "Правила",
|
||||
"Label-Settings": "Настройки",
|
||||
|
||||
"Connections": "Соединения",
|
||||
"Logs": "Логи",
|
||||
"Clear": "Очистить",
|
||||
"Proxies": "Прокси",
|
||||
"Proxy Groups": "Группы прокси",
|
||||
"rule": "правила",
|
||||
"global": "глобальный",
|
||||
"direct": "прямой",
|
||||
"script": "скриптовый",
|
||||
|
||||
"Profiles": "Профили",
|
||||
"Profile URL": "URL профиля",
|
||||
"Import": "Импорт",
|
||||
"New": "Новый",
|
||||
"Create Profile": "Создать профиль",
|
||||
"Choose File": "Выбрать файл",
|
||||
"Close All": "Закрыть всё",
|
||||
"Select": "Выбрать",
|
||||
"Edit Info": "Изменить информацию",
|
||||
"Edit File": "Изменить файл",
|
||||
"Open File": "Открыть файл",
|
||||
"Update": "Обновить",
|
||||
"Update(Proxy)": "Обновить (прокси)",
|
||||
"Delete": "Удалить",
|
||||
"Enable": "Включить",
|
||||
"Disable": "Отключить",
|
||||
"Refresh": "Обновить",
|
||||
"To Top": "Наверх",
|
||||
"To End": "Вниз",
|
||||
"Update All Profiles": "Обновить все профили",
|
||||
"View Runtime Config": "Просмотреть используемый конфиг",
|
||||
"Reactivate Profiles": "Реактивировать профили",
|
||||
|
||||
"Location": "Местоположение",
|
||||
"Delay check": "Проверка задержки",
|
||||
"Sort by default": "Сортировать по умолчанию",
|
||||
"Sort by delay": "Сортировать по задержке",
|
||||
"Sort by name": "Сортировать по названию",
|
||||
"Delay check URL": "URL проверки задержки",
|
||||
"Proxy detail": "Подробности о прокси",
|
||||
"Filter": "Фильтр",
|
||||
"Filter conditions": "Условия фильтрации",
|
||||
"Refresh profiles": "Обновить профили",
|
||||
|
||||
"Type": "Тип",
|
||||
"Name": "Название",
|
||||
"Descriptions": "Описания",
|
||||
"Subscription URL": "URL подписки",
|
||||
"Update Interval": "Интервал обновления",
|
||||
|
||||
"Settings": "Настройки",
|
||||
"Clash Setting": "Настройки Clash",
|
||||
"System Setting": "Настройки системы",
|
||||
"Verge Setting": "Настройки Verge",
|
||||
"Allow Lan": "Разрешить локальную сеть",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "Уровень логов",
|
||||
"Mixed Port": "Смешанный порт",
|
||||
"Clash Core": "Ядро Clash",
|
||||
"Tun Mode": "Режим туннеля",
|
||||
"Service Mode": "Режим сервиса",
|
||||
"Auto Launch": "Автозапуск",
|
||||
"Silent Start": "Тихий запуск",
|
||||
"System Proxy": "Системный прокси",
|
||||
"System Proxy Setting": "Настройка системного прокси",
|
||||
"Proxy Guard": "Защита прокси",
|
||||
"Guard Duration": "Период защиты",
|
||||
"Proxy Bypass": "Игнорирование прокси",
|
||||
"Current System Proxy": "Текущий системный прокси",
|
||||
"Theme Mode": "Режим темы",
|
||||
"Theme Blur": "Размытие темы",
|
||||
"Theme Setting": "Настройка темы",
|
||||
"Hotkey Setting": "Настройка клавиатурных сокращений",
|
||||
"Traffic Graph": "График трафика",
|
||||
"Language": "Язык",
|
||||
"Open App Dir": "Открыть папку приложения",
|
||||
"Open Core Dir": "Открыть папку ядра",
|
||||
"Open Logs Dir": "Открыть папку логов",
|
||||
"Verge Version": "Версия Verge",
|
||||
"theme.light": "Светлая",
|
||||
"theme.dark": "Тёмная",
|
||||
"theme.system": "Системная",
|
||||
"Clash Field": "Используемые настройки Clash",
|
||||
"Runtime Config": "Используемый конфиг",
|
||||
"ReadOnly": "Только для чтения",
|
||||
"Restart": "Перезапуск",
|
||||
|
||||
"Back": "Назад",
|
||||
"Save": "Сохранить",
|
||||
"Cancel": "Отмена",
|
||||
|
||||
"open_dashboard": "Open Dashboard",
|
||||
"clash_mode_rule": "Режим правил",
|
||||
"clash_mode_global": "Глобальный режим",
|
||||
"clash_mode_direct": "Прямой режим",
|
||||
"clash_mode_script": "Скриптовый режим",
|
||||
"toggle_system_proxy": "Переключить режим системного прокси",
|
||||
"enable_system_proxy": "Включить системный прокси",
|
||||
"disable_system_proxy": "Отключить системный прокси",
|
||||
"toggle_tun_mode": "Переключить режим туннеля",
|
||||
"enable_tun_mode": "Включить режим туннеля",
|
||||
"disable_tun_mode": "Отключить режим туннеля"
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
"global": "全局",
|
||||
"direct": "直连",
|
||||
"script": "脚本",
|
||||
|
||||
"Profiles": "配置",
|
||||
"Profile URL": "配置文件链接",
|
||||
"Import": "导入",
|
||||
|
@ -34,6 +35,9 @@
|
|||
"Refresh": "刷新",
|
||||
"To Top": "移到最前",
|
||||
"To End": "移到末尾",
|
||||
"Update All Profiles": "更新所有配置",
|
||||
"View Runtime Config": "查看运行时配置",
|
||||
"Reactivate Profiles": "重新激活配置",
|
||||
|
||||
"Location": "当前节点",
|
||||
"Delay check": "延迟测试",
|
||||
|
@ -76,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": "深色",
|
||||
|
@ -91,6 +98,7 @@
|
|||
"Clash Field": "Clash 字段",
|
||||
"Runtime Config": "运行配置",
|
||||
"ReadOnly": "只读",
|
||||
"Restart": "重启内核",
|
||||
|
||||
"Back": "返回",
|
||||
"Save": "保存",
|
||||
|
@ -100,6 +108,7 @@
|
|||
"Download Speed": "下载速度",
|
||||
"Upload Speed": "上传速度",
|
||||
|
||||
"open_dashboard": "打开面板",
|
||||
"clash_mode_rule": "规则模式",
|
||||
"clash_mode_global": "全局模式",
|
||||
"clash_mode_direct": "直连模式",
|
||||
|
@ -109,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天"
|
||||
}
|
||||
|
|
25
src/main.tsx
25
src/main.tsx
|
@ -8,19 +8,30 @@ if (!window.ResizeObserver) {
|
|||
}
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { BaseErrorBoundary } from "./components/base";
|
||||
import Layout from "./pages/_layout";
|
||||
import "./services/i18n";
|
||||
|
||||
ReactDOM.render(
|
||||
const mainElementId = "root";
|
||||
const container = document.getElementById(mainElementId);
|
||||
|
||||
if (!container) {
|
||||
throw new Error(
|
||||
`No container '${mainElementId}' found to render application`
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(container).render(
|
||||
<React.StrictMode>
|
||||
<RecoilRoot>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
<BaseErrorBoundary>
|
||||
<BrowserRouter>
|
||||
<Layout />
|
||||
</BrowserRouter>
|
||||
</BaseErrorBoundary>
|
||||
</RecoilRoot>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
@ -35,9 +36,9 @@ const Layout = () => {
|
|||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (OS === "macos") appWindow.hide();
|
||||
else appWindow.close();
|
||||
// macOS有cmd+w
|
||||
if (e.key === "Escape" && OS !== "macos") {
|
||||
appWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -47,6 +48,7 @@ const Layout = () => {
|
|||
mutate("getProxies");
|
||||
mutate("getVersion");
|
||||
mutate("getClashConfig");
|
||||
mutate("getProviders");
|
||||
});
|
||||
|
||||
// update the verge config
|
||||
|
@ -76,7 +78,7 @@ const Layout = () => {
|
|||
}, [language]);
|
||||
|
||||
return (
|
||||
<SWRConfig value={{}}>
|
||||
<SWRConfig value={{ errorRetryCount: 3 }}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<Paper
|
||||
square
|
||||
|
@ -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,
|
||||
|
@ -17,8 +17,13 @@ import { closeAllConnections } from "@/services/api";
|
|||
import { atomConnectionSetting } from "@/services/states";
|
||||
import { useClashInfo } from "@/hooks/use-clash";
|
||||
import { BaseEmpty, BasePage } from "@/components/base";
|
||||
import ConnectionItem from "@/components/connection/connection-item";
|
||||
import ConnectionTable from "@/components/connection/connection-table";
|
||||
import { useWebsocket } from "@/hooks/use-websocket";
|
||||
import { ConnectionItem } from "@/components/connection/connection-item";
|
||||
import { ConnectionTable } from "@/components/connection/connection-table";
|
||||
import {
|
||||
ConnectionDetail,
|
||||
ConnectionDetailRef,
|
||||
} from "@/components/connection/connection-detail";
|
||||
|
||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||
|
||||
|
@ -53,23 +58,18 @@ const ConnectionsPage = () => {
|
|||
return connections;
|
||||
}, [connData, filterText, curOrderOpt]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
const ws = new WebSocket(`ws://${server}/connections?token=${secret}`);
|
||||
|
||||
ws.addEventListener("message", (event) => {
|
||||
const { connect, disconnect } = useWebsocket(
|
||||
(event) => {
|
||||
// meta v1.15.0 出现data.connections为null的情况
|
||||
const data = JSON.parse(event.data) as IConnections;
|
||||
|
||||
// 尽量与前一次connections的展示顺序保持一致
|
||||
setConnData((old) => {
|
||||
const oldConn = old.connections;
|
||||
const maxLen = data.connections.length;
|
||||
const maxLen = data.connections?.length;
|
||||
|
||||
const connections: typeof oldConn = [];
|
||||
|
||||
const rest = data.connections.filter((each) => {
|
||||
const rest = (data.connections || []).filter((each) => {
|
||||
const index = oldConn.findIndex((o) => o.id === each.id);
|
||||
|
||||
if (index >= 0 && index < maxLen) {
|
||||
|
@ -93,23 +93,34 @@ const ConnectionsPage = () => {
|
|||
|
||||
return { ...data, connections };
|
||||
});
|
||||
});
|
||||
},
|
||||
{ errorCount: 3, retryInterval: 1000 }
|
||||
);
|
||||
|
||||
return () => ws?.close();
|
||||
useEffect(() => {
|
||||
if (!clashInfo) return;
|
||||
|
||||
const { server = "", secret = "" } = clashInfo;
|
||||
connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`);
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [clashInfo]);
|
||||
|
||||
const onCloseAll = useLockFn(closeAllConnections);
|
||||
|
||||
const detailRef = useRef<ConnectionDetailRef>(null!);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Connections")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() =>
|
||||
setSetting((o) =>
|
||||
o.layout === "list"
|
||||
|
@ -181,14 +192,24 @@ const ConnectionsPage = () => {
|
|||
{filterConn.length === 0 ? (
|
||||
<BaseEmpty text="No Connections" />
|
||||
) : isTableLayout ? (
|
||||
<ConnectionTable connections={filterConn} />
|
||||
<ConnectionTable
|
||||
connections={filterConn}
|
||||
onShowDetail={(detail) => detailRef.current?.open(detail)}
|
||||
/>
|
||||
) : (
|
||||
<Virtuoso
|
||||
data={filterConn}
|
||||
itemContent={(index, item) => <ConnectionItem value={item} />}
|
||||
itemContent={(index, item) => (
|
||||
<ConnectionItem
|
||||
value={item}
|
||||
onShowDetail={() => detailRef.current?.open(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<ConnectionDetail ref={detailRef} />
|
||||
</Paper>
|
||||
</BasePage>
|
||||
);
|
||||
|
|
|
@ -41,11 +41,10 @@ const LogPage = () => {
|
|||
title={t("Logs")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 2 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
sx={{ mr: 2 }}
|
||||
onClick={() => setEnableLog((e) => !e)}
|
||||
>
|
||||
{enableLog ? (
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import useSWR, { mutate } from "swr";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material";
|
||||
import { CachedRounded } from "@mui/icons-material";
|
||||
import {
|
||||
ClearRounded,
|
||||
ContentCopyRounded,
|
||||
LocalFireDepartmentRounded,
|
||||
RefreshRounded,
|
||||
TextSnippetOutlined,
|
||||
} from "@mui/icons-material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getProfiles,
|
||||
|
@ -10,9 +17,11 @@ import {
|
|||
enhanceProfiles,
|
||||
getRuntimeLogs,
|
||||
deleteProfile,
|
||||
updateProfile,
|
||||
} from "@/services/cmds";
|
||||
import { atomLoadingCache } from "@/services/states";
|
||||
import { closeAllConnections } from "@/services/api";
|
||||
import { BasePage, Notice } from "@/components/base";
|
||||
import { BasePage, DialogRef, Notice } from "@/components/base";
|
||||
import {
|
||||
ProfileViewer,
|
||||
ProfileViewerRef,
|
||||
|
@ -20,12 +29,15 @@ import {
|
|||
import { ProfileItem } from "@/components/profile/profile-item";
|
||||
import { ProfileMore } from "@/components/profile/profile-more";
|
||||
import { useProfiles } from "@/hooks/use-profiles";
|
||||
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
|
||||
import { throttle } from "lodash-es";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [url, setUrl] = useState("");
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [activating, setActivating] = useState("");
|
||||
|
||||
const {
|
||||
profiles = {},
|
||||
|
@ -41,6 +53,7 @@ const ProfilePage = () => {
|
|||
|
||||
const chain = profiles.chain || [];
|
||||
const viewerRef = useRef<ProfileViewerRef>(null);
|
||||
const configRef = useRef<DialogRef>(null);
|
||||
|
||||
// distinguish type
|
||||
const { regularItems, enhanceItems } = useMemo(() => {
|
||||
|
@ -50,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 };
|
||||
|
@ -89,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();
|
||||
|
@ -97,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("");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -149,18 +168,70 @@ const ProfilePage = () => {
|
|||
mutateLogs();
|
||||
});
|
||||
|
||||
// 更新所有配置
|
||||
const setLoadingCache = useSetRecoilState(atomLoadingCache);
|
||||
const onUpdateAll = useLockFn(async () => {
|
||||
const throttleMutate = throttle(mutateProfiles, 2000, {
|
||||
trailing: true,
|
||||
});
|
||||
const updateOne = async (uid: string) => {
|
||||
try {
|
||||
await updateProfile(uid);
|
||||
throttleMutate();
|
||||
} finally {
|
||||
setLoadingCache((cache) => ({ ...cache, [uid]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setLoadingCache((cache) => {
|
||||
// 获取没有正在更新的配置
|
||||
const items = regularItems.filter(
|
||||
(e) => e.type === "remote" && !cache[e.uid]
|
||||
);
|
||||
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
|
||||
|
||||
Promise.allSettled(items.map((e) => updateOne(e.uid))).then(resolve);
|
||||
return { ...cache, ...change };
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const onCopyLink = async () => {
|
||||
const text = await navigator.clipboard.readText();
|
||||
if (text) setUrl(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title={t("Profiles")}
|
||||
header={
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center" }}>
|
||||
<Box sx={{ mt: 1, display: "flex", alignItems: "center", gap: 1 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("Refresh profiles")}
|
||||
title={t("Update All Profiles")}
|
||||
onClick={onUpdateAll}
|
||||
>
|
||||
<RefreshRounded />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title={t("View Runtime Config")}
|
||||
onClick={() => configRef.current?.open()}
|
||||
>
|
||||
<TextSnippetOutlined />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
title={t("Reactivate Profiles")}
|
||||
onClick={onEnhance}
|
||||
>
|
||||
<CachedRounded />
|
||||
<LocalFireDepartmentRounded />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
|
@ -177,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}
|
||||
|
@ -201,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)}
|
||||
|
@ -232,6 +326,7 @@ const ProfilePage = () => {
|
|||
)}
|
||||
|
||||
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
|
||||
<ConfigViewer ref={configRef} />
|
||||
</BasePage>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,12 +2,17 @@ import useSWR from "swr";
|
|||
import { useEffect, useMemo } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonGroup, Paper } from "@mui/material";
|
||||
import { getClashConfig, updateConfigs } from "@/services/api";
|
||||
import { Box, Button, ButtonGroup, Paper } from "@mui/material";
|
||||
import {
|
||||
closeAllConnections,
|
||||
getClashConfig,
|
||||
updateConfigs,
|
||||
} from "@/services/api";
|
||||
import { patchClashConfig } from "@/services/cmds";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BasePage } from "@/components/base";
|
||||
import { ProxyGroups } from "@/components/proxy/proxy-groups";
|
||||
import { ProviderButton } from "@/components/proxy/provider-button";
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
|
@ -26,9 +31,13 @@ const ProxyPage = () => {
|
|||
return ["rule", "global", "direct", "script"];
|
||||
}, [verge?.clash_core]);
|
||||
|
||||
const curMode = clashConfig?.mode.toLowerCase();
|
||||
const curMode = clashConfig?.mode?.toLowerCase();
|
||||
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
// 断开连接
|
||||
if (mode !== curMode && verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
await updateConfigs({ mode });
|
||||
await patchClashConfig({ mode });
|
||||
mutateClash();
|
||||
|
@ -45,18 +54,22 @@ const ProxyPage = () => {
|
|||
contentStyle={{ height: "100%" }}
|
||||
title={t("Proxy Groups")}
|
||||
header={
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={mode === curMode ? "contained" : "outlined"}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ProviderButton />
|
||||
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
<Button
|
||||
key={mode}
|
||||
variant={mode === curMode ? "contained" : "outlined"}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Paper
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Paper } from "@mui/material";
|
||||
import { IconButton, Paper } from "@mui/material";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BasePage, Notice } from "@/components/base";
|
||||
import { GitHub } from "@mui/icons-material";
|
||||
import { openWebUrl } from "@/services/cmds";
|
||||
import SettingVerge from "@/components/setting/setting-verge";
|
||||
import SettingClash from "@/components/setting/setting-clash";
|
||||
import SettingSystem from "@/components/setting/setting-system";
|
||||
|
@ -12,8 +15,24 @@ const SettingPage = () => {
|
|||
Notice.error(err?.message || err.toString());
|
||||
};
|
||||
|
||||
const toGithubRepo = useLockFn(() => {
|
||||
return openWebUrl("https://github.com/zzzgydi/clash-verge");
|
||||
});
|
||||
|
||||
return (
|
||||
<BasePage title={t("Settings")}>
|
||||
<BasePage
|
||||
title={t("Settings")}
|
||||
header={
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
title="@zzzgydi/clash-verge"
|
||||
onClick={toGithubRepo}
|
||||
>
|
||||
<GitHub fontSize="inherit" />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 3 }}>
|
||||
<SettingClash onError={onError} />
|
||||
</Paper>
|
||||
|
|
|
@ -27,6 +27,7 @@ export const getAxios = async (force: boolean = false) => {
|
|||
axiosIns = axios.create({
|
||||
baseURL: `http://${server}`,
|
||||
headers: secret ? { Authorization: `Bearer ${secret}` } : {},
|
||||
timeout: 15000,
|
||||
});
|
||||
axiosIns.interceptors.response.use((r) => r.data);
|
||||
return axiosIns;
|
||||
|
@ -64,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();
|
||||
|
@ -168,6 +169,11 @@ export const providerHealthCheck = async (name: string) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const providerUpdate = async (name: string) => {
|
||||
const instance = await getAxios();
|
||||
return instance.put(`/providers/proxies/${encodeURIComponent(name)}`);
|
||||
};
|
||||
|
||||
export const getConnections = async () => {
|
||||
const instance = await getAxios();
|
||||
const result = await instance.get("/connections");
|
||||
|
|
|
@ -127,6 +127,10 @@ export async function restartSidecar() {
|
|||
return invoke<void>("restart_sidecar");
|
||||
}
|
||||
|
||||
export async function grantPermission(core: string) {
|
||||
return invoke<void>("grant_permission", { core });
|
||||
}
|
||||
|
||||
export async function openAppDir() {
|
||||
return invoke<void>("open_app_dir").catch((err) =>
|
||||
Notice.error(err?.message || err.toString(), 1500)
|
||||
|
@ -149,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 },
|
||||
};
|
||||
|
||||
|
|
4
src/services/types.d.ts
vendored
4
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;
|
||||
|
@ -182,7 +184,9 @@ interface IVergeConfig {
|
|||
};
|
||||
auto_close_connection?: boolean;
|
||||
default_latency_test?: string;
|
||||
enable_clash_fields?: boolean;
|
||||
enable_builtin_enhanced?: boolean;
|
||||
auto_log_clean?: 0 | 1 | 2 | 3;
|
||||
proxy_layout_column?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,9 +39,14 @@ export const OTHERS_FIELDS = [
|
|||
"authentication",
|
||||
"tls", // meta
|
||||
"sniffer", // meta
|
||||
"geox-url", // meta
|
||||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
"external-controller-tls", // meta
|
||||
"global-client-fingerprint", // meta
|
||||
] as const;
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
};
|
416
yarn.lock
416
yarn.lock
|
@ -437,10 +437,15 @@
|
|||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
|
||||
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
|
||||
|
||||
"@esbuild/linux-loong64@0.14.54":
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz#de2a4be678bd4d0d1ffbb86e6de779cde5999028"
|
||||
integrity sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==
|
||||
"@esbuild/android-arm@0.15.18":
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.18.tgz#266d40b8fdcf87962df8af05b76219bc786b4f80"
|
||||
integrity sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==
|
||||
|
||||
"@esbuild/linux-loong64@0.15.18":
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz#128b76ecb9be48b60cf5cfc1c63a4f00691a3239"
|
||||
integrity sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.2":
|
||||
version "0.3.2"
|
||||
|
@ -787,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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.2.2.tgz#f18b0689b399b64dc67da41c0fd4da474be324fe"
|
||||
integrity sha512-W+Cp2weUMlvmGkRJeUjypbz9Lpl6o98xkgKAtobZSum5SNwpsBQfawJTESakNoD+FXyVg/snIk5sRdHge+tAaA==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.2.2.tgz#4450bd244f586da346651f62a5a41686cee36db7"
|
||||
integrity sha512-vmVAqt+ECH2d6cbcGJ7ddcCAZgmKe5xmxlL5r4xoaphu7OqU4gnv4VFURYkVltOfwzIFQVOPVSqwYyIDToCYNQ==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.2.2.tgz#a4d5b95665620605fb0f54cb77f8134239b58370"
|
||||
integrity sha512-yYTdQurgi4QZR8z+fANjl522jdQz/VtesFpw+C/A0+zXg7tiRjicsywBDdPsvNzCqFeGKKkmTR+Lny5qxhGaeQ==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.2.2.tgz#c992907586b42e2085160d45b3a17316c99febea"
|
||||
integrity sha512-ZSOVT6Eq1ay2+27B8KfA0MnpO7KYzONU6TjenH7DNcQki6eWGG5JoNu8QQ9Mdn3dAzY0XBP9i1ZHQOFu4iPtEg==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.2.2.tgz#f8bd7901ce525b8433919e2cfe7ec22aace0f776"
|
||||
integrity sha512-/NHSkqNQ+Pr4PshvyD1CeNFaPCaCpe1OeuAQgVi0rboSecC9fXN96G5dQbSBoxOUcCo6f8aTVE7zkZ4WchFVog==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.2.2.tgz#a558862d7101b0a641c5a994c9ee74edb8300fc5"
|
||||
integrity sha512-4YTmfPuyvlHsvCkATDMwhklfuQm3HKxYXv/IOW9H0ra6pS9efVhrFYIC9Vfv6XaKN85Vnn/FYTEGMJLwCxZw2Q==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.2.2.tgz#573e965a638864a65b606783d3dbd34114939277"
|
||||
integrity sha512-wr46tbscwFuCcA931R+ItOiUTT0djMmgKLd1HFCmFF82V9BKE2reIjr6O9l0NCXCo2WeD4pe3jA/Pt1dxDu+JA==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.2.2.tgz#0fe38283ffeb71acac6a6a317951dc75ec2d3dad"
|
||||
integrity sha512-6VmbVJOWUZJK5/JKhb3mNFKrKGfq0KV7lJGumfN95WJgkHeyL61p8bZit+o6ZgUGUhrOabkAawhDkrRY+ZQhIw==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.2.2.tgz#9b8ea7c63f2883af105e204fc4f1f11a83990a59"
|
||||
integrity sha512-YRPJguJma+zSuRZpFoSZqls6+laggG1vqG0FPQWQTi+ywATgMpai2b2RZnffDlpHKp9mt4V/s2dtqOy6bpGZHg==
|
||||
"@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.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmmirror.com/@tauri-apps/cli/-/cli-1.2.2.tgz#cf7cb90d11c3c3b757efe07b7e2c48abde045ada"
|
||||
integrity sha512-D8zib3A0vWCvPPSyYLxww/OdDlVcY7fpcDVBH6qUvheOjj2aCyU7H9AYMRBwpgCfz8zY5+vomee+laLeB0H13w==
|
||||
"@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.2"
|
||||
"@tauri-apps/cli-darwin-x64" "1.2.2"
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf" "1.2.2"
|
||||
"@tauri-apps/cli-linux-arm64-gnu" "1.2.2"
|
||||
"@tauri-apps/cli-linux-arm64-musl" "1.2.2"
|
||||
"@tauri-apps/cli-linux-x64-gnu" "1.2.2"
|
||||
"@tauri-apps/cli-linux-x64-musl" "1.2.2"
|
||||
"@tauri-apps/cli-win32-ia32-msvc" "1.2.2"
|
||||
"@tauri-apps/cli-win32-x64-msvc" "1.2.2"
|
||||
"@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"
|
||||
|
@ -869,6 +874,18 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d"
|
||||
integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==
|
||||
|
||||
"@types/lodash-es@^4.17.7":
|
||||
version "4.17.7"
|
||||
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.7.tgz#22edcae9f44aff08546e71db8925f05b33c7cc40"
|
||||
integrity sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.191"
|
||||
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
||||
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
|
||||
|
||||
"@types/lodash@^4.14.180":
|
||||
version "4.14.180"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.180.tgz#4ab7c9ddfc92ec4a887886483bc14c79fb380670"
|
||||
|
@ -899,10 +916,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/react-dom@^17.0.0":
|
||||
version "17.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.14.tgz#c8f917156b652ddf807711f5becbd2ab018dea9f"
|
||||
integrity sha512-H03xwEP1oXmSfl3iobtmQ/2dHF5aBHr8aUMwyGZya6OW45G+xtdzmq6HkncefiBt5JU8DVyaWl/nWZbjZCnzAQ==
|
||||
"@types/react-dom@^18.0.11":
|
||||
version "18.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.11.tgz#321351c1459bc9ca3d216aefc8a167beec334e33"
|
||||
integrity sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
|
@ -920,7 +937,7 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^17.0.0":
|
||||
"@types/react@*":
|
||||
version "17.0.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55"
|
||||
integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A==
|
||||
|
@ -1282,132 +1299,133 @@ error-ex@^1.3.1:
|
|||
dependencies:
|
||||
is-arrayish "^0.2.1"
|
||||
|
||||
esbuild-android-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz#505f41832884313bbaffb27704b8bcaa2d8616be"
|
||||
integrity sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==
|
||||
esbuild-android-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz#20a7ae1416c8eaade917fb2453c1259302c637a5"
|
||||
integrity sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==
|
||||
|
||||
esbuild-android-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz#8ce69d7caba49646e009968fe5754a21a9871771"
|
||||
integrity sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==
|
||||
esbuild-android-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz#9cc0ec60581d6ad267568f29cf4895ffdd9f2f04"
|
||||
integrity sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==
|
||||
|
||||
esbuild-darwin-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz#24ba67b9a8cb890a3c08d9018f887cc221cdda25"
|
||||
integrity sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==
|
||||
esbuild-darwin-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz#428e1730ea819d500808f220fbc5207aea6d4410"
|
||||
integrity sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==
|
||||
|
||||
esbuild-darwin-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz#3f7cdb78888ee05e488d250a2bdaab1fa671bf73"
|
||||
integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==
|
||||
esbuild-darwin-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz#b6dfc7799115a2917f35970bfbc93ae50256b337"
|
||||
integrity sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==
|
||||
|
||||
esbuild-freebsd-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz#09250f997a56ed4650f3e1979c905ffc40bbe94d"
|
||||
integrity sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==
|
||||
esbuild-freebsd-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz#4e190d9c2d1e67164619ae30a438be87d5eedaf2"
|
||||
integrity sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==
|
||||
|
||||
esbuild-freebsd-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz#bafb46ed04fc5f97cbdb016d86947a79579f8e48"
|
||||
integrity sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==
|
||||
esbuild-freebsd-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz#18a4c0344ee23bd5a6d06d18c76e2fd6d3f91635"
|
||||
integrity sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==
|
||||
|
||||
esbuild-linux-32@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz#e2a8c4a8efdc355405325033fcebeb941f781fe5"
|
||||
integrity sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==
|
||||
esbuild-linux-32@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz#9a329731ee079b12262b793fb84eea762e82e0ce"
|
||||
integrity sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==
|
||||
|
||||
esbuild-linux-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz#de5fdba1c95666cf72369f52b40b03be71226652"
|
||||
integrity sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==
|
||||
esbuild-linux-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz#532738075397b994467b514e524aeb520c191b6c"
|
||||
integrity sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==
|
||||
|
||||
esbuild-linux-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz#dae4cd42ae9787468b6a5c158da4c84e83b0ce8b"
|
||||
integrity sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==
|
||||
esbuild-linux-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz#5372e7993ac2da8f06b2ba313710d722b7a86e5d"
|
||||
integrity sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==
|
||||
|
||||
esbuild-linux-arm@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz#a2c1dff6d0f21dbe8fc6998a122675533ddfcd59"
|
||||
integrity sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==
|
||||
esbuild-linux-arm@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz#e734aaf259a2e3d109d4886c9e81ec0f2fd9a9cc"
|
||||
integrity sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==
|
||||
|
||||
esbuild-linux-mips64le@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz#d9918e9e4cb972f8d6dae8e8655bf9ee131eda34"
|
||||
integrity sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==
|
||||
esbuild-linux-mips64le@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz#c0487c14a9371a84eb08fab0e1d7b045a77105eb"
|
||||
integrity sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==
|
||||
|
||||
esbuild-linux-ppc64le@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz#3f9a0f6d41073fb1a640680845c7de52995f137e"
|
||||
integrity sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==
|
||||
esbuild-linux-ppc64le@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz#af048ad94eed0ce32f6d5a873f7abe9115012507"
|
||||
integrity sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==
|
||||
|
||||
esbuild-linux-riscv64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz#618853c028178a61837bc799d2013d4695e451c8"
|
||||
integrity sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==
|
||||
esbuild-linux-riscv64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz#423ed4e5927bd77f842bd566972178f424d455e6"
|
||||
integrity sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==
|
||||
|
||||
esbuild-linux-s390x@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz#d1885c4c5a76bbb5a0fe182e2c8c60eb9e29f2a6"
|
||||
integrity sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==
|
||||
esbuild-linux-s390x@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz#21d21eaa962a183bfb76312e5a01cc5ae48ce8eb"
|
||||
integrity sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==
|
||||
|
||||
esbuild-netbsd-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz#69ae917a2ff241b7df1dbf22baf04bd330349e81"
|
||||
integrity sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==
|
||||
esbuild-netbsd-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz#ae75682f60d08560b1fe9482bfe0173e5110b998"
|
||||
integrity sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==
|
||||
|
||||
esbuild-openbsd-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz#db4c8495287a350a6790de22edea247a57c5d47b"
|
||||
integrity sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==
|
||||
esbuild-openbsd-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz#79591a90aa3b03e4863f93beec0d2bab2853d0a8"
|
||||
integrity sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==
|
||||
|
||||
esbuild-sunos-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz#54287ee3da73d3844b721c21bc80c1dc7e1bf7da"
|
||||
integrity sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==
|
||||
esbuild-sunos-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz#fd528aa5da5374b7e1e93d36ef9b07c3dfed2971"
|
||||
integrity sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==
|
||||
|
||||
esbuild-windows-32@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz#f8aaf9a5667630b40f0fb3aa37bf01bbd340ce31"
|
||||
integrity sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==
|
||||
esbuild-windows-32@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz#0e92b66ecdf5435a76813c4bc5ccda0696f4efc3"
|
||||
integrity sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==
|
||||
|
||||
esbuild-windows-64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz#bf54b51bd3e9b0f1886ffdb224a4176031ea0af4"
|
||||
integrity sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==
|
||||
esbuild-windows-64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz#0fc761d785414284fc408e7914226d33f82420d0"
|
||||
integrity sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==
|
||||
|
||||
esbuild-windows-arm64@0.14.54:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz#937d15675a15e4b0e4fafdbaa3a01a776a2be982"
|
||||
integrity sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==
|
||||
esbuild-windows-arm64@0.15.18:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz#5b5bdc56d341d0922ee94965c89ee120a6a86eb7"
|
||||
integrity sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==
|
||||
|
||||
esbuild@^0.14.47:
|
||||
version "0.14.54"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.54.tgz#8b44dcf2b0f1a66fc22459943dccf477535e9aa2"
|
||||
integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==
|
||||
esbuild@^0.15.9:
|
||||
version "0.15.18"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.18.tgz#ea894adaf3fbc036d32320a00d4d6e4978a2f36d"
|
||||
integrity sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==
|
||||
optionalDependencies:
|
||||
"@esbuild/linux-loong64" "0.14.54"
|
||||
esbuild-android-64 "0.14.54"
|
||||
esbuild-android-arm64 "0.14.54"
|
||||
esbuild-darwin-64 "0.14.54"
|
||||
esbuild-darwin-arm64 "0.14.54"
|
||||
esbuild-freebsd-64 "0.14.54"
|
||||
esbuild-freebsd-arm64 "0.14.54"
|
||||
esbuild-linux-32 "0.14.54"
|
||||
esbuild-linux-64 "0.14.54"
|
||||
esbuild-linux-arm "0.14.54"
|
||||
esbuild-linux-arm64 "0.14.54"
|
||||
esbuild-linux-mips64le "0.14.54"
|
||||
esbuild-linux-ppc64le "0.14.54"
|
||||
esbuild-linux-riscv64 "0.14.54"
|
||||
esbuild-linux-s390x "0.14.54"
|
||||
esbuild-netbsd-64 "0.14.54"
|
||||
esbuild-openbsd-64 "0.14.54"
|
||||
esbuild-sunos-64 "0.14.54"
|
||||
esbuild-windows-32 "0.14.54"
|
||||
esbuild-windows-64 "0.14.54"
|
||||
esbuild-windows-arm64 "0.14.54"
|
||||
"@esbuild/android-arm" "0.15.18"
|
||||
"@esbuild/linux-loong64" "0.15.18"
|
||||
esbuild-android-64 "0.15.18"
|
||||
esbuild-android-arm64 "0.15.18"
|
||||
esbuild-darwin-64 "0.15.18"
|
||||
esbuild-darwin-arm64 "0.15.18"
|
||||
esbuild-freebsd-64 "0.15.18"
|
||||
esbuild-freebsd-arm64 "0.15.18"
|
||||
esbuild-linux-32 "0.15.18"
|
||||
esbuild-linux-64 "0.15.18"
|
||||
esbuild-linux-arm "0.15.18"
|
||||
esbuild-linux-arm64 "0.15.18"
|
||||
esbuild-linux-mips64le "0.15.18"
|
||||
esbuild-linux-ppc64le "0.15.18"
|
||||
esbuild-linux-riscv64 "0.15.18"
|
||||
esbuild-linux-s390x "0.15.18"
|
||||
esbuild-netbsd-64 "0.15.18"
|
||||
esbuild-openbsd-64 "0.15.18"
|
||||
esbuild-sunos-64 "0.15.18"
|
||||
esbuild-windows-32 "0.15.18"
|
||||
esbuild-windows-64 "0.15.18"
|
||||
esbuild-windows-arm64 "0.15.18"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
|
@ -1722,6 +1740,11 @@ locate-path@^5.0.0:
|
|||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
|
@ -1924,10 +1947,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
|
|||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
|
||||
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
|
||||
|
||||
postcss@^8.4.16:
|
||||
version "8.4.16"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c"
|
||||
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==
|
||||
postcss@^8.4.18:
|
||||
version "8.4.21"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4"
|
||||
integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==
|
||||
dependencies:
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
|
@ -1972,14 +1995,13 @@ pump@^3.0.0:
|
|||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
||||
react-dom@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-error-boundary@^3.1.4:
|
||||
version "3.1.4"
|
||||
|
@ -2049,13 +2071,12 @@ react-virtuoso@^3.1.3:
|
|||
"@virtuoso.dev/react-urx" "^0.2.12"
|
||||
"@virtuoso.dev/urx" "^0.2.12"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||
react@^18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
readdirp@~3.6.0:
|
||||
version "3.6.0"
|
||||
|
@ -2105,10 +2126,10 @@ resolve@^1.19.0, resolve@^1.22.1:
|
|||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
"rollup@>=2.75.6 <2.77.0 || ~2.77.0":
|
||||
version "2.77.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.77.3.tgz#8f00418d3a2740036e15deb653bed1a90ee0cc12"
|
||||
integrity sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==
|
||||
rollup@^2.79.1:
|
||||
version "2.79.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
||||
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
|
@ -2126,13 +2147,12 @@ sass@^1.54.0:
|
|||
immutable "^4.0.0"
|
||||
source-map-js ">=0.6.2 <2.0.0"
|
||||
|
||||
scheduler@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
screenfull@^5.0.0:
|
||||
version "5.2.0"
|
||||
|
@ -2275,15 +2295,15 @@ vite-plugin-svgr@^2.2.1:
|
|||
"@rollup/pluginutils" "^4.2.1"
|
||||
"@svgr/core" "^6.3.1"
|
||||
|
||||
vite@^3.0.9:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.0.9.tgz#45fac22c2a5290a970f23d66c1aef56a04be8a30"
|
||||
integrity sha512-waYABTM+G6DBTCpYAxvevpG50UOlZuynR0ckTK5PawNVt7ebX6X7wNXHaGIO6wYYFXSM7/WcuFuO2QzhBB6aMw==
|
||||
vite@^3.2.5:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.5.tgz#dee5678172a8a0ab3e547ad4148c3d547f90e86a"
|
||||
integrity sha512-4mVEpXpSOgrssFZAOmGIr85wPHKvaDAcXqxVxVRZhljkJOMZi1ibLibzjLHzJvcok8BMguLc7g1W6W/GqZbLdQ==
|
||||
dependencies:
|
||||
esbuild "^0.14.47"
|
||||
postcss "^8.4.16"
|
||||
esbuild "^0.15.9"
|
||||
postcss "^8.4.18"
|
||||
resolve "^1.22.1"
|
||||
rollup ">=2.75.6 <2.77.0 || ~2.77.0"
|
||||
rollup "^2.79.1"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue