Compare commits

...
52 Commits
Author SHA1 Message Date
MystiPanda 2f77a07b3d Release 1.7.4 2024-07-15 20:56:18 +08:00
MystiPanda 4cd7ccda57 fix: unified switch style 2024-07-15 20:42:00 +08:00
MystiPanda ef337ffb69 feat: optimize the service mode interaction logic. 2024-07-15 20:02:05 +08:00
MystiPanda 9ff5d8d4e0 Revert "fix: try to fix abnormal stuck"
This reverts commit e14ddbe23c.
2024-07-14 12:49:06 +08:00
Sukka 199a5eed60 fix(#1387): avoid catastrophic backtracking (#1396) 2024-07-14 12:26:02 +08:00
MystiPanda e14ddbe23c fix: try to fix abnormal stuck
#1387
2024-07-13 21:41:04 +08:00
MystiPanda 3878e66ea4 chore: update locale 2024-07-13 19:15:07 +08:00
MystiPanda c14a33444b chore: unified icon style 2024-07-13 19:01:16 +08:00
MystiPanda 48f7c15035 feat: display network interface 2024-07-13 14:15:13 +08:00
Avan 32212a46e2 feature: copy clash env (#1391)
* feat: copy clash env

* style: use ContentCopyRounded replace CopyAll
2024-07-13 01:03:46 +08:00
Avan b4025c45da style: EditorViewer title align center (#1390) 2024-07-12 23:29:55 +08:00
MystiPanda bd589c4422 fix: service install failed on macos 2024-07-12 20:54:31 +08:00
dongchengjie f85f7758e6 chore: checkbox items' title & button icons 2024-07-12 18:24:19 +08:00
dongchengjie 49e36b6e00 chore: editor resizing debounce 2024-07-08 23:18:06 +08:00
dongchengjie df6e245e5c chore: remove dialog overflow-scroll 2024-07-08 22:07:43 +08:00
dongchengjie 4e4f9af5b7 fix: editor fails to resize after toggling on macos 2024-07-08 21:40:56 +08:00
MystiPanda aa9e63b51a fix dev.yml 2024-07-08 16:17:23 +08:00
MystiPanda 82340582b2 ci: add dev workflow 2024-07-08 13:20:36 +08:00
Akioe Yu 9d045e24db fix: dnd box z-index (#1353)
* fix: dnd box z-index

* fix: dnd boxes
2024-07-08 10:49:57 +08:00
MystiPanda 1dcc95d16a perf: optimized response speed 2024-07-08 00:29:49 +08:00
dongchengjie 78c32d51db chore: group types locale 2024-07-07 21:56:20 +08:00
❤是纱雾酱哟~ 545a2c6688 ci: Integrate "Winget Releaser" (#1326) 2024-07-07 20:42:17 +08:00
MystiPanda 359d9285fe perf: change port too slow 2024-07-07 18:22:02 +08:00
MystiPanda 1bd51be99c feat: get network interface 2024-07-07 18:13:10 +08:00
dongchengjie ee56080af0 fix: usage percent style 2024-07-07 18:08:02 +08:00
MystiPanda 18196c4a77 fix: trojan uri parser 2024-07-07 11:47:49 +08:00
MystiPanda 14cd3b99cc fix: tray icon size
#1341
2024-07-07 11:18:22 +08:00
MystiPanda ff75fe47ee chore: disable browser autocomplete 2024-07-07 11:16:59 +08:00
MystiPanda 7e2cc180c3 Release 1.7.3 2024-07-06 18:48:25 +08:00
MystiPanda bef9eea87b fix: bypass reg error
#1335
2024-07-06 18:23:23 +08:00
MystiPanda 9cd1aef1db fix: limite cipher types 2024-07-06 10:49:42 +08:00
MystiPanda ab25cf0637 fix: rule parser 2024-07-06 10:35:29 +08:00
MystiPanda 32f37720dd Revert "chore: add base64 decode step"
This reverts commit 38eb3123be.
2024-07-06 10:12:51 +08:00
MystiPanda 766cf3aeae chore: decode base64 2024-07-06 10:12:40 +08:00
MystiPanda 905353d540 fix: some error 2024-07-06 09:54:16 +08:00
MystiPanda 38eb3123be chore: add base64 decode step 2024-07-06 00:57:38 +08:00
MystiPanda 0183edd450 fix: style 2024-07-06 00:45:21 +08:00
MystiPanda 2f640a946e fix: type 2024-07-06 00:23:52 +08:00
MystiPanda ec6c2adf9b chore: Improve URI parser 2024-07-05 22:44:05 +08:00
MystiPanda 5a1edc5ffb feat: finish rpoxies editor 2024-07-05 19:49:32 +08:00
MystiPanda 149d482c7d fix: groups config type error
feat(unfinished): add proxy editor
2024-07-05 00:38:50 +08:00
MystiPanda c9f784e4fa feat: add profile name param for script 2024-07-04 23:11:54 +08:00
MystiPanda 9edafa75e7 build: remove unused resource file 2024-07-04 22:34:48 +08:00
MystiPanda 08baba545d chore: disable unnecessary ports by default 2024-07-04 18:58:34 +08:00
MystiPanda 60d0b29236 feat: support cache for groups editor 2024-07-04 18:53:39 +08:00
MystiPanda f5ee4fb5b5 chore: unified style 2024-07-04 18:50:21 +08:00
MystiPanda 5b7b3be6f9 fix: useseq error 2024-07-04 14:30:34 +08:00
MystiPanda c933560102 fix: locale typo 2024-07-04 14:18:21 +08:00
MystiPanda 12a80f35fe feat: support visual edit for proxy group 2024-07-04 14:13:19 +08:00
MystiPanda 43f901eeb9 fix: rules editor get groups error 2024-07-04 11:03:51 +08:00
MystiPanda e8440e06a1 feat: support get merged rule-set name 2024-07-03 22:13:24 +08:00
dongchengjie 19e9e9d032 fix: remove rule condition where is not required 2024-07-03 17:03:12 +08:00
68 changed files with 5102 additions and 937 deletions
+86
View File
@@ -0,0 +1,86 @@
name: Development Test
on:
workflow_dispatch:
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
concurrency:
# only allow per workflow per commit (and not pr) to run at a time
group: "${{ github.workflow }} - ${{ github.head_ref || github.ref }}"
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
dev:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
bundle: nsis
- os: macos-latest
target: aarch64-apple-darwin
bundle: dmg
- os: macos-latest
target: x86_64-apple-darwin
bundle: dmg
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@1.77.0
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }} -b ${{ matrix.bundle }}
- name: Upload Artifacts
if: matrix.os == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg
if-no-files-found: error
- name: Upload Artifacts
if: matrix.os == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.target }}
path: src-tauri/target/${{ matrix.target }}/release/bundle/nsis/*.exe
if-no-files-found: error
+11
View File
@@ -256,3 +256,14 @@ jobs:
run: pnpm updater-fixed-webview2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
runs-on: windows-latest
needs: [release-update]
steps:
- name: Submit to Winget
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: ClashVergeRev.ClashVergeRev
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
token: ${{ secrets.GITHUB_TOKEN }}
+2 -1
View File
@@ -7,4 +7,5 @@ dist-ssr
update.json
scripts/_env.sh
.vscode
.tool-versions
.tool-versions
.idea
+40
View File
@@ -1,3 +1,43 @@
## v1.7.4
### Features
- 展示局域网 IP 地址信息
- 在设置页面直接复制环境变量
- 优化服务模式安装逻辑
### Performance
- 优化切换订阅速度
- 优化更改端口速度
### Bugs Fixes
- 调整 MacOS 托盘图标大小
- Trojan URI 解析错误
- 卡片拖动显示层级错误
- 代理绕过格式检查错误
- MacOS 下编辑器最大化失败
- MacOS 服务安装失败
- 更改窗口大小导致闪退的问题
---
## v1.7.3
### Features
- 支持可视化编辑订阅代理组
- 支持可视化编辑订阅节点
- 支持可视化编辑订阅规则
- 扩展脚本支持订阅名称参数 `function main(config, profileName)`
### Bugs Fixes
- 代理绕过格式检查错误
---
## v1.7.2
### Break Changes
+8 -6
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.7.2",
"version": "1.7.4",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",
@@ -24,10 +24,10 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.15.21",
"@mui/icons-material": "^5.16.0",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.15.21",
"@mui/x-data-grid": "^7.8.0",
"@mui/material": "^5.16.0",
"@mui/x-data-grid": "^7.9.0",
"@tauri-apps/api": "^1.6.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.0",
@@ -35,12 +35,14 @@
"dayjs": "1.11.5",
"foxact": "^0.2.35",
"i18next": "^23.11.5",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"meta-json-schema": "1.18.6",
"monaco-editor": "^0.49.0",
"monaco-yaml": "^5.2.0",
"nanoid": "^5.0.7",
"peggy": "^4.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4",
@@ -48,7 +50,7 @@
"react-i18next": "^13.5.0",
"react-markdown": "^9.0.1",
"react-monaco-editor": "^0.55.0",
"react-router-dom": "^6.24.0",
"react-router-dom": "^6.24.1",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.7.11",
"sockette": "^2.0.6",
@@ -79,7 +81,7 @@
"sass": "^1.77.6",
"terser": "^5.31.1",
"typescript": "^5.5.3",
"vite": "^5.3.2",
"vite": "^5.3.3",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},
+196 -98
View File
@@ -26,17 +26,17 @@ importers:
specifier: ^3.4.0
version: 3.4.0
"@mui/icons-material":
specifier: ^5.15.21
version: 5.15.21(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
specifier: ^5.16.0
version: 5.16.0(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/lab":
specifier: 5.0.0-alpha.149
version: 5.0.0-alpha.149(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
version: 5.0.0-alpha.149(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/material":
specifier: ^5.15.21
version: 5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^5.16.0
version: 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/x-data-grid":
specifier: ^7.8.0
version: 7.8.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^7.9.0
version: 7.9.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@tauri-apps/api":
specifier: ^1.6.0
version: 1.6.0
@@ -58,6 +58,9 @@ importers:
i18next:
specifier: ^23.11.5
version: 23.11.5
js-base64:
specifier: ^3.7.7
version: 3.7.7
js-yaml:
specifier: ^4.1.0
version: 4.1.0
@@ -76,6 +79,9 @@ importers:
nanoid:
specifier: ^5.0.7
version: 5.0.7
peggy:
specifier: ^4.0.3
version: 4.0.3
react:
specifier: ^18.3.1
version: 18.3.1
@@ -98,8 +104,8 @@ importers:
specifier: ^0.55.0
version: 0.55.0(@types/react@18.3.3)(monaco-editor@0.49.0)(react@18.3.1)
react-router-dom:
specifier: ^6.24.0
version: 6.24.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: ^6.24.1
version: 6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-transition-group:
specifier: ^4.4.5
version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -148,10 +154,10 @@ importers:
version: 4.4.10
"@vitejs/plugin-legacy":
specifier: ^5.4.1
version: 5.4.1(terser@5.31.1)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
version: 5.4.1(terser@5.31.1)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1))
"@vitejs/plugin-react":
specifier: ^4.3.1
version: 4.3.1(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
version: 4.3.1(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1))
adm-zip:
specifier: ^0.5.14
version: 0.5.14
@@ -186,14 +192,14 @@ importers:
specifier: ^5.5.3
version: 5.5.3
vite:
specifier: ^5.3.2
version: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
specifier: ^5.3.3
version: 5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1)
vite-plugin-monaco-editor:
specifier: ^1.1.0
version: 1.1.0(monaco-editor@0.49.0)
vite-plugin-svgr:
specifier: ^4.2.0
version: 4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))
version: 4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1))
packages:
"@actions/github@5.1.1":
@@ -1574,16 +1580,16 @@ packages:
"@types/react":
optional: true
"@mui/core-downloads-tracker@5.15.21":
"@mui/core-downloads-tracker@5.16.0":
resolution:
{
integrity: sha512-dp9lXBaJZzJYeJfQY3Ow4Rb49QaCEdkl2KKYscdQHQm6bMJ+l4XPY3Cd9PCeeJTsHPIDJ60lzXbeRgs6sx/rpw==,
integrity: sha512-8SLffXYPRVpcZx5QzxNE8fytTqzp+IuU3deZbQWg/vSaTlDpR5YVrQ4qQtXTi5cRdhOufV5INylmwlKK+//nPw==,
}
"@mui/icons-material@5.15.21":
"@mui/icons-material@5.16.0":
resolution:
{
integrity: sha512-yqkq1MbdkmX5ZHyvZTBuAaA6RkvoqkoAgwBSx9Oh0L0jAfj9T/Ih/NhMNjkl8PWVSonjfDUkKroBnjRyo/1M9Q==,
integrity: sha512-6ISoOhkp9w5gD0PEW9JklrcbyARDkFWNTBdwXZ1Oy5IGlyu9B0zG0hnUIe4H17IaF1Vgj6C8VI+v4tkSdK0veg==,
}
engines: { node: ">=12.0.0" }
peerDependencies:
@@ -1615,10 +1621,10 @@ packages:
"@types/react":
optional: true
"@mui/material@5.15.21":
"@mui/material@5.16.0":
resolution:
{
integrity: sha512-nTyCcgduKwHqiuQ/B03EQUa+utSMzn2sQp0QAibsnYe4tvc3zkMbO0amKpl48vhABIY3IvT6w9615BFIgMt0YA==,
integrity: sha512-DbR1NckTLpjt9Zut9EGQ70th86HfN0BYQgyYro6aXQrNfjzSwe3BJS1AyBQ5mJ7TdL6YVRqohfukxj9JlqZZUg==,
}
engines: { node: ">=12.0.0" }
peerDependencies:
@@ -1635,10 +1641,10 @@ packages:
"@types/react":
optional: true
"@mui/private-theming@5.15.20":
"@mui/private-theming@5.16.0":
resolution:
{
integrity: sha512-BK8F94AIqSrnaPYXf2KAOjGZJgWfvqAVQ2gVR3EryvQFtuBnG6RwodxrCvd3B48VuMy6Wsk897+lQMUxJyk+6g==,
integrity: sha512-sYpubkO1MZOnxNyVOClrPNOTs0MfuRVVnAvCeMaOaXt6GimgQbnUcshYv2pSr6PFj+Mqzdff/FYOBceK8u5QgA==,
}
engines: { node: ">=12.0.0" }
peerDependencies:
@@ -1664,10 +1670,10 @@ packages:
"@emotion/styled":
optional: true
"@mui/system@5.15.20":
"@mui/system@5.16.0":
resolution:
{
integrity: sha512-LoMq4IlAAhxzL2VNUDBTQxAb4chnBe8JvRINVNDiMtHE2PiPOoHlhOPutSxEbaL5mkECPVWSv6p8JEV+uykwIA==,
integrity: sha512-9YbkC2m3+pNumAvubYv+ijLtog6puJ0fJ6rYfzfLCM47pWrw3m+30nXNM8zMgDaKL6vpfWJcCXm+LPaWBpy7sw==,
}
engines: { node: ">=12.0.0" }
peerDependencies:
@@ -1694,10 +1700,10 @@ packages:
"@types/react":
optional: true
"@mui/utils@5.15.20":
"@mui/utils@5.16.0":
resolution:
{
integrity: sha512-mAbYx0sovrnpAu1zHc3MDIhPqL8RPVC5W5xcO1b7PiSCJPtckIZmBkp8hefamAvUiAV8gpfMOM6Zb+eSisbI2A==,
integrity: sha512-kLLi5J1xY+mwtUlMb8Ubdxf4qFAA1+U7WPBvjM/qQ4CIwLCohNb0sHo1oYPufjSIH/Z9+dhVxD7dJlfGjd1AVA==,
}
engines: { node: ">=12.0.0" }
peerDependencies:
@@ -1707,10 +1713,10 @@ packages:
"@types/react":
optional: true
"@mui/x-data-grid@7.8.0":
"@mui/x-data-grid@7.9.0":
resolution:
{
integrity: sha512-X3t6EVSZ28vVKY9NfqKcClchw2o/KmHsywybp1tNFevIJiwjZSp7NDJ091GyTqMgyDt1Dy5z2hGxoTDUYYfeGg==,
integrity: sha512-RkrVD+tfcR/h3j2p2uqohxA00C5tCJIV5gb5+2ap8XdM0Y8XMF81bB8UADWenU5W83UTErWvtU7n4gCl7hJO9g==,
}
engines: { node: ">=14.0.0" }
peerDependencies:
@@ -1718,6 +1724,15 @@ packages:
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
"@mui/x-internals@7.9.0":
resolution:
{
integrity: sha512-RJRrM6moaDZ8S11gDt8OKVclKm2v9khpIyLkpenNze+tT4dQYoU3liW5P2t31hA4Na/T6JQKNosB4qmB2TYfZw==,
}
engines: { node: ">=14.0.0" }
peerDependencies:
react: ^17.0.0 || ^18.0.0
"@mui/x-tree-view@6.0.0-alpha.1":
resolution:
{
@@ -1797,16 +1812,23 @@ packages:
integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==,
}
"@peggyjs/from-mem@1.3.0":
resolution:
{
integrity: sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==,
}
engines: { node: ">=18" }
"@popperjs/core@2.11.8":
resolution:
{
integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==,
}
"@remix-run/router@1.17.0":
"@remix-run/router@1.17.1":
resolution:
{
integrity: sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==,
integrity: sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==,
}
engines: { node: ">=14.0.0" }
@@ -2255,10 +2277,10 @@ packages:
integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==,
}
"@types/node@20.14.9":
"@types/node@20.14.10":
resolution:
{
integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==,
integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==,
}
"@types/parse-json@4.0.2":
@@ -2328,10 +2350,10 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0
acorn@8.12.0:
acorn@8.12.1:
resolution:
{
integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==,
integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==,
}
engines: { node: ">=0.4.0" }
hasBin: true
@@ -2486,10 +2508,10 @@ packages:
}
engines: { node: ">=10" }
caniuse-lite@1.0.30001639:
caniuse-lite@1.0.30001640:
resolution:
{
integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==,
integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==,
}
ccount@2.0.1:
@@ -2581,6 +2603,13 @@ packages:
integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==,
}
commander@12.1.0:
resolution:
{
integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==,
}
engines: { node: ">=18" }
commander@2.20.3:
resolution:
{
@@ -2720,10 +2749,10 @@ packages:
integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==,
}
electron-to-chromium@1.4.816:
electron-to-chromium@1.4.818:
resolution:
{
integrity: sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==,
integrity: sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==,
}
end-of-stream@1.4.4:
@@ -3128,6 +3157,12 @@ packages:
integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==,
}
js-base64@3.7.7:
resolution:
{
integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==,
}
js-cookie@2.2.1:
resolution:
{
@@ -3244,6 +3279,13 @@ packages:
integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==,
}
lru-cache@6.0.0:
resolution:
{
integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==,
}
engines: { node: ">=10" }
magic-string@0.30.10:
resolution:
{
@@ -3708,6 +3750,14 @@ packages:
}
engines: { node: ">=8" }
peggy@4.0.3:
resolution:
{
integrity: sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==,
}
engines: { node: ">=18" }
hasBin: true
picocolors@1.0.1:
resolution:
{
@@ -3863,20 +3913,20 @@ packages:
}
engines: { node: ">=0.10.0" }
react-router-dom@6.24.0:
react-router-dom@6.24.1:
resolution:
{
integrity: sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==,
integrity: sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==,
}
engines: { node: ">=14.0.0" }
peerDependencies:
react: ">=16.8"
react-dom: ">=16.8"
react-router@6.24.0:
react-router@6.24.1:
resolution:
{
integrity: sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==,
integrity: sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==,
}
engines: { node: ">=14.0.0" }
peerDependencies:
@@ -4028,6 +4078,14 @@ packages:
}
hasBin: true
semver@7.6.0:
resolution:
{
integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==,
}
engines: { node: ">=10" }
hasBin: true
server-only@0.0.1:
resolution:
{
@@ -4066,6 +4124,13 @@ packages:
integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==,
}
source-map-generator@0.8.0:
resolution:
{
integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==,
}
engines: { node: ">= 10" }
source-map-js@1.2.0:
resolution:
{
@@ -4328,10 +4393,10 @@ packages:
}
engines: { node: ">= 10.0.0" }
update-browserslist-db@1.0.16:
update-browserslist-db@1.1.0:
resolution:
{
integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==,
integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==,
}
hasBin: true
peerDependencies:
@@ -4373,10 +4438,10 @@ packages:
peerDependencies:
vite: ^2.6.0 || 3 || 4 || 5
vite@5.3.2:
vite@5.3.3:
resolution:
{
integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==,
integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==,
}
engines: { node: ^18.0.0 || >=20.0.0 }
hasBin: true
@@ -5531,7 +5596,7 @@ snapshots:
"@babel/runtime": 7.24.7
"@floating-ui/react-dom": 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/types": 7.2.14(@types/react@18.3.3)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@popperjs/core": 2.11.8
clsx: 2.1.1
prop-types: 15.8.1
@@ -5545,7 +5610,7 @@ snapshots:
"@babel/runtime": 7.24.7
"@floating-ui/react-dom": 2.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/types": 7.2.14(@types/react@18.3.3)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@popperjs/core": 2.11.8
clsx: 2.1.1
prop-types: 15.8.1
@@ -5554,25 +5619,25 @@ snapshots:
optionalDependencies:
"@types/react": 18.3.3
"@mui/core-downloads-tracker@5.15.21": {}
"@mui/core-downloads-tracker@5.16.0": {}
"@mui/icons-material@5.15.21(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)":
"@mui/icons-material@5.16.0(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/material": 5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/material": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
optionalDependencies:
"@types/react": 18.3.3
"@mui/lab@5.0.0-alpha.149(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
"@mui/lab@5.0.0-alpha.149(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/base": 5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/material": 5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/system": 5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/material": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/system": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/types": 7.2.14(@types/react@18.3.3)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/x-tree-view": 6.0.0-alpha.1(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/base@5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@mui/x-tree-view": 6.0.0-alpha.1(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/base@5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
@@ -5582,14 +5647,14 @@ snapshots:
"@emotion/styled": 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@types/react": 18.3.3
"@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
"@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/base": 5.0.0-beta.40(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/core-downloads-tracker": 5.15.21
"@mui/system": 5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/core-downloads-tracker": 5.16.0
"@mui/system": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/types": 7.2.14(@types/react@18.3.3)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@types/react-transition-group": 4.4.10
clsx: 2.1.1
csstype: 3.1.3
@@ -5603,10 +5668,10 @@ snapshots:
"@emotion/styled": 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@types/react": 18.3.3
"@mui/private-theming@5.15.20(@types/react@18.3.3)(react@18.3.1)":
"@mui/private-theming@5.16.0(@types/react@18.3.3)(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
prop-types: 15.8.1
react: 18.3.1
optionalDependencies:
@@ -5623,13 +5688,13 @@ snapshots:
"@emotion/react": 11.11.4(@types/react@18.3.3)(react@18.3.1)
"@emotion/styled": 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/system@5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)":
"@mui/system@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/private-theming": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/private-theming": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@mui/styled-engine": 5.15.14(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(react@18.3.1)
"@mui/types": 7.2.14(@types/react@18.3.3)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
clsx: 2.1.1
csstype: 3.1.3
prop-types: 15.8.1
@@ -5643,7 +5708,7 @@ snapshots:
optionalDependencies:
"@types/react": 18.3.3
"@mui/utils@5.15.20(@types/react@18.3.3)(react@18.3.1)":
"@mui/utils@5.16.0(@types/react@18.3.3)(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@types/prop-types": 15.7.12
@@ -5653,12 +5718,13 @@ snapshots:
optionalDependencies:
"@types/react": 18.3.3
"@mui/x-data-grid@7.8.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
"@mui/x-data-grid@7.9.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/material": 5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/system": 5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/material": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/system": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@mui/x-internals": 7.9.0(@types/react@18.3.3)(react@18.3.1)
clsx: 2.1.1
prop-types: 15.8.1
react: 18.3.1
@@ -5669,15 +5735,23 @@ snapshots:
- "@emotion/styled"
- "@types/react"
"@mui/x-tree-view@6.0.0-alpha.1(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/base@5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/material@5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
"@mui/x-internals@7.9.0(@types/react@18.3.3)(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
react: 18.3.1
transitivePeerDependencies:
- "@types/react"
"@mui/x-tree-view@6.0.0-alpha.1(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@mui/base@5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/material@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)":
dependencies:
"@babel/runtime": 7.24.7
"@emotion/react": 11.11.4(@types/react@18.3.3)(react@18.3.1)
"@emotion/styled": 11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/base": 5.0.0-beta.20(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/material": 5.15.21(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/system": 5.15.20(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.15.20(@types/react@18.3.3)(react@18.3.1)
"@mui/material": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
"@mui/system": 5.16.0(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@emotion/styled@11.11.5(@emotion/react@11.11.4(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1))(@types/react@18.3.3)(react@18.3.1)
"@mui/utils": 5.16.0(@types/react@18.3.3)(react@18.3.1)
"@types/react-transition-group": 4.4.10
clsx: 2.1.1
prop-types: 15.8.1
@@ -5751,9 +5825,13 @@ snapshots:
dependencies:
"@octokit/openapi-types": 12.11.0
"@peggyjs/from-mem@1.3.0":
dependencies:
semver: 7.6.0
"@popperjs/core@2.11.8": {}
"@remix-run/router@1.17.0": {}
"@remix-run/router@1.17.1": {}
"@rollup/pluginutils@5.1.0(rollup@4.18.0)":
dependencies:
@@ -5959,7 +6037,7 @@ snapshots:
"@types/fs-extra@9.0.13":
dependencies:
"@types/node": 20.14.9
"@types/node": 20.14.10
"@types/hast@3.0.4":
dependencies:
@@ -5983,7 +6061,7 @@ snapshots:
"@types/ms@0.7.34": {}
"@types/node@20.14.9":
"@types/node@20.14.10":
dependencies:
undici-types: 5.26.5
@@ -6010,7 +6088,7 @@ snapshots:
"@ungap/structured-clone@1.2.0": {}
"@vitejs/plugin-legacy@5.4.1(terser@5.31.1)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))":
"@vitejs/plugin-legacy@5.4.1(terser@5.31.1)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1))":
dependencies:
"@babel/core": 7.24.7
"@babel/preset-env": 7.24.7(@babel/core@7.24.7)
@@ -6021,22 +6099,22 @@ snapshots:
regenerator-runtime: 0.14.1
systemjs: 6.15.1
terser: 5.31.1
vite: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1)
transitivePeerDependencies:
- supports-color
"@vitejs/plugin-react@4.3.1(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1))":
"@vitejs/plugin-react@4.3.1(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1))":
dependencies:
"@babel/core": 7.24.7
"@babel/plugin-transform-react-jsx-self": 7.24.7(@babel/core@7.24.7)
"@babel/plugin-transform-react-jsx-source": 7.24.7(@babel/core@7.24.7)
"@types/babel__core": 7.20.5
react-refresh: 0.14.2
vite: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1)
transitivePeerDependencies:
- supports-color
acorn@8.12.0: {}
acorn@8.12.1: {}
adm-zip@0.5.14: {}
@@ -6127,10 +6205,10 @@ snapshots:
browserslist@4.23.1:
dependencies:
caniuse-lite: 1.0.30001639
electron-to-chromium: 1.4.816
caniuse-lite: 1.0.30001640
electron-to-chromium: 1.4.818
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.1)
update-browserslist-db: 1.1.0(browserslist@4.23.1)
buffer-from@1.1.2: {}
@@ -6138,7 +6216,7 @@ snapshots:
camelcase@6.3.0: {}
caniuse-lite@1.0.30001639: {}
caniuse-lite@1.0.30001640: {}
ccount@2.0.1: {}
@@ -6186,6 +6264,8 @@ snapshots:
comma-separated-tokens@2.0.3: {}
commander@12.1.0: {}
commander@2.20.3: {}
convert-source-map@1.9.0: {}
@@ -6259,7 +6339,7 @@ snapshots:
no-case: 3.0.4
tslib: 2.6.3
electron-to-chromium@1.4.816: {}
electron-to-chromium@1.4.818: {}
end-of-stream@1.4.4:
dependencies:
@@ -6492,6 +6572,8 @@ snapshots:
isexe@2.0.0: {}
js-base64@3.7.7: {}
js-cookie@2.2.1: {}
js-tokens@4.0.0: {}
@@ -6542,6 +6624,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
magic-string@0.30.10:
dependencies:
"@jridgewell/sourcemap-codec": 1.4.15
@@ -6914,6 +7000,12 @@ snapshots:
path-type@4.0.0: {}
peggy@4.0.3:
dependencies:
"@peggyjs/from-mem": 1.3.0
commander: 12.1.0
source-map-generator: 0.8.0
picocolors@1.0.1: {}
picomatch@2.3.1: {}
@@ -7010,16 +7102,16 @@ snapshots:
react-refresh@0.14.2: {}
react-router-dom@6.24.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-router-dom@6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
"@remix-run/router": 1.17.0
"@remix-run/router": 1.17.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router: 6.24.0(react@18.3.1)
react-router: 6.24.1(react@18.3.1)
react-router@6.24.0(react@18.3.1):
react-router@6.24.1(react@18.3.1):
dependencies:
"@remix-run/router": 1.17.0
"@remix-run/router": 1.17.1
react: 18.3.1
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -7134,6 +7226,10 @@ snapshots:
semver@6.3.1: {}
semver@7.6.0:
dependencies:
lru-cache: 6.0.0
server-only@0.0.1: {}
shebang-command@2.0.0:
@@ -7151,6 +7247,8 @@ snapshots:
sockette@2.0.6: {}
source-map-generator@0.8.0: {}
source-map-js@1.2.0: {}
source-map-support@0.5.21:
@@ -7205,7 +7303,7 @@ snapshots:
terser@5.31.1:
dependencies:
"@jridgewell/source-map": 0.3.6
acorn: 8.12.0
acorn: 8.12.1
commander: 2.20.3
source-map-support: 0.5.21
@@ -7288,7 +7386,7 @@ snapshots:
universalify@2.0.1: {}
update-browserslist-db@1.0.16(browserslist@4.23.1):
update-browserslist-db@1.1.0(browserslist@4.23.1):
dependencies:
browserslist: 4.23.1
escalade: 3.1.2
@@ -7313,24 +7411,24 @@ snapshots:
dependencies:
monaco-editor: 0.49.0
vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)):
vite-plugin-svgr@4.2.0(rollup@4.18.0)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1)):
dependencies:
"@rollup/pluginutils": 5.1.0(rollup@4.18.0)
"@svgr/core": 8.1.0(typescript@5.5.3)
"@svgr/plugin-jsx": 8.1.0(@svgr/core@8.1.0(typescript@5.5.3))
vite: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1)
vite: 5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1)
transitivePeerDependencies:
- rollup
- supports-color
- typescript
vite@5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1):
vite@5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1):
dependencies:
esbuild: 0.21.5
postcss: 8.4.39
rollup: 4.18.0
optionalDependencies:
"@types/node": 20.14.9
"@types/node": 20.14.10
fsevents: 2.3.3
sass: 1.77.6
terser: 5.31.1
-12
View File
@@ -400,16 +400,6 @@ const resolveUninstall = () => {
});
};
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/set_dns.sh`,
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/unset_dns.sh`,
});
const resolveMmdb = () =>
resolveResource({
file: "Country.mmdb",
@@ -449,8 +439,6 @@ const tasks = [
{ name: "service", func: resolveService, retry: 5 },
{ name: "install", func: resolveInstall, retry: 5 },
{ name: "uninstall", func: resolveUninstall, retry: 5 },
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 },
{ name: "geosite", func: resolveGeosite, retry: 5 },
{ name: "geoip", func: resolveGeoIP, retry: 5 },
+273 -230
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.7.2"
version = "1.7.4"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -38,6 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
tauri = { version="1", features = [ "fs-read-file", "fs-exists", "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "icon-ico", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "devtools"] }
network-interface = { version = "2.0.0", features = ["serde"] }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
deelevate = "0.2.0"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+37
View File
@@ -6,12 +6,19 @@ use crate::{
};
use crate::{ret_err, wrap_err};
use anyhow::{Context, Result};
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque};
use sysproxy::{Autoproxy, Sysproxy};
use tauri::{api, Manager};
type CmdResult<T = ()> = Result<T, String>;
#[tauri::command]
pub fn copy_clash_env(app_handle: tauri::AppHandle) -> CmdResult {
feat::copy_clash_env(&app_handle);
Ok(())
}
#[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> {
Ok(Config::profiles().data().clone())
@@ -322,6 +329,36 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
}
}
#[tauri::command]
pub fn get_network_interfaces() -> Vec<String> {
use sysinfo::Networks;
let mut result = Vec::new();
let networks = Networks::new_with_refreshed_list();
for (interface_name, _) in &networks {
result.push(interface_name.clone());
}
return result;
}
#[tauri::command]
pub fn get_network_interfaces_info() -> CmdResult<Vec<NetworkInterface>> {
use network_interface::NetworkInterface;
use network_interface::NetworkInterfaceConfig;
let names = get_network_interfaces();
let interfaces = wrap_err!(NetworkInterface::show())?;
let mut result = Vec::new();
for interface in interfaces {
if names.contains(&interface.name) {
result.push(interface);
}
}
Ok(result)
}
#[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_window("main") {
+4 -4
View File
@@ -227,16 +227,16 @@ impl IVerge {
#[cfg(not(target_os = "windows"))]
verge_redir_port: Some(7895),
#[cfg(not(target_os = "windows"))]
verge_redir_enabled: Some(true),
verge_redir_enabled: Some(false),
#[cfg(target_os = "linux")]
verge_tproxy_port: Some(7896),
#[cfg(target_os = "linux")]
verge_tproxy_enabled: Some(true),
verge_tproxy_enabled: Some(false),
verge_mixed_port: Some(7897),
verge_socks_port: Some(7898),
verge_socks_enabled: Some(true),
verge_socks_enabled: Some(false),
verge_port: Some(7899),
verge_http_enabled: Some(true),
verge_http_enabled: Some(false),
enable_proxy_guard: Some(false),
use_default_bypass: Some(true),
proxy_guard_duration: Some(30),
+34 -31
View File
@@ -7,7 +7,7 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use serde_yaml::Mapping;
use std::{sync::Arc, time::Duration};
use sysinfo::System;
use sysinfo::{ProcessRefreshKind, RefreshKind, System};
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep;
@@ -44,13 +44,27 @@ impl CoreManager {
let config_path = dirs::path_to_str(&config_path)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
let app_dir = dirs::app_home_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?;
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let test_dir = dirs::app_home_dir()?.join("test");
let test_dir = dirs::path_to_str(&test_dir)?;
let output = Command::new_sidecar(clash_core)?
.args(["-t", "-d", app_dir, "-f", config_path])
.args(["-t", "-d", test_dir, "-f", config_path])
.output()?;
if !output.status.success() {
@@ -70,12 +84,6 @@ impl CoreManager {
pub async fn run_core(&self) -> Result<()> {
let config_path = Config::generate_file(ConfigType::Run)?;
#[allow(unused_mut)]
let mut should_kill = match self.sidecar.lock().take() {
Some(_) => true,
None => false,
};
// 关闭tun模式
let mut disable = Mapping::new();
let mut tun = Mapping::new();
@@ -84,23 +92,19 @@ impl CoreManager {
log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await;
let mut system = System::new();
system.refresh_all();
let procs = system.processes_by_name("verge-mihomo");
for proc in procs {
log::debug!(target: "app", "kill all clash process");
proc.kill();
}
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await);
should_kill = true;
}
} else {
let system = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo");
// 这里得等一会儿
if should_kill {
sleep(Duration::from_millis(500)).await;
for proc in procs {
log::debug!(target: "app", "kill all clash process");
proc.kill();
}
}
// 服务模式
@@ -237,8 +241,9 @@ impl CoreManager {
let mut sidecar = self.sidecar.lock();
let _ = sidecar.take();
let mut system = System::new();
system.refresh_all();
let system = System::new_with_specifics(
RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo");
for proc in procs {
log::debug!(target: "app", "kill all clash process");
@@ -287,7 +292,6 @@ impl CoreManager {
/// 如果涉及端口和外部控制则需要重启
pub async fn update_config(&self) -> Result<()> {
log::debug!(target: "app", "try to update clash config");
// 更新订阅
Config::generate().await?;
@@ -299,20 +303,19 @@ impl CoreManager {
let path = dirs::path_to_str(&path)?;
// 发送请求 发送5次
for i in 0..5 {
for i in 0..10 {
match clash_api::put_configs(path).await {
Ok(_) => break,
Err(err) => {
if i < 4 {
if i < 9 {
log::info!(target: "app", "{err}");
} else {
bail!(err);
}
}
}
sleep(Duration::from_millis(250)).await;
sleep(Duration::from_millis(100)).await;
}
Ok(())
}
}
+6
View File
@@ -103,6 +103,12 @@ pub async fn install_service() -> Result<()> {
if !installer_path.exists() {
bail!("installer not found");
}
let _ = StdCommand::new("chmod")
.arg("+x")
.arg(installer_path.to_string_lossy().replace(" ", "\\ "))
.output();
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
+1 -1
View File
@@ -1,4 +1,4 @@
function main(config) {
function main(config, _name) {
if (config.mode === "script") {
config.mode = "rule";
}
@@ -1,4 +1,4 @@
function main(config) {
function main(config, _name) {
if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") {
+15 -7
View File
@@ -32,21 +32,21 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
verge.clash_core.clone(),
verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(true),
verge.verge_http_enabled.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(false),
verge.verge_http_enabled.unwrap_or(false),
)
};
#[cfg(not(target_os = "windows"))]
let redir_enabled = {
let verge = Config::verge();
let verge = verge.latest();
verge.verge_redir_enabled.unwrap_or(true)
verge.verge_redir_enabled.unwrap_or(false)
};
#[cfg(target_os = "linux")]
let tproxy_enabled = {
let verge = Config::verge();
let verge = verge.latest();
verge.verge_tproxy_enabled.unwrap_or(true)
verge.verge_tproxy_enabled.unwrap_or(false)
};
// 从profiles里拿东西
@@ -59,6 +59,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups_item,
global_merge,
global_script,
profile_name,
) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
@@ -123,6 +124,12 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let name = profiles
.get_item(&profiles.get_current().unwrap_or_default())
.ok()
.and_then(|item| item.name.clone())
.unwrap_or_default();
(
current,
merge,
@@ -132,6 +139,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups,
global_merge,
global_script,
name,
)
};
@@ -147,7 +155,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
match use_script(script, config.to_owned(), profile_name.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@@ -180,7 +188,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
if let ChainType::Script(script) = script_item.data {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
match use_script(script, config.to_owned(), profile_name.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
@@ -239,7 +247,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned()) {
match use_script(script, config.to_owned(), "".to_string()) {
Ok((res_config, _)) => {
config = res_config;
}
+7 -3
View File
@@ -2,7 +2,11 @@ use super::use_lowercase;
use anyhow::{Error, Result};
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
pub fn use_script(
script: String,
config: Mapping,
name: String,
) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex};
let mut context = Context::default();
@@ -42,7 +46,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
let code = format!(
r#"try{{
{script};
JSON.stringify(main({config_str})||'')
JSON.stringify(main({config_str},'{name}')||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
@@ -97,7 +101,7 @@ fn test_script() {
"#;
let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config).unwrap();
let (config, results) = use_script(script.into(), config, "".to_string()).unwrap();
let config_str = serde_yaml::to_string(&config).unwrap();
+9 -7
View File
@@ -16,14 +16,7 @@ pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
val.as_sequence().unwrap_or(&Sequence::default()).clone()
});
let mut seq = origin_seq.clone();
prepend.reverse();
for item in prepend {
seq.insert(0, item);
}
for item in append {
seq.push(item);
}
let mut delete_names = Vec::new();
for item in delete {
let item = item.clone();
@@ -47,6 +40,15 @@ pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
}
});
prepend.reverse();
for item in prepend {
seq.insert(0, item);
}
for item in append {
seq.push(item);
}
let mut config = config.clone();
config.insert(Value::from(name), Value::from(seq));
return config;
+37 -52
View File
@@ -107,48 +107,13 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
let res = {
let redir_port = patch.get("redir-port");
let tproxy_port = patch.get("tproxy-port");
let mixed_port = patch.get("mixed-port");
let socks_port = patch.get("socks-port");
let port = patch.get("port");
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
if mixed_port.is_some() && !enable_random_port {
let changed = mixed_port.unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用
if changed {
if let Some(port) = mixed_port.unwrap().as_u64() {
if !port_scanner::local_port_available(port as u16) {
Config::clash().discard();
bail!("port already in use");
}
}
}
};
// 激活订阅
if redir_port.is_some()
|| tproxy_port.is_some()
|| mixed_port.is_some()
|| socks_port.is_some()
|| port.is_some()
|| patch.get("secret").is_some()
|| patch.get("external-controller").is_some()
{
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
handle::Handle::refresh_clash();
}
// 更新系统代理
if mixed_port.is_some() {
log_err!(sysopt::Sysopt::global().init_sysproxy());
}
if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part());
}
@@ -174,7 +139,6 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
/// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
@@ -182,7 +146,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let pac_content = patch.pac_file_content;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
let mixed_port = patch.verge_mixed_port;
#[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon;
let common_tray_icon = patch.common_tray_icon;
@@ -190,41 +154,62 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let tun_tray_icon = patch.tun_tray_icon;
#[cfg(not(target_os = "windows"))]
let redir_enabled = patch.verge_redir_enabled;
#[cfg(not(target_os = "windows"))]
let redir_port = patch.verge_redir_port;
#[cfg(target_os = "linux")]
let tproxy_enabled = patch.verge_tproxy_enabled;
#[cfg(target_os = "linux")]
let tproxy_port = patch.verge_tproxy_port;
let socks_enabled = patch.verge_socks_enabled;
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let res = {
let service_mode = patch.enable_service_mode;
let mut generated = false;
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
Config::generate().await?;
CoreManager::global().run_core().await?;
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
} else if tun_mode.is_some() {
update_core_config().await?;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
if redir_enabled.is_some() || redir_port.is_some() {
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
if tproxy_enabled.is_some() || tproxy_port.is_some() {
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
}
if socks_enabled.is_some() || http_enabled.is_some() {
Config::generate().await?;
CoreManager::global().run_core().await?;
if socks_enabled.is_some()
|| http_enabled.is_some()
|| socks_port.is_some()
|| http_port.is_some()
|| mixed_port.is_some()
{
if !generated {
Config::generate().await?;
CoreManager::global().run_core().await?;
}
}
if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?;
}
if system_proxy.is_some()
|| proxy_bypass.is_some()
|| port.is_some()
|| mixed_port.is_some()
|| pac.is_some()
|| pac_content.is_some()
{
+3
View File
@@ -51,6 +51,7 @@ fn main() -> std::io::Result<()> {
cmds::open_web_url,
cmds::open_core_dir,
cmds::get_portable_flag,
cmds::get_network_interfaces,
// cmds::kill_sidecar,
cmds::restart_sidecar,
// clash
@@ -63,6 +64,7 @@ fn main() -> std::io::Result<()> {
cmds::get_runtime_exists,
cmds::get_runtime_logs,
cmds::uwp::invoke_uwp_tool,
cmds::copy_clash_env,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
@@ -72,6 +74,7 @@ fn main() -> std::io::Result<()> {
cmds::download_icon_cache,
cmds::open_devtools,
cmds::exit_app,
cmds::get_network_interfaces_info,
// cmds::update_hotkeys,
// profile
cmds::get_profiles,
+1 -1
View File
@@ -27,7 +27,7 @@ pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Cla
/// enhanced profile
pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
function main(config) {
function main(config, profileName) {
return config;
}
";
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Clash Verge",
"version": "1.7.2"
"version": "1.7.4"
},
"build": {
"distDir": "../dist",
+6 -2
View File
@@ -1,7 +1,11 @@
import { createRoot } from "react-dom/client";
import { ReactNode, useState, useEffect } from "react";
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material";
import {
CloseRounded,
CheckCircleRounded,
ErrorRounded,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge";
import { appWindow } from "@tauri-apps/api/window";
interface InnerProps {
@@ -81,7 +85,7 @@ const NoticeInner = (props: InnerProps) => {
transitionDuration={200}
action={
<IconButton size="small" color="inherit" onClick={onBtnClose}>
<Close fontSize="inherit" />
<CloseRounded fontSize="inherit" />
</IconButton>
}
/>
+2 -2
View File
@@ -26,7 +26,7 @@ type SearchProps = {
export const BaseSearchBox = styled((props: SearchProps) => {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? true);
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
const [matchWholeWord, setMatchWholeWord] = useState(
props.matchWholeWord ?? false
);
@@ -96,7 +96,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
return (
<Tooltip title={errorMessage} placement="bottom-start">
<TextField
autoComplete="off"
autoComplete="new-password"
inputRef={inputRef}
hiddenLabel
fullWidth
+1 -1
View File
@@ -4,7 +4,7 @@ export const BaseStyledSelect = styled((props: SelectProps<string>) => {
return (
<Select
size="small"
autoComplete="off"
autoComplete="new-password"
sx={{
width: 120,
height: 33.375,
@@ -6,7 +6,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
return (
<TextField
autoComplete="off"
autoComplete="new-password"
hiddenLabel
fullWidth
size="small"
+1
View File
@@ -11,6 +11,7 @@ export const Switch = styled((props: SwitchProps) => (
width: 42,
height: 26,
padding: 0,
marginRight: 1,
"& .MuiSwitch-switchBase": {
padding: 0,
margin: 2,
+6 -6
View File
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react";
import { Box, Typography } from "@mui/material";
import {
ArrowDownward,
ArrowUpward,
MemoryOutlined,
ArrowDownwardRounded,
ArrowUpwardRounded,
MemoryRounded,
} from "@mui/icons-material";
import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
@@ -153,7 +153,7 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}>
<Box title={t("Upload Speed")} {...boxStyle}>
<ArrowUpward
<ArrowUpwardRounded
{...iconStyle}
color={+up > 0 ? "secondary" : "disabled"}
/>
@@ -164,7 +164,7 @@ export const LayoutTraffic = () => {
</Box>
<Box title={t("Download Speed")} {...boxStyle}>
<ArrowDownward
<ArrowDownwardRounded
{...iconStyle}
color={+down > 0 ? "primary" : "disabled"}
/>
@@ -184,7 +184,7 @@ export const LayoutTraffic = () => {
isDebug && (await gc());
}}
>
<MemoryOutlined {...iconStyle} />
<MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography>
</Box>
+25 -11
View File
@@ -10,14 +10,17 @@ import {
DialogTitle,
IconButton,
} from "@mui/material";
import FormatPaintIcon from "@mui/icons-material/FormatPaint";
import OpenInFullIcon from "@mui/icons-material/OpenInFull";
import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen";
import {
FormatPaintRounded,
OpenInFullRounded,
CloseFullscreenRounded,
} from "@mui/icons-material";
import { useThemeMode } from "@/services/states";
import { Notice } from "@/components/base";
import { nanoid } from "nanoid";
import { appWindow } from "@tauri-apps/api/window";
import getSystem from "@/utils/get-system";
import debounce from "@/utils/debounce";
import * as monaco from "monaco-editor";
import MonacoEditor from "react-monaco-editor";
@@ -144,12 +147,19 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
}
});
const editorResize = debounce(() => {
editorRef.current?.layout();
setTimeout(() => editorRef.current?.layout(), 500);
}, 100);
useEffect(() => {
const unlistenResized = appWindow.onResized(() => {
const onResized = debounce(() => {
editorResize();
appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized);
});
});
}, 100);
const unlistenResized = appWindow.onResized(onResized);
return () => {
unlistenResized.then((fn) => fn());
@@ -162,7 +172,13 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ width: "auto", height: "calc(100vh - 185px)" }}>
<DialogContent
sx={{
width: "auto",
height: "calc(100vh - 185px)",
overflow: "hidden",
}}
>
<MonacoEditor
language={language}
theme={themeMode === "light" ? "vs" : "vs-dark"}
@@ -209,17 +225,15 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
?.run()
}
>
<FormatPaintIcon fontSize="inherit" />
<FormatPaintRounded fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
color="inherit"
title={t(isMaximized ? "Minimize" : "Maximize")}
onClick={() =>
appWindow.toggleMaximize().then(() => editorRef.current?.layout())
}
onClick={() => appWindow.toggleMaximize().then(editorResize)}
>
{isMaximized ? <CloseFullscreenIcon /> : <OpenInFullIcon />}
{isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
</IconButton>
</ButtonGroup>
</DialogContent>
+173
View File
@@ -0,0 +1,173 @@
import {
Box,
IconButton,
ListItem,
ListItemText,
alpha,
styled,
} from "@mui/material";
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { downloadIconCache } from "@/services/cmds";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { useEffect, useState } from "react";
interface Props {
type: "prepend" | "original" | "delete" | "append";
group: IProxyGroupConfig;
onDelete: () => void;
}
export const GroupItem = (props: Props) => {
let { type, group, onDelete } = props;
const sortable = type === "prepend" || type === "append";
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = sortable
? useSortable({ id: group.name })
: {
attributes: {},
listeners: {},
setNodeRef: null,
transform: null,
transition: null,
isDragging: false,
};
const [iconCachePath, setIconCachePath] = useState("");
useEffect(() => {
initIconCachePath();
}, [group]);
async function initIconCachePath() {
if (group.icon && group.icon.trim().startsWith("http")) {
const fileName =
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
const iconPath = await downloadIconCache(group.icon, fileName);
setIconCachePath(convertFileSrc(iconPath));
}
}
function getFileName(url: string) {
return url.substring(url.lastIndexOf("/") + 1);
}
return (
<ListItem
dense
sx={({ palette }) => ({
position: "relative",
background:
type === "original"
? palette.mode === "dark"
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})}
>
{group.icon && group.icon?.trim().startsWith("http") && (
<img
src={iconCachePath === "" ? group.icon : iconCachePath}
width="32px"
style={{
marginRight: "12px",
borderRadius: "6px",
}}
/>
)}
{group.icon && group.icon?.trim().startsWith("data") && (
<img
src={group.icon}
width="32px"
style={{
marginRight: "12px",
borderRadius: "6px",
}}
/>
)}
{group.icon && group.icon?.trim().startsWith("<svg") && (
<img
src={`data:image/svg+xml;base64,${btoa(group.icon ?? "")}`}
width="32px"
/>
)}
<ListItemText
{...attributes}
{...listeners}
ref={setNodeRef}
sx={{ cursor: sortable ? "move" : "" }}
primary={
<StyledPrimary
sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
>
{group.name}
</StyledPrimary>
}
secondary={
<ListItemTextChild
sx={{
overflow: "hidden",
display: "flex",
alignItems: "center",
pt: "2px",
}}
>
<Box sx={{ marginTop: "2px" }}>
<StyledTypeBox>{group.type}</StyledTypeBox>
</Box>
</ListItemTextChild>
}
secondaryTypographyProps={{
sx: {
display: "flex",
alignItems: "center",
color: "#ccc",
},
}}
/>
<IconButton onClick={onDelete}>
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
</IconButton>
</ListItem>
);
};
const StyledPrimary = styled("div")`
font-size: 15px;
font-weight: 700;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
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),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
padding: "0 4px",
lineHeight: 1.5,
marginRight: "8px",
}));
@@ -0,0 +1,918 @@
import { useEffect, useMemo, useState } from "react";
import { useLockFn } from "ahooks";
import yaml from "js-yaml";
import { useTranslation } from "react-i18next";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import {
Autocomplete,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
InputAdornment,
List,
ListItem,
ListItemText,
TextField,
styled,
} from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { GroupItem } from "@/components/profile/group-item";
import {
getNetworkInterfaces,
readProfileFile,
saveProfileFile,
} from "@/services/cmds";
import { Notice, Switch } from "@/components/base";
import getSystem from "@/utils/get-system";
import { BaseSearchBox } from "../base/base-search-box";
import { Virtuoso } from "react-virtuoso";
import MonacoEditor from "react-monaco-editor";
import { useThemeMode } from "@/services/states";
import { Controller, useForm } from "react-hook-form";
interface Props {
proxiesUid: string;
mergeUid: string;
profileUid: string;
property: string;
open: boolean;
onClose: () => void;
onSave?: (prev?: string, curr?: string) => void;
}
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
export const GroupsEditorViewer = (props: Props) => {
const { mergeUid, proxiesUid, profileUid, property, open, onClose, onSave } =
props;
const { t } = useTranslation();
const themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
const [visualization, setVisualization] = useState(true);
const [match, setMatch] = useState(() => (_: string) => true);
const [interfaceNameList, setInterfaceNameList] = useState<string[]>([]);
const { control, watch, register, ...formIns } = useForm<IProxyGroupConfig>({
defaultValues: {
type: "select",
name: "",
interval: 300,
timeout: 5000,
"max-failed-times": 5,
lazy: true,
},
});
const [groupList, setGroupList] = useState<IProxyGroupConfig[]>([]);
const [proxyPolicyList, setProxyPolicyList] = useState<string[]>([]);
const [proxyProviderList, setProxyProviderList] = useState<string[]>([]);
const [prependSeq, setPrependSeq] = useState<IProxyGroupConfig[]>([]);
const [appendSeq, setAppendSeq] = useState<IProxyGroupConfig[]>([]);
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
const filteredPrependSeq = useMemo(
() => prependSeq.filter((group) => match(group.name)),
[prependSeq, match]
);
const filteredGroupList = useMemo(
() => groupList.filter((group) => match(group.name)),
[groupList, match]
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((group) => match(group.name)),
[appendSeq, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const reorder = (
list: IProxyGroupConfig[],
startIndex: number,
endIndex: number
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const onPrependDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = 0;
let overIndex = 0;
prependSeq.forEach((item, index) => {
if (item.name === active.id) {
activeIndex = index;
}
if (item.name === over.id) {
overIndex = index;
}
});
setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
}
}
};
const onAppendDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = 0;
let overIndex = 0;
appendSeq.forEach((item, index) => {
if (item.name === active.id) {
activeIndex = index;
}
if (item.name === over.id) {
overIndex = index;
}
});
setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
}
}
};
const fetchContent = async () => {
let data = await readProfileFile(property);
let obj = yaml.load(data) as ISeqProfileConfig | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
setPrevData(data);
setCurrData(data);
};
useEffect(() => {
if (currData === "") return;
if (visualization !== true) return;
let obj = yaml.load(currData) as {
prepend: [];
append: [];
delete: [];
} | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
}, [visualization]);
useEffect(() => {
if (prependSeq && appendSeq && deleteSeq)
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{
forceQuotes: true,
}
)
);
}, [prependSeq, appendSeq, deleteSeq]);
const fetchProxyPolicy = async () => {
let data = await readProfileFile(profileUid);
let proxiesData = await readProfileFile(proxiesUid);
let originGroupsObj = yaml.load(data) as {
"proxy-groups": IProxyGroupConfig[];
} | null;
let originProxiesObj = yaml.load(data) as { proxies: [] } | null;
let originProxies = originProxiesObj?.proxies || [];
let moreProxiesObj = yaml.load(proxiesData) as ISeqProfileConfig | null;
let morePrependProxies = moreProxiesObj?.prepend || [];
let moreAppendProxies = moreProxiesObj?.append || [];
let moreDeleteProxies =
moreProxiesObj?.delete || ([] as string[] | { name: string }[]);
let proxies = morePrependProxies.concat(
originProxies.filter((proxy: any) => {
if (proxy.name) {
return !moreDeleteProxies.includes(proxy.name);
} else {
return !moreDeleteProxies.includes(proxy);
}
}),
moreAppendProxies
);
setProxyPolicyList(
builtinProxyPolicies.concat(
prependSeq.map((group: IProxyGroupConfig) => group.name),
originGroupsObj?.["proxy-groups"]
.map((group: IProxyGroupConfig) => group.name)
.filter((name) => !deleteSeq.includes(name)) || [],
appendSeq.map((group: IProxyGroupConfig) => group.name),
proxies.map((proxy: any) => proxy.name)
)
);
};
const fetchProfile = async () => {
let data = await readProfileFile(profileUid);
let mergeData = await readProfileFile(mergeUid);
let globalMergeData = await readProfileFile("Merge");
let originGroupsObj = yaml.load(data) as {
"proxy-groups": IProxyGroupConfig[];
} | null;
let originProviderObj = yaml.load(data) as { "proxy-providers": {} } | null;
let originProvider = originProviderObj?.["proxy-providers"] || {};
let moreProviderObj = yaml.load(mergeData) as {
"proxy-providers": {};
} | null;
let moreProvider = moreProviderObj?.["proxy-providers"] || {};
let globalProviderObj = yaml.load(globalMergeData) as {
"proxy-providers": {};
} | null;
let globalProvider = globalProviderObj?.["proxy-providers"] || {};
let provider = Object.assign(
{},
originProvider,
moreProvider,
globalProvider
);
setProxyProviderList(Object.keys(provider));
setGroupList(originGroupsObj?.["proxy-groups"] || []);
};
const getInterfaceNameList = async () => {
let list = await getNetworkInterfaces();
setInterfaceNameList(list);
};
useEffect(() => {
fetchProxyPolicy();
}, [prependSeq, appendSeq, deleteSeq]);
useEffect(() => {
if (!open) return;
fetchContent();
fetchProxyPolicy();
fetchProfile();
getInterfaceNameList();
}, [open]);
const validateGroup = () => {
let group = formIns.getValues();
if (group.name === "") {
throw new Error(t("Group Name Required"));
}
};
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Groups")}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
</Button>
</Box>
</Box>
}
</DialogTitle>
<DialogContent
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
>
{visualization ? (
<>
<List
sx={{
width: "50%",
padding: "0 10px",
}}
>
<Box
sx={{
height: "calc(100% - 80px)",
overflowY: "auto",
}}
>
<Controller
name="type"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Group Type")} />
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
options={[
"select",
"url-test",
"fallback",
"load-balance",
"relay",
]}
value={field.value}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="name"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Group Name")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
error={field.value === ""}
required={true}
/>
</Item>
)}
/>
<Controller
name="icon"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Proxy Group Icon")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="proxies"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Use Proxies")} />
<Autocomplete
size="small"
sx={{
width: "calc(100% - 150px)",
}}
multiple
options={proxyPolicyList}
disableCloseOnSelect
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
/>
</Item>
)}
/>
<Controller
name="use"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Use Provider")} />
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
multiple
options={proxyProviderList}
disableCloseOnSelect
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="url"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Health Check Url")} />
<TextField
autoComplete="new-password"
placeholder="https://www.gstatic.com/generate_204"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="expected-status"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Expected Status")} />
<TextField
autoComplete="new-password"
placeholder="*"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller
name="interval"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Interval")} />
<TextField
autoComplete="new-password"
placeholder="300"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{t("seconds")}
</InputAdornment>
),
}}
/>
</Item>
)}
/>
<Controller
name="timeout"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Timeout")} />
<TextField
autoComplete="new-password"
placeholder="5000"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
InputProps={{
endAdornment: (
<InputAdornment position="end">
{t("millis")}
</InputAdornment>
),
}}
/>
</Item>
)}
/>
<Controller
name="max-failed-times"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Max Failed Times")} />
<TextField
autoComplete="new-password"
placeholder="5"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller
name="interface-name"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Interface Name")} />
<Autocomplete
size="small"
sx={{ width: "calc(100% - 150px)" }}
options={interfaceNameList}
value={field.value}
onChange={(_, value) => value && field.onChange(value)}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="routing-mark"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Routing Mark")} />
<TextField
autoComplete="new-password"
type="number"
size="small"
sx={{ width: "calc(100% - 150px)" }}
onChange={(e) => {
field.onChange(parseInt(e.target.value));
}}
/>
</Item>
)}
/>
<Controller
name="filter"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Filter")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="exclude-filter"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Exclude Filter")} />
<TextField
autoComplete="new-password"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
/>
</Item>
)}
/>
<Controller
name="exclude-type"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Exclude Type")} />
<Autocomplete
multiple
options={[
"Direct",
"Reject",
"RejectDrop",
"Compatible",
"Pass",
"Dns",
"Shadowsocks",
"ShadowsocksR",
"Snell",
"Socks5",
"Http",
"Vmess",
"Vless",
"Trojan",
"Hysteria",
"Hysteria2",
"WireGuard",
"Tuic",
"Relay",
"Selector",
"Fallback",
"URLTest",
"LoadBalance",
"Ssh",
]}
size="small"
disableCloseOnSelect
sx={{ width: "calc(100% - 150px)" }}
value={field.value?.split("|")}
onChange={(_, value) => {
field.onChange(value.join("|"));
}}
renderInput={(params) => <TextField {...params} />}
/>
</Item>
)}
/>
<Controller
name="include-all"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All")} />
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="include-all-proxies"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All Proxies")} />
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="include-all-providers"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Include All Providers")} />
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="lazy"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Lazy")} />
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="disable-udp"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Disable UDP")} />
<Switch checked={field.value} {...field} />
</Item>
)}
/>
<Controller
name="hidden"
control={control}
render={({ field }) => (
<Item>
<ListItemText primary={t("Hidden")} />
<Switch checked={field.value} {...field} />
</Item>
)}
/>
</Box>
<Item>
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => {
try {
validateGroup();
for (const item of [...prependSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists"));
}
}
setPrependSeq([...prependSeq, formIns.getValues()]);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
}}
>
{t("Prepend Group")}
</Button>
</Item>
<Item>
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => {
try {
validateGroup();
for (const item of [...appendSeq, ...groupList]) {
if (item.name === formIns.getValues().name) {
throw new Error(t("Group Name Already Exists"));
}
}
setAppendSeq([...appendSeq, formIns.getValues()]);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
}}
>
{t("Append Group")}
</Button>
</Item>
</List>
<List
sx={{
width: "50%",
padding: "0 10px",
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={
filteredGroupList.length +
(filteredPrependSeq.length > 0 ? 1 : 0) +
(filteredAppendSeq.length > 0 ? 1 : 0)
}
increaseViewportBy={256}
itemContent={(index) => {
let shift = filteredPrependSeq.length > 0 ? 1 : 0;
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onPrependDragEnd}
>
<SortableContext
items={filteredPrependSeq.map((x) => {
return x.name;
})}
>
{filteredPrependSeq.map((item, index) => {
return (
<GroupItem
key={`${item.name}-${index}`}
type="prepend"
group={item}
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name
)
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
} else if (index < filteredGroupList.length + shift) {
let newIndex = index - shift;
return (
<GroupItem
key={`${filteredGroupList[newIndex].name}-${index}`}
type={
deleteSeq.includes(filteredGroupList[newIndex].name)
? "delete"
: "original"
}
group={filteredGroupList[newIndex]}
onDelete={() => {
if (
deleteSeq.includes(filteredGroupList[newIndex].name)
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredGroupList[newIndex].name
)
);
} else {
setDeleteSeq((prev) => [
...prev,
filteredGroupList[newIndex].name,
]);
}
}}
/>
);
} else {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onAppendDragEnd}
>
<SortableContext
items={filteredAppendSeq.map((x) => {
return x.name;
})}
>
{filteredAppendSeq.map((item, index) => {
return (
<GroupItem
key={`${item.name}-${index}`}
type="append"
group={item}
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name
)
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
}
}}
/>
</List>
</>
) : (
<MonacoEditor
height="100%"
language="yaml"
value={currData}
theme={themeMode === "light" ? "vs" : "vs-dark"}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {
enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
quickSuggestions: {
strings: true, // 字符串类型的建议
comments: true, // 注释类型的建议
other: true, // 其他类型的建议
},
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: true, // 连字符
smoothScrolling: true, // 平滑滚动
}}
onChange={(value) => setCurrData(value)}
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
const Item = styled(ListItem)(() => ({
padding: "5px 2px",
}));
+89 -64
View File
@@ -15,7 +15,7 @@ import {
Menu,
CircularProgress,
} from "@mui/material";
import { RefreshRounded, DragIndicator } from "@mui/icons-material";
import { RefreshRounded, DragIndicatorRounded } from "@mui/icons-material";
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import {
viewProfile,
@@ -24,12 +24,14 @@ import {
saveProfileFile,
} from "@/services/cmds";
import { Notice } from "@/components/base";
import { GroupsEditorViewer } from "@/components/profile/groups-editor-viewer";
import { RulesEditorViewer } from "@/components/profile/rules-editor-viewer";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { ProfileBox } from "./profile-box";
import parseTraffic from "@/utils/parse-traffic";
import { ConfirmViewer } from "@/components/profile/confirm-viewer";
import { open } from "@tauri-apps/api/shell";
import { ProxiesEditorViewer } from "./proxies-editor-viewer";
const round = keyframes`
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
@@ -49,8 +51,14 @@ interface Props {
export const ProfileItem = (props: Props) => {
const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } =
props;
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.id });
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -71,7 +79,10 @@ export const ProfileItem = (props: Props) => {
const from = parseUrl(itemData.url);
const description = itemData.desc;
const expire = parseExpire(extra?.expire);
const progress = Math.round(((download + upload) * 100) / (total + 0.01) + 1);
const progress = Math.min(
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
);
const loading = loadingCache[itemData.uid] ?? false;
@@ -295,8 +306,10 @@ export const ProfileItem = (props: Props) => {
return (
<Box
sx={{
position: "relative",
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
}}
>
<ProfileBox
@@ -335,7 +348,7 @@ export const ProfileItem = (props: Props) => {
{...attributes}
{...listeners}
>
<DragIndicator
<DragIndicatorRounded
sx={[
{ cursor: "move", marginLeft: "-6px" },
({ palette: { text } }) => {
@@ -470,66 +483,78 @@ export const ProfileItem = (props: Props) => {
</MenuItem>
))}
</Menu>
{fileOpen && (
<EditorViewer
open={true}
initialData={readProfileFile(uid)}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(uid, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
)}
{rulesOpen && (
<RulesEditorViewer
groupsUid={option?.groups ?? ""}
mergeUid={option?.merge ?? ""}
profileUid={uid}
property={option?.rules ?? ""}
open={true}
onSave={onSave}
onClose={() => setRulesOpen(false)}
/>
)}
{proxiesOpen && (
<ProxiesEditorViewer
profileUid={uid}
property={option?.proxies ?? ""}
open={true}
onSave={onSave}
onClose={() => setProxiesOpen(false)}
/>
)}
{groupsOpen && (
<GroupsEditorViewer
mergeUid={option?.merge ?? ""}
proxiesUid={option?.proxies ?? ""}
profileUid={uid}
property={option?.groups ?? ""}
open={true}
onSave={onSave}
onClose={() => {
setGroupsOpen(false);
}}
/>
)}
{mergeOpen && (
<EditorViewer
open={true}
initialData={readProfileFile(option?.merge ?? "")}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setMergeOpen(false)}
/>
)}
{scriptOpen && (
<EditorViewer
open={true}
initialData={readProfileFile(option?.script ?? "")}
language="javascript"
onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setScriptOpen(false)}
/>
)}
<EditorViewer
open={fileOpen}
initialData={readProfileFile(uid)}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(uid, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
<RulesEditorViewer
profileUid={uid}
property={option?.rules ?? ""}
open={rulesOpen}
onSave={onSave}
onClose={() => setRulesOpen(false)}
/>
<EditorViewer
open={proxiesOpen}
initialData={readProfileFile(option?.proxies ?? "")}
language="yaml"
onSave={async (prev, curr) => {
await saveProfileFile(option?.proxies ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setProxiesOpen(false)}
/>
<EditorViewer
open={groupsOpen}
initialData={readProfileFile(option?.groups ?? "")}
language="yaml"
onSave={async (prev, curr) => {
await saveProfileFile(option?.groups ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setGroupsOpen(false)}
/>
<EditorViewer
open={mergeOpen}
initialData={readProfileFile(option?.merge ?? "")}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setMergeOpen(false)}
/>
<EditorViewer
open={scriptOpen}
initialData={readProfileFile(option?.script ?? "")}
language="javascript"
onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setScriptOpen(false)}
/>
<ConfirmViewer
title={t("Confirm deletion")}
message={t("This operation is not reversible")}
+21 -19
View File
@@ -167,25 +167,27 @@ export const ProfileMore = (props: Props) => {
</MenuItem>
))}
</Menu>
<EditorViewer
open={fileOpen}
title={`${t("Global " + id)}`}
initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined}
onSave={async (prev, curr) => {
await saveProfileFile(id, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
<LogViewer
open={logOpen}
logInfo={logInfo}
onClose={() => setLogOpen(false)}
/>
{fileOpen && (
<EditorViewer
open={true}
title={`${t("Global " + id)}`}
initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined}
onSave={async (prev, curr) => {
await saveProfileFile(id, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
)}
{logOpen && (
<LogViewer
open={logOpen}
logInfo={logInfo}
onClose={() => setLogOpen(false)}
/>
)}
</>
);
};
@@ -0,0 +1,459 @@
import { ReactNode, useEffect, useMemo, useState } from "react";
import { useLockFn } from "ahooks";
import yaml from "js-yaml";
import { useTranslation } from "react-i18next";
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
TextField,
styled,
} from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { ProxyItem } from "@/components/profile/proxy-item";
import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice } from "@/components/base";
import getSystem from "@/utils/get-system";
import { BaseSearchBox } from "../base/base-search-box";
import { Virtuoso } from "react-virtuoso";
import MonacoEditor from "react-monaco-editor";
import { useThemeMode } from "@/services/states";
import parseUri from "@/utils/uri-parser";
interface Props {
profileUid: string;
property: string;
open: boolean;
onClose: () => void;
onSave?: (prev?: string, curr?: string) => void;
}
export const ProxiesEditorViewer = (props: Props) => {
const { profileUid, property, open, onClose, onSave } = props;
const { t } = useTranslation();
const themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
const [visualization, setVisualization] = useState(true);
const [match, setMatch] = useState(() => (_: string) => true);
const [proxyUri, setProxyUri] = useState<string>("");
const [proxyList, setProxyList] = useState<IProxyConfig[]>([]);
const [prependSeq, setPrependSeq] = useState<IProxyConfig[]>([]);
const [appendSeq, setAppendSeq] = useState<IProxyConfig[]>([]);
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
const filteredPrependSeq = useMemo(
() => prependSeq.filter((proxy) => match(proxy.name)),
[prependSeq, match]
);
const filteredProxyList = useMemo(
() => proxyList.filter((proxy) => match(proxy.name)),
[proxyList, match]
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((proxy) => match(proxy.name)),
[appendSeq, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const reorder = (
list: IProxyConfig[],
startIndex: number,
endIndex: number
) => {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
};
const onPrependDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = 0;
let overIndex = 0;
prependSeq.forEach((item, index) => {
if (item.name === active.id) {
activeIndex = index;
}
if (item.name === over.id) {
overIndex = index;
}
});
setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
}
}
};
const onAppendDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = 0;
let overIndex = 0;
appendSeq.forEach((item, index) => {
if (item.name === active.id) {
activeIndex = index;
}
if (item.name === over.id) {
overIndex = index;
}
});
setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
}
}
};
const handleParse = () => {
let proxies = [] as IProxyConfig[];
let names: string[] = [];
let uris = "";
try {
uris = atob(proxyUri);
} catch {
uris = proxyUri;
}
uris
.trim()
.split("\n")
.forEach((uri) => {
try {
let proxy = parseUri(uri.trim());
if (!names.includes(proxy.name)) {
proxies.push(proxy);
names.push(proxy.name);
}
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return proxies;
};
const fetchProfile = async () => {
let data = await readProfileFile(profileUid);
let originProxiesObj = yaml.load(data) as {
proxies: IProxyConfig[];
} | null;
setProxyList(originProxiesObj?.proxies || []);
};
const fetchContent = async () => {
let data = await readProfileFile(property);
let obj = yaml.load(data) as ISeqProfileConfig | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
setPrevData(data);
setCurrData(data);
};
useEffect(() => {
if (currData === "") return;
if (visualization !== true) return;
let obj = yaml.load(currData) as {
prepend: [];
append: [];
delete: [];
} | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
}, [visualization]);
useEffect(() => {
if (prependSeq && appendSeq && deleteSeq)
setCurrData(
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{
forceQuotes: true,
}
)
);
}, [prependSeq, appendSeq, deleteSeq]);
useEffect(() => {
if (!open) return;
fetchContent();
fetchProfile();
}, [open]);
const handleSave = useLockFn(async () => {
try {
await saveProfileFile(property, currData);
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Proxies")}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
setVisualization((prev) => !prev);
}}
>
{visualization ? t("Advanced") : t("Visualization")}
</Button>
</Box>
</Box>
}
</DialogTitle>
<DialogContent
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
>
{visualization ? (
<>
<List
sx={{
width: "50%",
padding: "0 10px",
}}
>
<Box
sx={{
height: "calc(100% - 80px)",
overflowY: "auto",
}}
>
<Item>
<TextField
autoComplete="new-password"
placeholder={t("Use newlines for multiple uri")}
fullWidth
rows={9}
multiline
size="small"
onChange={(e) => setProxyUri(e.target.value)}
/>
</Item>
</Box>
<Item>
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => {
let proxies = handleParse();
setPrependSeq([...prependSeq, ...proxies]);
}}
>
{t("Prepend Proxy")}
</Button>
</Item>
<Item>
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => {
let proxies = handleParse();
setAppendSeq([...appendSeq, ...proxies]);
}}
>
{t("Append Proxy")}
</Button>
</Item>
</List>
<List
sx={{
width: "50%",
padding: "0 10px",
}}
>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={
filteredProxyList.length +
(filteredPrependSeq.length > 0 ? 1 : 0) +
(filteredAppendSeq.length > 0 ? 1 : 0)
}
increaseViewportBy={256}
itemContent={(index) => {
let shift = filteredPrependSeq.length > 0 ? 1 : 0;
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onPrependDragEnd}
>
<SortableContext
items={filteredPrependSeq.map((x) => {
return x.name;
})}
>
{filteredPrependSeq.map((item, index) => {
return (
<ProxyItem
key={`${item.name}-${index}`}
type="prepend"
proxy={item}
onDelete={() => {
setPrependSeq(
prependSeq.filter(
(v) => v.name !== item.name
)
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
} else if (index < filteredProxyList.length + shift) {
let newIndex = index - shift;
return (
<ProxyItem
key={`${filteredProxyList[newIndex].name}-${index}`}
type={
deleteSeq.includes(filteredProxyList[newIndex].name)
? "delete"
: "original"
}
proxy={filteredProxyList[newIndex]}
onDelete={() => {
if (
deleteSeq.includes(filteredProxyList[newIndex].name)
) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredProxyList[newIndex].name
)
);
} else {
setDeleteSeq((prev) => [
...prev,
filteredProxyList[newIndex].name,
]);
}
}}
/>
);
} else {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onAppendDragEnd}
>
<SortableContext
items={filteredAppendSeq.map((x) => {
return x.name;
})}
>
{filteredAppendSeq.map((item, index) => {
return (
<ProxyItem
key={`${item.name}-${index}`}
type="append"
proxy={item}
onDelete={() => {
setAppendSeq(
appendSeq.filter(
(v) => v.name !== item.name
)
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
}
}}
/>
</List>
</>
) : (
<MonacoEditor
height="100%"
language="yaml"
value={currData}
theme={themeMode === "light" ? "vs" : "vs-dark"}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {
enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
quickSuggestions: {
strings: true, // 字符串类型的建议
comments: true, // 注释类型的建议
other: true, // 其他类型的建议
},
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: true, // 连字符
smoothScrolling: true, // 平滑滚动
}}
onChange={(value) => setCurrData(value)}
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
const Item = styled(ListItem)(() => ({
padding: "5px 2px",
}));
+127
View File
@@ -0,0 +1,127 @@
import {
Box,
IconButton,
ListItem,
ListItemText,
alpha,
styled,
} from "@mui/material";
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
interface Props {
type: "prepend" | "original" | "delete" | "append";
proxy: IProxyConfig;
onDelete: () => void;
}
export const ProxyItem = (props: Props) => {
let { type, proxy, onDelete } = props;
const sortable = type === "prepend" || type === "append";
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = sortable
? useSortable({ id: proxy.name })
: {
attributes: {},
listeners: {},
setNodeRef: null,
transform: null,
transition: null,
isDragging: false,
};
return (
<ListItem
dense
sx={({ palette }) => ({
position: "relative",
background:
type === "original"
? palette.mode === "dark"
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})}
>
<ListItemText
{...attributes}
{...listeners}
ref={setNodeRef}
sx={{ cursor: sortable ? "move" : "" }}
primary={
<StyledPrimary
title={proxy.name}
sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
>
{proxy.name}
</StyledPrimary>
}
secondary={
<ListItemTextChild
sx={{
overflow: "hidden",
display: "flex",
alignItems: "center",
pt: "2px",
}}
>
<Box sx={{ marginTop: "2px" }}>
<StyledTypeBox>{proxy.type}</StyledTypeBox>
</Box>
</ListItemTextChild>
}
secondaryTypographyProps={{
sx: {
display: "flex",
alignItems: "center",
color: "#ccc",
},
}}
/>
<IconButton onClick={onDelete}>
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
</IconButton>
</ListItem>
);
};
const StyledPrimary = styled("div")`
font-size: 15px;
font-weight: 700;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
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),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
padding: "0 4px",
lineHeight: 1.5,
marginRight: "8px",
}));
+92 -33
View File
@@ -1,11 +1,10 @@
import {
Box,
Divider,
IconButton,
ListItem,
ListItemText,
Typography,
alpha,
styled,
} from "@mui/material";
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
import { useSortable } from "@dnd-kit/sortable";
@@ -19,8 +18,20 @@ interface Props {
export const RuleItem = (props: Props) => {
let { type, ruleRaw, onDelete } = props;
const sortable = type === "prepend" || type === "append";
const rule = ruleRaw.replace(",no-resolve", "").split(",");
const { attributes, listeners, setNodeRef, transform, transition } = sortable
const rule = ruleRaw.replace(",no-resolve", "");
const ruleType = rule.match(/^[^,]+/)?.[0] ?? "";
const proxyPolicy = rule.match(/[^,]+$/)?.[0] ?? "";
const ruleContent = rule.slice(ruleType.length + 1, -proxyPolicy.length - 1);
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = sortable
? useSortable({ id: ruleRaw })
: {
attributes: {},
@@ -28,56 +39,104 @@ export const RuleItem = (props: Props) => {
setNodeRef: null,
transform: null,
transition: null,
isDragging: false,
};
return (
<ListItem
dense
sx={({ palette }) => ({
p: 0,
borderRadius: "10px",
border: "solid 2px",
borderColor:
position: "relative",
background:
type === "original"
? "var(--divider-color)"
? palette.mode === "dark"
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete"
? alpha(palette.error.main, 0.5)
: alpha(palette.success.main, 0.5),
mb: 1,
? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.3),
height: "100%",
margin: "8px 0",
borderRadius: "8px",
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})}
>
<ListItemText
{...attributes}
{...listeners}
ref={setNodeRef}
sx={{ px: 1 }}
sx={{ cursor: sortable ? "move" : "" }}
primary={
<>
<Typography
sx={{
textDecoration: type === "delete" ? "line-through" : "",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
variant="h6"
title={rule.length === 3 ? rule[1] : "-"}
>
{rule.length === 3 ? rule[1] : "-"}
</Typography>
</>
<StyledPrimary
title={ruleContent || "-"}
sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
>
{ruleContent || "-"}
</StyledPrimary>
}
secondary={
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Box>{rule[0]}</Box>
<Box>{rule.length === 3 ? rule[2] : rule[1]}</Box>
</Box>
<ListItemTextChild
sx={{
width: "62%",
overflow: "hidden",
display: "flex",
justifyContent: "space-between",
pt: "2px",
}}
>
<Box sx={{ marginTop: "2px" }}>
<StyledTypeBox>{ruleType}</StyledTypeBox>
</Box>
<StyledSubtitle sx={{ color: "text.secondary" }}>
{proxyPolicy}
</StyledSubtitle>
</ListItemTextChild>
}
secondaryTypographyProps={{
sx: {
display: "flex",
alignItems: "center",
color: "#ccc",
},
}}
/>
<Divider orientation="vertical" flexItem />
<IconButton size="small" color="inherit" onClick={onDelete}>
<IconButton onClick={onDelete}>
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
</IconButton>
</ListItem>
);
};
const StyledPrimary = styled("div")`
font-size: 15px;
font-weight: 700;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledSubtitle = styled("span")`
font-size: 13px;
overflow: hidden;
color: text.secondary;
text-overflow: ellipsis;
white-space: nowrap;
`;
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),
color: alpha(theme.palette.primary.main, 0.8),
borderRadius: 4,
fontSize: 10,
padding: "0 4px",
lineHeight: 1.5,
marginRight: "8px",
}));
+103 -53
View File
@@ -29,7 +29,10 @@ import {
TextField,
styled,
} from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice, Switch } from "@/components/base";
import getSystem from "@/utils/get-system";
@@ -40,8 +43,9 @@ import MonacoEditor from "react-monaco-editor";
import { useThemeMode } from "@/services/states";
interface Props {
groupsUid: string;
mergeUid: string;
profileUid: string;
title?: string | ReactNode;
property: string;
open: boolean;
onClose: () => void;
@@ -230,13 +234,14 @@ const rules: {
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
export const RulesEditorViewer = (props: Props) => {
const { title, profileUid, property, open, onClose, onSave } = props;
const { groupsUid, mergeUid, profileUid, property, open, onClose, onSave } =
props;
const { t } = useTranslation();
const themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
const [visible, setVisible] = useState(true);
const [visualization, setVisualization] = useState(true);
const [match, setMatch] = useState(() => (_: string) => true);
const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]);
@@ -252,10 +257,18 @@ export const RulesEditorViewer = (props: Props) => {
const [appendSeq, setAppendSeq] = useState<string[]>([]);
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)),
[prependSeq, match]
);
const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)),
[ruleList, match]
);
const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)),
[appendSeq, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
@@ -291,11 +304,11 @@ export const RulesEditorViewer = (props: Props) => {
};
const fetchContent = async () => {
let data = await readProfileFile(property);
let obj = yaml.load(data) as { prepend: []; append: []; delete: [] };
let obj = yaml.load(data) as ISeqProfileConfig | null;
setPrependSeq(obj.prepend || []);
setAppendSeq(obj.append || []);
setDeleteSeq(obj.delete || []);
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
setPrevData(data);
setCurrData(data);
@@ -303,46 +316,83 @@ export const RulesEditorViewer = (props: Props) => {
useEffect(() => {
if (currData === "") return;
if (visible !== true) return;
if (visualization !== true) return;
let obj = yaml.load(currData) as { prepend: []; append: []; delete: [] };
setPrependSeq(obj.prepend || []);
setAppendSeq(obj.append || []);
setDeleteSeq(obj.delete || []);
}, [visible]);
let obj = yaml.load(currData) as ISeqProfileConfig | null;
setPrependSeq(obj?.prepend || []);
setAppendSeq(obj?.append || []);
setDeleteSeq(obj?.delete || []);
}, [visualization]);
useEffect(() => {
if (prependSeq && appendSeq && deleteSeq)
setCurrData(
yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq })
yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{
forceQuotes: true,
}
)
);
}, [prependSeq, appendSeq, deleteSeq]);
const fetchProfile = async () => {
let data = await readProfileFile(profileUid);
let groupsObj = yaml.load(data) as { "proxy-groups": [] };
let rulesObj = yaml.load(data) as { rules: [] };
let ruleSetObj = yaml.load(data) as { "rule-providers": [] };
let subRuleObj = yaml.load(data) as { "sub-rules": [] };
let data = await readProfileFile(profileUid); // 原配置文件
let groupsData = await readProfileFile(groupsUid); // groups配置文件
let mergeData = await readProfileFile(mergeUid); // merge配置文件
let globalMergeData = await readProfileFile("Merge"); // global merge配置文件
let rulesObj = yaml.load(data) as { rules: [] } | null;
let originGroupsObj = yaml.load(data) as { "proxy-groups": [] } | null;
let originGroups = originGroupsObj?.["proxy-groups"] || [];
let moreGroupsObj = yaml.load(groupsData) as ISeqProfileConfig | null;
let morePrependGroups = moreGroupsObj?.["prepend"] || [];
let moreAppendGroups = moreGroupsObj?.["append"] || [];
let moreDeleteGroups =
moreGroupsObj?.["delete"] || ([] as string[] | { name: string }[]);
let groups = morePrependGroups.concat(
originGroups.filter((group: any) => {
if (group.name) {
return !moreDeleteGroups.includes(group.name);
} else {
return !moreDeleteGroups.includes(group);
}
}),
moreAppendGroups
);
let originRuleSetObj = yaml.load(data) as { "rule-providers": {} } | null;
let originRuleSet = originRuleSetObj?.["rule-providers"] || {};
let moreRuleSetObj = yaml.load(mergeData) as {
"rule-providers": {};
} | null;
let moreRuleSet = moreRuleSetObj?.["rule-providers"] || {};
let globalRuleSetObj = yaml.load(globalMergeData) as {
"rule-providers": {};
} | null;
let globalRuleSet = globalRuleSetObj?.["rule-providers"] || {};
let ruleSet = Object.assign({}, originRuleSet, moreRuleSet, globalRuleSet);
let originSubRuleObj = yaml.load(data) as { "sub-rules": {} } | null;
let originSubRule = originSubRuleObj?.["sub-rules"] || {};
let moreSubRuleObj = yaml.load(mergeData) as { "sub-rules": {} } | null;
let moreSubRule = moreSubRuleObj?.["sub-rules"] || {};
let globalSubRuleObj = yaml.load(globalMergeData) as {
"sub-rules": {};
} | null;
let globalSubRule = globalSubRuleObj?.["sub-rules"] || {};
let subRule = Object.assign({}, originSubRule, moreSubRule, globalSubRule);
setProxyPolicyList(
builtinProxyPolicies.concat(
groupsObj["proxy-groups"]
? groupsObj["proxy-groups"].map((item: any) => item.name)
: []
)
);
setRuleList(rulesObj.rules || []);
setRuleSetList(
ruleSetObj["rule-providers"]
? Object.keys(ruleSetObj["rule-providers"])
: []
);
setSubRuleList(
subRuleObj["sub-rules"] ? Object.keys(subRuleObj["sub-rules"]) : []
builtinProxyPolicies.concat(groups.map((group: any) => group.name))
);
setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule));
setRuleList(rulesObj?.rules || []);
};
useEffect(() => {
if (!open) return;
fetchContent();
fetchProfile();
}, [open]);
@@ -355,9 +405,10 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule"));
}
return `${ruleType.name}${
ruleContent ? "," + ruleContent : ""
},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""}`;
const condition = ruleType.required ?? true ? ruleContent : "";
return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
};
const handleSave = useLockFn(async () => {
@@ -381,10 +432,10 @@ export const RulesEditorViewer = (props: Props) => {
variant="contained"
size="small"
onClick={() => {
setVisible((prev) => !prev);
setVisualization((prev) => !prev);
}}
>
{visible ? t("Advanced") : t("Visible")}
{visualization ? t("Advanced") : t("Visualization")}
</Button>
</Box>
</Box>
@@ -394,7 +445,7 @@ export const RulesEditorViewer = (props: Props) => {
<DialogContent
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
>
{visible ? (
{visualization ? (
<>
<List
sx={{
@@ -447,7 +498,7 @@ export const RulesEditorViewer = (props: Props) => {
{ruleType.name !== "RULE-SET" &&
ruleType.name !== "SUB-RULE" && (
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ minWidth: "240px" }}
value={ruleContent}
@@ -487,6 +538,7 @@ export const RulesEditorViewer = (props: Props) => {
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => {
try {
let raw = validateRule();
@@ -504,6 +556,7 @@ export const RulesEditorViewer = (props: Props) => {
<Button
fullWidth
variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => {
try {
let raw = validateRule();
@@ -525,21 +578,18 @@ export const RulesEditorViewer = (props: Props) => {
padding: "0 10px",
}}
>
<BaseSearchBox
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={
filteredRuleList.length +
(prependSeq.length > 0 ? 1 : 0) +
(appendSeq.length > 0 ? 1 : 0)
(filteredPrependSeq.length > 0 ? 1 : 0) +
(filteredAppendSeq.length > 0 ? 1 : 0)
}
increaseViewportBy={256}
itemContent={(index) => {
let shift = prependSeq.length > 0 ? 1 : 0;
if (prependSeq.length > 0 && index === 0) {
let shift = filteredPrependSeq.length > 0 ? 1 : 0;
if (filteredPrependSeq.length > 0 && index === 0) {
return (
<DndContext
sensors={sensors}
@@ -547,11 +597,11 @@ export const RulesEditorViewer = (props: Props) => {
onDragEnd={onPrependDragEnd}
>
<SortableContext
items={prependSeq.map((x) => {
items={filteredPrependSeq.map((x) => {
return x;
})}
>
{prependSeq.map((item, index) => {
{filteredPrependSeq.map((item, index) => {
return (
<RuleItem
key={`${item}-${index}`}
@@ -603,11 +653,11 @@ export const RulesEditorViewer = (props: Props) => {
onDragEnd={onAppendDragEnd}
>
<SortableContext
items={appendSeq.map((x) => {
items={filteredAppendSeq.map((x) => {
return x;
})}
>
{appendSeq.map((item, index) => {
{filteredAppendSeq.map((item, index) => {
return (
<RuleItem
key={`${item}-${index}`}
+4 -2
View File
@@ -105,8 +105,9 @@ export const ProviderButton = () => {
const download = sub?.Download || 0;
const total = sub?.Total || 0;
const expire = sub?.Expire || 0;
const progress = Math.round(
((download + upload) * 100) / (total + 0.1)
const progress = Math.min(
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
);
return (
<>
@@ -159,6 +160,7 @@ export const ProviderButton = () => {
<LinearProgress
variant="determinate"
value={progress}
style={{ opacity: total > 0 ? 1 : 0 }}
/>
</>
)}
+2 -2
View File
@@ -132,7 +132,7 @@ export const ProxyHead = (props: Props) => {
{textState === "filter" && (
<TextField
autoComplete="off"
autoComplete="new-password"
autoFocus={autoFocus}
hiddenLabel
value={filterText}
@@ -146,7 +146,7 @@ export const ProxyHead = (props: Props) => {
{textState === "url" && (
<TextField
autoComplete="off"
autoComplete="new-password"
autoFocus={autoFocus}
hiddenLabel
autoSave="off"
@@ -5,7 +5,10 @@ import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { useLockFn } from "ahooks";
import { LoadingButton } from "@mui/lab";
import { SwitchAccessShortcut, RestartAlt } from "@mui/icons-material";
import {
SwitchAccessShortcutRounded,
RestartAltRounded,
} from "@mui/icons-material";
import {
Box,
Button,
@@ -85,7 +88,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<LoadingButton
variant="contained"
size="small"
startIcon={<SwitchAccessShortcut />}
startIcon={<SwitchAccessShortcutRounded />}
loadingPosition="start"
loading={upgrading}
sx={{ marginRight: "8px" }}
@@ -97,7 +100,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
variant="contained"
size="small"
onClick={onRestart}
startIcon={<RestartAlt />}
startIcon={<RestartAltRounded />}
>
{t("Restart")}
</Button>
@@ -13,19 +13,18 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false);
const [redirPort, setRedirPort] = useState(
verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895
);
const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? true
verge?.verge_redir_enabled ?? false
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? clashInfo?.tproxy_port ?? 7896
);
const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? true
verge?.verge_tproxy_enabled ?? false
);
const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
@@ -34,26 +33,26 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
verge?.verge_socks_port ?? clashInfo?.socks_port ?? 7898
);
const [socksEnabled, setSocksEnabled] = useState(
verge?.verge_socks_enabled ?? true
verge?.verge_socks_enabled ?? false
);
const [port, setPort] = useState(
verge?.verge_port ?? clashInfo?.port ?? 7899
);
const [httpEnabled, setHttpEnabled] = useState(
verge?.verge_http_enabled ?? true
verge?.verge_http_enabled ?? false
);
useImperativeHandle(ref, () => ({
open: () => {
if (verge?.verge_redir_port) setRedirPort(verge?.verge_redir_port);
setRedirEnabled(verge?.verge_redir_enabled ?? true);
setRedirEnabled(verge?.verge_redir_enabled ?? false);
if (verge?.verge_tproxy_port) setTproxyPort(verge?.verge_tproxy_port);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? true);
setTproxyEnabled(verge?.verge_tproxy_enabled ?? false);
if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port);
if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port);
setSocksEnabled(verge?.verge_socks_enabled ?? true);
setSocksEnabled(verge?.verge_socks_enabled ?? false);
if (verge?.verge_port) setPort(verge?.verge_port);
setHttpEnabled(verge?.verge_http_enabled ?? true);
setHttpEnabled(verge?.verge_http_enabled ?? false);
setOpen(true);
},
close: () => setOpen(false),
@@ -94,24 +93,57 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
return;
}
try {
if (OS !== "windows") {
await patchInfo({ "redir-port": redirPort });
await patchVerge({ verge_redir_port: redirPort });
await patchVerge({ verge_redir_enabled: redirEnabled });
if (OS === "windows") {
await patchInfo({
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
}
if (OS === "macos") {
await patchInfo({
"redir-port": redirPort,
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
}
if (OS === "linux") {
await patchInfo({ "tproxy-port": tproxyPort });
await patchVerge({ verge_tproxy_port: tproxyPort });
await patchVerge({ verge_tproxy_enabled: tproxyEnabled });
await patchInfo({
"redir-port": redirPort,
"tproxy-port": tproxyPort,
"mixed-port": mixedPort,
"socks-port": socksPort,
port,
});
await patchVerge({
verge_redir_port: redirPort,
verge_redir_enabled: redirEnabled,
verge_tproxy_port: tproxyPort,
verge_tproxy_enabled: tproxyEnabled,
verge_mixed_port: mixedPort,
verge_socks_port: socksPort,
verge_socks_enabled: socksEnabled,
verge_port: port,
verge_http_enabled: httpEnabled,
});
}
await patchInfo({ "mixed-port": mixedPort });
await patchInfo({ "socks-port": socksPort });
await patchInfo({ port });
await patchVerge({ verge_mixed_port: mixedPort });
await patchVerge({ verge_socks_port: socksPort });
await patchVerge({ verge_port: port });
await patchVerge({ verge_socks_enabled: socksEnabled });
await patchVerge({ verge_http_enabled: httpEnabled });
setOpen(false);
Notice.success(t("Clash Port Modified"), 1000);
} catch (err: any) {
@@ -134,7 +166,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Mixed Port")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={mixedPort}
@@ -146,7 +178,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Socks Port")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={socksPort}
@@ -169,7 +201,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Http Port")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={port}
@@ -193,7 +225,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Redir Port")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={redirPort}
@@ -218,7 +250,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Tproxy Port")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 135 }}
value={tproxyPort}
@@ -20,11 +20,12 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
close: () => setOpen(false),
}));
if (!open) return null;
return (
<EditorViewer
open={open}
open={true}
title={
<Box>
<Box display="flex" alignItems="center" gap={2}>
{t("Runtime Config")}
<Chip label={t("ReadOnly")} size="small" />
</Box>
@@ -48,7 +48,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("External Controller")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 175 }}
value={controller}
@@ -60,7 +60,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Core Secret")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
sx={{ width: 175 }}
value={secret}
+2 -2
View File
@@ -198,7 +198,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/>
<TooltipIcon title={t("Default Latency Test Info")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
autoCorrect="off"
autoCapitalize="off"
@@ -215,7 +215,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
type="number"
autoCorrect="off"
@@ -0,0 +1,141 @@
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { getNetworkInterfacesInfo } from "@/services/cmds";
import { alpha, Box, Button, Chip, IconButton } from "@mui/material";
import { ContentCopyRounded } from "@mui/icons-material";
import { writeText } from "@tauri-apps/api/clipboard";
export const NetworkInterfaceViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [networkInterfaces, setNetworkInterfaces] = useState<
INetworkInterface[]
>([]);
const [isV4, setIsV4] = useState(true);
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
},
close: () => setOpen(false),
}));
useEffect(() => {
if (!open) return;
getNetworkInterfacesInfo().then((res) => {
console.log(res);
setNetworkInterfaces(res);
});
}, [open]);
return (
<BaseDialog
open={open}
title={
<Box display="flex" justifyContent="space-between">
{t("Network Interface")}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
setIsV4((prev) => !prev);
}}
>
{isV4 ? "Ipv6" : "Ipv4"}
</Button>
</Box>
</Box>
}
contentSx={{ width: 450, maxHeight: 330 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
{networkInterfaces.map((item) => (
<Box key={item.name}>
<h4>{item.name}</h4>
<Box>
{isV4 && (
<>
{item.addr.map(
(address) =>
address.V4 && (
<AddressDisplay
key={address.V4.ip}
label={t("Ip Address")}
content={address.V4.ip}
/>
)
)}
<AddressDisplay
label={t("Mac Address")}
content={item.mac_addr ?? ""}
/>
</>
)}
{!isV4 && (
<>
{item.addr.map(
(address) =>
address.V6 && (
<AddressDisplay
key={address.V6.ip}
label={t("Ip Address")}
content={address.V6.ip}
/>
)
)}
<AddressDisplay
label={t("Mac Address")}
content={item.mac_addr ?? ""}
/>
</>
)}
</Box>
</Box>
))}
</BaseDialog>
);
});
const AddressDisplay = (props: { label: string; content: string }) => {
const { t } = useTranslation();
return (
<Box
sx={{
display: "flex",
justifyContent: "space-between",
margin: "8px 0",
}}
>
<Box>{props.label}</Box>
<Box
sx={({ palette }) => ({
borderRadius: "8px",
padding: "2px",
background:
palette.mode === "dark"
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3),
})}
>
<Box sx={{ display: "inline", userSelect: "text" }}>
{props.content}
</Box>
<IconButton
size="small"
onClick={async () => {
await writeText(props.content);
Notice.success(t("Copy Success"));
}}
>
<ContentCopyRounded sx={{ fontSize: "18px" }} />
</IconButton>
</Box>
</Box>
);
};
@@ -0,0 +1,97 @@
import { KeyedMutator } from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { installService, uninstallService } from "@/services/cmds";
import { Notice } from "@/components/base";
import { LoadingButton } from "@mui/lab";
interface Props {
status: "active" | "installed" | "unknown" | "uninstall";
mutate: KeyedMutator<"active" | "installed" | "unknown" | "uninstall">;
patchVerge: (value: Partial<IVergeConfig>) => Promise<void>;
onChangeData: (patch: Partial<IVergeConfig>) => void;
}
export const ServiceSwitcher = (props: Props) => {
const { status, mutate, patchVerge, onChangeData } = props;
const isActive = status === "active";
const isInstalled = status === "installed";
const isUninstall = status === "uninstall" || status === "unknown";
const { t } = useTranslation();
const [serviceLoading, setServiceLoading] = useState(false);
const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
const onInstallOrEnableService = useLockFn(async () => {
setServiceLoading(true);
try {
if (isUninstall) {
// install service
await installService();
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Installed Successfully"));
setServiceLoading(false);
} else {
// enable or disable service
await patchVerge({ enable_service_mode: !isActive });
onChangeData({ enable_service_mode: !isActive });
await mutate();
setTimeout(() => {
mutate();
}, 2000);
setServiceLoading(false);
}
} catch (err: any) {
await mutate();
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
});
const onUninstallService = useLockFn(async () => {
setUninstallServiceLoading(true);
try {
await uninstallService();
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Uninstalled Successfully"));
setUninstallServiceLoading(false);
} catch (err: any) {
await mutate();
Notice.error(err.message || err.toString());
setUninstallServiceLoading(false);
}
});
return (
<>
<LoadingButton
size="small"
variant={isUninstall ? "outlined" : "contained"}
onClick={onInstallOrEnableService}
loading={serviceLoading}
>
{isActive ? t("Disable") : isInstalled ? t("Enable") : t("Install")}
</LoadingButton>
{isInstalled && (
<LoadingButton
size="small"
variant="outlined"
color="error"
sx={{ ml: 1 }}
onClick={onUninstallService}
loading={uninstallServiceLoaing}
>
{t("Uninstall")}
</LoadingButton>
)}
</>
);
};
@@ -1,129 +0,0 @@
import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Button, Stack, Typography } from "@mui/material";
import {
checkService,
installService,
uninstallService,
patchVergeConfig,
} from "@/services/cmds";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
interface Props {
enable: boolean;
}
export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
const { enable } = props;
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const { data: status, mutate: mutateCheck } = useSWR(
"checkService",
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}));
const state = status != null ? status : "pending";
const onInstall = useLockFn(async () => {
try {
await installService();
await mutateCheck();
setOpen(false);
setTimeout(() => {
mutateCheck();
}, 2000);
Notice.success(t("Service Installed Successfully"));
} catch (err: any) {
mutateCheck();
Notice.error(err.message || err.toString());
}
});
const onUninstall = useLockFn(async () => {
try {
if (enable) {
await patchVergeConfig({ enable_service_mode: false });
}
await uninstallService();
mutateCheck();
setOpen(false);
Notice.success(t("Service Uninstalled Successfully"));
} catch (err: any) {
mutateCheck();
Notice.error(err.message || err.toString());
}
});
// fix unhandled error of the service mode
const onDisable = useLockFn(async () => {
try {
await patchVergeConfig({ enable_service_mode: false });
mutateCheck();
setOpen(false);
} catch (err: any) {
mutateCheck();
Notice.error(err.message || err.toString());
}
});
return (
<BaseDialog
open={open}
title={t("Service Mode")}
contentSx={{ width: 360, userSelect: "text" }}
disableFooter
onClose={() => setOpen(false)}
>
<Typography>
{t("Current State")}: {t(state)}
</Typography>
{(state === "unknown" || state === "uninstall") && (
<Typography>
{t(
"Information: Please make sure that the Clash Verge Service is installed and enabled"
)}
</Typography>
)}
<Stack
direction="row"
spacing={1}
sx={{ mt: 4, justifyContent: "flex-end" }}
>
{state === "uninstall" && enable && (
<Button variant="contained" onClick={onDisable}>
{t("Disable Service Mode")}
</Button>
)}
{state === "uninstall" && (
<Button variant="contained" onClick={onInstall}>
{t("Install")}
</Button>
)}
{(state === "active" || state === "installed") && (
<Button variant="outlined" onClick={onUninstall}>
{t("Uninstall")}
</Button>
)}
</Stack>
</BaseDialog>
);
});
+52 -28
View File
@@ -1,4 +1,4 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
@@ -14,7 +14,7 @@ import {
import { useVerge } from "@/hooks/use-verge";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { Edit } from "@mui/icons-material";
import { EditRounded } from "@mui/icons-material";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { BaseFieldset } from "@/components/base/base-fieldset";
import getSystem from "@/utils/get-system";
@@ -23,16 +23,38 @@ const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}`;
/** NO_PROXY validation */
// *., cdn*., *, etc.
const domain_subdomain_part = String.raw`(?:[a-z0-9\-\*]+\.|\*)*`;
// .*, .cn, .moe, .co*, *
const domain_tld_part = String.raw`(?:\w{2,64}\*?|\*)`;
// *epicgames*, *skk.moe, *.skk.moe, skk.*, sponsor.cdn.skk.moe, *.*, etc.
// also matches 192.168.*, 10.*, 127.0.0.*, etc. (partial ipv4)
const rDomainSimple = domain_subdomain_part + domain_tld_part;
const ipv4_part = String.raw`\d{1,3}`;
// 127.0.0.1 (full ipv4)
const rIPv4 = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}`;
// const rIPv4Partial = String.raw`${ipv4_part}\.(?:(?:${ipv4_part}|\*)\.){0,2}\.\*`;
const ipv6_part = "(?:[a-fA-F0-9:])+";
const rIPv6 = `(?:${ipv6_part}:+)+${ipv6_part}`;
const rLocal = `localhost|<local>|localdomain`;
const rValidPart = `${rDomainSimple}|${rIPv4}|${rIPv6}|${rLocal}`;
const getValidReg = (isWindows: boolean) => {
const separator = isWindows ? ";" : ",";
const rValid = String.raw`^(${rValidPart})(?:${separator}\s?(${rValidPart}))*${separator}?$`;
return new RegExp(rValid);
};
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
let validReg;
if (getSystem() === "windows") {
validReg =
/^((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\*|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+|localhost|<local>)(;((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\*|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+|localhost|<local>))*;?$/;
} else {
validReg =
/^((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?|localhost|<local>)(,((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?|localhost|<local>))*,?$/;
}
const isWindows = getSystem() === "windows";
const validReg = useMemo(() => getValidReg(isWindows), [isWindows]);
const [open, setOpen] = useState(false);
const [editorOpen, setEditorOpen] = useState(false);
@@ -188,7 +210,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} />
<TextField
autoComplete="off"
autoComplete="new-password"
disabled={!enabled}
size="small"
value={value.duration}
@@ -219,7 +241,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<>
<ListItemText primary={t("Proxy Bypass")} />
<TextField
autoComplete="off"
autoComplete="new-password"
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
size="small"
@@ -234,7 +256,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItemText primary={t("Bypass")} />
<FlexBox>
<TextField
autoComplete="off"
autoComplete="new-password"
disabled={true}
size="small"
multiline
@@ -253,7 +275,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ padding: "3px 0" }}
/>
<Button
startIcon={<Edit />}
startIcon={<EditRounded />}
variant="outlined"
onClick={() => {
setEditorOpen(true);
@@ -261,20 +283,22 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
>
{t("Edit")} PAC
</Button>
<EditorViewer
open={editorOpen}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
onClose={() => setEditorOpen(false)}
/>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
onClose={() => setEditorOpen(false)}
/>
)}
</ListItem>
</>
)}
+17 -15
View File
@@ -14,7 +14,7 @@ import { useVerge } from "@/hooks/use-verge";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { Edit } from "@mui/icons-material";
import { EditRounded } from "@mui/icons-material";
export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -115,7 +115,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
<Item>
<ListItemText primary={t("CSS Injection")} />
<Button
startIcon={<Edit />}
startIcon={<EditRounded />}
variant="outlined"
onClick={() => {
setEditorOpen(true);
@@ -123,19 +123,21 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
>
{t("Edit")} CSS
</Button>
<EditorViewer
open={editorOpen}
title={`${t("Edit")} CSS`}
initialData={Promise.resolve(theme.css_injection ?? "")}
language="css"
onSave={(_prev, curr) => {
theme.css_injection = curr;
handleChange("css_injection");
}}
onClose={() => {
setEditorOpen(false);
}}
/>
{editorOpen && (
<EditorViewer
open={true}
title={`${t("Edit")} CSS`}
initialData={Promise.resolve(theme.css_injection ?? "")}
language="css"
onSave={(_prev, curr) => {
theme.css_injection = curr;
handleChange("css_injection");
}}
onClose={() => {
setEditorOpen(false);
}}
/>
)}
</Item>
</List>
</BaseDialog>
+3 -3
View File
@@ -144,7 +144,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
autoCorrect="off"
autoCapitalize="off"
@@ -190,7 +190,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
autoCorrect="off"
autoCapitalize="off"
@@ -207,7 +207,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} />
<TextField
autoComplete="off"
autoComplete="new-password"
size="small"
type="number"
autoCorrect="off"
+1 -1
View File
@@ -43,7 +43,7 @@ export const WebUIItem = (props: Props) => {
<>
<Stack spacing={0.75} direction="row" mt={1} mb={1} alignItems="center">
<TextField
autoComplete="off"
autoComplete="new-password"
fullWidth
size="small"
value={editValue}
+24 -5
View File
@@ -2,7 +2,11 @@ import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { TextField, Select, MenuItem, Typography } from "@mui/material";
import { Settings, Shuffle } from "@mui/icons-material";
import {
SettingsRounded,
ShuffleRounded,
LanRounded,
} from "@mui/icons-material";
import { DialogRef, Notice, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state";
@@ -16,6 +20,7 @@ import getSystem from "@/utils/get-system";
import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
const isWIN = getSystem() === "windows";
@@ -37,6 +42,7 @@ const SettingClash = ({ onError }: Props) => {
const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null);
const networkRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => {
@@ -60,8 +66,21 @@ const SettingClash = ({ onError }: Props) => {
<ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} />
<NetworkInterfaceViewer ref={networkRef} />
<SettingItem label={t("Allow Lan")}>
<SettingItem
label={t("Allow Lan")}
extra={
<TooltipIcon
title={t("Network Interface")}
color={"inherit"}
icon={LanRounded}
onClick={() => {
networkRef.current?.open();
}}
/>
}
>
<GuardState
value={allowLan ?? false}
valueProps="checked"
@@ -112,7 +131,7 @@ const SettingClash = ({ onError }: Props) => {
<TooltipIcon
title={t("Random Port")}
color={enable_random_port ? "primary" : "inherit"}
icon={Shuffle}
icon={ShuffleRounded}
onClick={() => {
Notice.success(
t("Restart Application to Apply Modifications"),
@@ -125,7 +144,7 @@ const SettingClash = ({ onError }: Props) => {
}
>
<TextField
autoComplete="off"
autoComplete="new-password"
disabled={enable_random_port}
size="small"
value={verge_mixed_port ?? 7897}
@@ -148,7 +167,7 @@ const SettingClash = ({ onError }: Props) => {
label={t("Clash Core")}
extra={
<TooltipIcon
icon={Settings}
icon={SettingsRounded}
onClick={() => coreRef.current?.open()}
/>
}
+36 -41
View File
@@ -1,13 +1,13 @@
import useSWR from "swr";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { PrivacyTipRounded, Settings } from "@mui/icons-material";
import { SettingsRounded } from "@mui/icons-material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { DialogRef, Switch } from "@/components/base";
import { DialogRef, Notice, Switch } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state";
import { ServiceViewer } from "./mods/service-viewer";
import { ServiceSwitcher } from "./mods/service-switcher";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@@ -20,22 +20,23 @@ const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge();
// service mode
const { data: serviceStatus } = useSWR("checkService", checkService, {
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
});
const { data: serviceStatus, mutate: mutateServiceStatus } = useSWR(
"checkService",
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null);
const {
enable_tun_mode,
enable_auto_launch,
enable_service_mode,
enable_silent_start,
enable_system_proxy,
} = verge ?? {};
@@ -49,14 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
<SettingItem
label={t("Tun Mode")}
extra={
<TooltipIcon
title={t("Tun Mode Info")}
icon={Settings}
icon={SettingsRounded}
onClick={() => tunRef.current?.open()}
/>
}
@@ -66,38 +66,33 @@ const SettingSystem = ({ onError }: Props) => {
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_tun_mode: e })}
onGuard={(e) => patchVerge({ enable_tun_mode: e })}
onChange={(e) => {
if (serviceStatus !== "active") {
onChangeData({ enable_tun_mode: false });
} else {
onChangeData({ enable_tun_mode: e });
}
}}
onGuard={(e) => {
if (serviceStatus !== "active" && e) {
Notice.error(t("Please Enable Service Mode"));
return Promise.resolve();
} else {
return patchVerge({ enable_tun_mode: e });
}
}}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem
label={t("Service Mode")}
extra={
<TooltipIcon
title={t("Service Mode Info")}
icon={PrivacyTipRounded}
onClick={() => serviceRef.current?.open()}
/>
}
>
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
>
<Switch
edge="end"
disabled={
serviceStatus !== "active" && serviceStatus !== "installed"
}
/>
</GuardState>
<SettingItem label={t("Service Mode")}>
<ServiceSwitcher
status={serviceStatus ?? "unknown"}
mutate={mutateServiceStatus}
patchVerge={patchVerge}
onChangeData={onChangeData}
/>
</SettingItem>
<SettingItem
@@ -106,7 +101,7 @@ const SettingSystem = ({ onError }: Props) => {
<>
<TooltipIcon
title={t("System Proxy Info")}
icon={Settings}
icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()}
/>
</>
+23 -3
View File
@@ -1,13 +1,21 @@
import { useRef } from "react";
import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/api/dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material";
import {
Button,
MenuItem,
Select,
Input,
Typography,
Box,
} from "@mui/material";
import {
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
copyClashEnv,
} from "@/services/cmds";
import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge";
@@ -24,6 +32,8 @@ import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { ContentCopyRounded } from "@mui/icons-material";
interface Props {
onError?: (err: Error) => void;
@@ -67,6 +77,11 @@ const SettingVerge = ({ onError }: Props) => {
}
};
const onCopyClashEnv = useCallback(async () => {
await copyClashEnv();
Notice.success(t("Copy Success"), 1000);
}, []);
return (
<SettingList title={t("Verge Setting")}>
<ThemeViewer ref={themeRef} />
@@ -123,7 +138,12 @@ const SettingVerge = ({ onError }: Props) => {
</SettingItem>
)}
<SettingItem label={t("Copy Env Type")}>
<SettingItem
label={t("Copy Env Type")}
extra={
<TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} />
}
>
<GuardState
value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
onCatch={onError}
+12 -4
View File
@@ -13,7 +13,7 @@ import {
alpha,
} from "@mui/material";
import { BaseLoading } from "@/components/base";
import { LanguageTwoTone } from "@mui/icons-material";
import { LanguageRounded } from "@mui/icons-material";
import { Notice } from "@/components/base";
import { TestBox } from "./test-box";
import delayManager from "@/services/delay";
@@ -32,8 +32,14 @@ let eventListener: UnlistenFn | null = null;
export const TestItem = (props: Props) => {
const { itemData, onEdit, onDelete: onDeleteItem } = props;
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.id });
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -99,8 +105,10 @@ export const TestItem = (props: Props) => {
return (
<Box
sx={{
position: "relative",
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
}}
>
<TestBox
@@ -138,7 +146,7 @@ export const TestItem = (props: Props) => {
</Box>
) : (
<Box sx={{ display: "flex", justifyContent: "center" }}>
<LanguageTwoTone sx={{ height: "40px" }} fontSize="large" />
<LanguageRounded sx={{ height: "40px" }} fontSize="large" />
</Box>
)}
+38 -3
View File
@@ -1,5 +1,6 @@
{
"millis": "millis",
"seconds": "seconds",
"mins": "mins",
"Back": "Back",
"Close": "Close",
@@ -54,6 +55,7 @@
"Create Profile": "Create Profile",
"Edit Profile": "Edit Profile",
"Edit Proxies": "Edit Proxies",
"Use newlines for multiple uri": "Use newlines for multiple uri(Base64 encoding supported)",
"Edit Rules": "Edit Rules",
"Rule Type": "Rule Type",
"Rule Content": "Rule Content",
@@ -61,11 +63,14 @@
"No Resolve": "No Resolve",
"Prepend Rule": "Prepend Rule",
"Append Rule": "Append Rule",
"Delete Rule": "Delete Rule",
"Prepend Group": "Prepend Group",
"Append Group": "Append Group",
"Prepend Proxy": "Prepend Proxy",
"Append Proxy": "Append Proxy",
"Rule Condition Required": "Rule Condition Required",
"Invalid Rule": "Invalid Rule",
"Advanced": "Advanced",
"Visible": "Visible",
"Visualization": "Visualization",
"DOMAIN": "Matches the full domain name",
"DOMAIN-SUFFIX": "Matches the domain suffix",
"DOMAIN-KEYWORD": "Matches the domain keyword",
@@ -104,6 +109,32 @@
"REJECT-DROP": "Discards requests",
"PASS": "Skips this rule when matched",
"Edit Groups": "Edit Proxy Groups",
"Group Type": "Group Type",
"select": "Select proxy manually",
"url-test": "Select proxy based on URL test delay",
"fallback": "Switch to another proxy on error",
"load-balance": "Distribute proxy based on load balancing",
"relay": "Pass through the defined proxy chain",
"Group Name": "Group Name",
"Use Proxies": "Use Proxies",
"Use Provider": "Use Provider",
"Health Check Url": "Health Check Url",
"Expected Status": "Expected Status",
"Interval": "Interval",
"Lazy": "Lazy",
"Timeout": "Timeout",
"Max Failed Times": "Max Failed Times",
"Interface Name": "Interface Name",
"Routing Mark": "Routing Mark",
"Include All": "Include All Proxies and Providers",
"Include All Providers": "Include All Providers",
"Include All Proxies": "Include All Proxies",
"Exclude Filter": "Exclude Filter",
"Exclude Type": "Exclude Type",
"Disable UDP": "Disable UDP",
"Hidden": "Hidden",
"Group Name Required": "Group Name Required",
"Group Name Already Exists": "Group Name Already Exists",
"Extend Config": "Extend Config",
"Extend Script": "Extend Script",
"Global Merge": "Global Extend Config",
@@ -211,6 +242,9 @@
"Github Repo": "Github Repo",
"Clash Setting": "Clash Setting",
"Allow Lan": "Allow LAN",
"Network Interface": "Network Interface",
"Ip Address": "IP Address",
"Mac Address": "MAC Address",
"IPv6": "IPv6",
"Log Level": "Log Level",
"Port Config": "Port Config",
@@ -232,7 +266,7 @@
"Restart": "Restart",
"Release Version": "Release Version",
"Alpha Version": "Alpha Version",
"Tun mode requires": "Tun mode requires",
"Please Enable Service Mode": "Please Install and Enable Service Mode First",
"Grant": "Grant",
"Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
@@ -246,6 +280,7 @@
"Tray Click Event": "Tray Click Event",
"Show Main Window": "Show Main Window",
"Copy Env Type": "Copy Env Type",
"Copy Success": "Copy Success",
"Start Page": "Start Page",
"Startup Script": "Startup Script",
"Browse": "Browse",
+37 -2
View File
@@ -1,5 +1,6 @@
{
"millis": "میلی‌ثانیه",
"seconds": "ثانیه‌ها",
"mins": "دقیقه",
"Back": "بازگشت",
"Close": "بستن",
@@ -54,6 +55,7 @@
"Create Profile": "ایجاد پروفایل",
"Edit Profile": "ویرایش پروفایل",
"Edit Proxies": "ویرایش پروکسی‌ها",
"Use newlines for multiple uri": "استفاده از خطوط جدید برای چندین آدرس (پشتیبانی از رمزگذاری Base64)",
"Edit Rules": "ویرایش قوانین",
"Rule Type": "نوع قانون",
"Rule Content": "محتوای قانون",
@@ -61,9 +63,12 @@
"No Resolve": "بدون حل",
"Prepend Rule": "اضافه کردن قانون به ابتدا",
"Append Rule": "اضافه کردن قانون به انتها",
"Delete Rule": "حذف قانون",
"Prepend Group": "اضافه کردن گروه به ابتدا",
"Append Group": "اضافه کردن گروه به انتها",
"Rule Condition Required": "شرط قانون الزامی است",
"Invalid Rule": "قانون نامعتبر",
"Advanced": "پیشرفته",
"Visualization": "تجسم",
"DOMAIN": "مطابقت با نام کامل دامنه",
"DOMAIN-SUFFIX": "مطابقت با پسوند دامنه",
"DOMAIN-KEYWORD": "مطابقت با کلمه کلیدی دامنه",
@@ -102,6 +107,32 @@
"REJECT-DROP": "درخواست‌ها را نادیده می‌گیرد",
"PASS": "این قانون را در صورت تطابق نادیده می‌گیرد",
"Edit Groups": "ویرایش گروه‌های پروکسی",
"Group Type": "نوع گروه",
"select": "انتخاب پروکسی به صورت دستی",
"url-test": "انتخاب پروکسی بر اساس تأخیر آزمایش URL",
"fallback": "تعویض به پروکسی دیگر در صورت بروز خطا",
"load-balance": "توزیع پروکسی بر اساس توازن بار",
"relay": "عبور از زنجیره پروکسی تعریف شده",
"Group Name": "نام گروه",
"Use Proxies": "استفاده از پروکسی‌ها",
"Use Provider": "استفاده از ارائه‌دهنده",
"Health Check Url": "آدرس بررسی سلامت",
"Expected Status": "وضعیت مورد انتظار",
"Interval": "فاصله زمانی",
"Lazy": "تنبل",
"Timeout": "زمان قطع",
"Max Failed Times": "حداکثر تعداد شکست‌ها",
"Interface Name": "نام رابط",
"Routing Mark": "علامت مسیریابی",
"Include All": "شامل همه پروکسی‌ها و ارائه‌دهنده‌ها",
"Include All Providers": "شامل همه ارائه‌دهنده‌ها",
"Include All Proxies": "شامل همه پروکسی‌ها",
"Exclude Filter": "فیلتر استثناء",
"Exclude Type": "نوع استثناء",
"Disable UDP": "غیرفعال کردن UDP",
"Hidden": "مخفی",
"Group Name Required": "نام گروه مورد نیاز است",
"Group Name Already Exists": "نام گروه قبلا وجود دارد",
"Extend Config": "توسعه پیکربندی",
"Extend Script": "ادغام اسکریپت",
"Global Merge": "تنظیمات گسترده‌ی سراسری",
@@ -206,6 +237,9 @@
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN",
"Network Interface": "رابط شبکه",
"Ip Address": "آدرس IP",
"Mac Address": "آدرس MAC",
"IPv6": "IPv6",
"Log Level": "سطح لاگ",
"Port Config": "پیکربندی پورت",
@@ -227,7 +261,7 @@
"Restart": "راه‌اندازی مجدد",
"Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا",
"Tun mode requires": "Tun mode نیاز دارد",
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
@@ -244,6 +278,7 @@
"Tray Click Event": "رویداد کلیک در سینی سیستم",
"Show Main Window": "نمایش پنجره اصلی",
"Copy Env Type": "کپی نوع محیط",
"Copy Success": "کپی با موفقیت انجام شد",
"Start Page": "صفحه شروع",
"Startup Script": "اسکریپت راه‌اندازی",
"Browse": "مرور کردن",
+37 -2
View File
@@ -1,5 +1,6 @@
{
"millis": "миллисекунды",
"seconds": "секунды",
"mins": "минуты",
"Back": "Назад",
"Close": "Закрыть",
@@ -54,6 +55,7 @@
"Create Profile": "Создать профиль",
"Edit Profile": "Изменить профиль",
"Edit Proxies": "Редактировать прокси",
"Use newlines for multiple uri": "Используйте символы новой строки для нескольких URI (поддерживается кодировка Base64)",
"Edit Rules": "Редактировать правила",
"Rule Type": "Тип правила",
"Rule Content": "Содержимое правила",
@@ -61,9 +63,12 @@
"No Resolve": "Без разрешения",
"Prepend Rule": "Добавить правило в начало",
"Append Rule": "Добавить правило в конец",
"Delete Rule": "Удалить правило",
"Prepend Group": "Добавить группу в начало",
"Append Group": "Добавить группу в конец",
"Rule Condition Required": "Требуется условие правила",
"Invalid Rule": "Недействительное правило",
"Advanced": "Дополнительно",
"Visualization": "Визуализация",
"DOMAIN": "Соответствует полному доменному имени",
"DOMAIN-SUFFIX": "Соответствует суффиксу домена",
"DOMAIN-KEYWORD": "Соответствует ключевому слову домена",
@@ -102,6 +107,32 @@
"REJECT-DROP": "Отклоняет запросы",
"PASS": "Пропускает это правило при совпадении",
"Edit Groups": "Редактировать группы прокси",
"Group Type": "Тип группы",
"select": "Выбор прокси вручную",
"url-test": "Выбор прокси на основе задержки теста URL",
"fallback": "Переключение на другой прокси при ошибке",
"load-balance": "Распределение прокси на основе балансировки нагрузки",
"relay": "Передача через определенную цепочку прокси",
"Group Name": "Имя группы",
"Use Proxies": "Использовать прокси",
"Use Provider": "Использовать провайдера",
"Health Check Url": "URL проверки здоровья",
"Expected Status": "Ожидаемый статус",
"Interval": "Интервал",
"Lazy": "Ленивый",
"Timeout": "Таймаут",
"Max Failed Times": "Максимальное количество неудач",
"Interface Name": "Имя интерфейса",
"Routing Mark": "Марка маршрутизации",
"Include All": "Включить все прокси и провайдеры",
"Include All Providers": "Включить всех провайдеров",
"Include All Proxies": "Включить все прокси",
"Exclude Filter": "Исключить фильтр",
"Exclude Type": "Тип исключения",
"Disable UDP": "Отключить UDP",
"Hidden": "Скрытый",
"Group Name Required": "Требуется имя группы",
"Group Name Already Exists": "Имя группы уже существует",
"Extend Config": "Изменить Merge.",
"Extend Script": "Изменить Script",
"Global Merge": "Глобальный расширенный Настройки",
@@ -209,6 +240,9 @@
"Github Repo": "GitHub репозиторий",
"Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть",
"Network Interface": "Сетевой интерфейс",
"Ip Address": "IP адрес",
"Mac Address": "MAC адрес",
"IPv6": "IPv6",
"Log Level": "Уровень логов",
"Port Config": "Настройка порта",
@@ -230,7 +264,7 @@
"Restart": "Перезапуск",
"Release Version": "Официальная версия",
"Alpha Version": "Альфа-версия",
"Tun mode requires": "Требуется Режим туннеля",
"Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания",
"Grant": "Предоставить",
"Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
@@ -244,6 +278,7 @@
"Tray Click Event": "Событие щелчка в лотке",
"Show Main Window": "Показать главное окно",
"Copy Env Type": "Скопировать тип Env",
"Copy Success": "Скопировано",
"Start Page": "Главная страница",
"Startup Script": "Скрипт запуска",
"Browse": "Просмотреть",
+38 -3
View File
@@ -1,6 +1,7 @@
{
"millis": "毫秒",
"mins": "分钟",
"seconds": "秒",
"Back": "返回",
"Close": "关闭",
"Cancel": "取消",
@@ -54,6 +55,7 @@
"Create Profile": "新建配置",
"Edit Profile": "编辑配置",
"Edit Proxies": "编辑节点",
"Use newlines for multiple uri": "多条URI请使用换行分隔(支持Base64编码)",
"Edit Rules": "编辑规则",
"Rule Type": "规则类型",
"Rule Content": "规则内容",
@@ -61,11 +63,14 @@
"No Resolve": "跳过DNS解析",
"Prepend Rule": "添加前置规则",
"Append Rule": "添加后置规则",
"Delete Rule": "删除规则",
"Prepend Group": "添加前置代理组",
"Append Group": "添加后置代理组",
"Prepend Proxy": "添加前置代理节点",
"Append Proxy": "添加后置代理节点",
"Rule Condition Required": "规则条件缺失",
"Invalid Rule": "无效规则",
"Advanced": "高级",
"Visible": "可视化",
"Visualization": "可视化",
"DOMAIN": "匹配完整域名",
"DOMAIN-SUFFIX": "匹配域名后缀",
"DOMAIN-KEYWORD": "匹配域名关键字",
@@ -104,6 +109,32 @@
"REJECT-DROP": "抛弃请求",
"PASS": "跳过此规则",
"Edit Groups": "编辑代理组",
"Group Type": "代理组类型",
"select": "手动选择代理",
"url-test": "根据URL测试延迟选择代理",
"fallback": "不可用时切换到另一个代理",
"load-balance": "根据负载均衡分配代理",
"relay": "根据定义的代理链传递",
"Group Name": "代理组组名",
"Use Proxies": "引入代理",
"Use Provider": "引入代理集合",
"Health Check Url": "健康检查测试地址",
"Expected Status": "期望状态码",
"Interval": "检查间隔",
"Lazy": "懒惰状态",
"Timeout": "超时时间",
"Max Failed Times": "最大失败次数",
"Interface Name": "出站接口",
"Routing Mark": "路由标记",
"Include All": "引入所有出站代理、代理集合",
"Include All Providers": "引入所有代理集合",
"Include All Proxies": "引入所有出站代理",
"Exclude Filter": "排除节点",
"Exclude Type": "排除节点类型",
"Disable UDP": "禁用UDP",
"Hidden": "隐藏代理组",
"Group Name Required": "代理组名称不能为空",
"Group Name Already Exists": "代理组名称已存在",
"Extend Config": "扩展配置",
"Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置",
@@ -211,6 +242,9 @@
"Github Repo": "GitHub 项目地址",
"Clash Setting": "Clash 设置",
"Allow Lan": "局域网连接",
"Network Interface": "网络接口",
"Ip Address": "IP 地址",
"Mac Address": "MAC 地址",
"IPv6": "IPv6",
"Log Level": "日志等级",
"Port Config": "端口设置",
@@ -232,7 +266,7 @@
"Restart": "重启内核",
"Release Version": "正式版",
"Alpha Version": "预览版",
"Tun mode requires": "如需启用 Tun 模式需要授权",
"Please Enable Service Mode": "请先安装并启用服务模式",
"Grant": "授权",
"Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
@@ -246,6 +280,7 @@
"Tray Click Event": "托盘点击事件",
"Show Main Window": "显示主窗口",
"Copy Env Type": "复制环境变量类型",
"Copy Success": "复制成功",
"Start Page": "启动页面",
"Startup Script": "启动脚本",
"Browse": "浏览",
+4 -3
View File
@@ -171,10 +171,11 @@ const ProfilePage = () => {
}, 100);
try {
await patchProfiles({ current });
mutateLogs();
await mutateLogs();
closeAllConnections();
setTimeout(() => activateSelected(), 2000);
Notice.success(t("Profile Switched"), 1000);
activateSelected().then(() => {
Notice.success(t("Profile Switched"), 1000);
});
} catch (err: any) {
Notice.error(err?.message || err.toString(), 4000);
} finally {
+2 -2
View File
@@ -2,7 +2,7 @@ import { Box, ButtonGroup, Grid, IconButton } from "@mui/material";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineSharp, Telegram } from "@mui/icons-material";
import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge";
import SettingClash from "@/components/setting/setting-clash";
@@ -42,7 +42,7 @@ const SettingPage = () => {
title={t("Manual")}
onClick={toGithubDoc}
>
<HelpOutlineSharp fontSize="inherit" />
<HelpOutlineRounded fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
+12
View File
@@ -2,6 +2,10 @@ import dayjs from "dayjs";
import { invoke } from "@tauri-apps/api/tauri";
import { Notice } from "@/components/base";
export async function copyClashEnv() {
return invoke<void>("copy_clash_env");
}
export async function getClashLogs() {
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/;
const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
@@ -233,3 +237,11 @@ export async function copyIconFile(
export async function downloadIconCache(url: string, name: string) {
return invoke<string>("download_icon_cache", { url, name });
}
export async function getNetworkInterfaces() {
return invoke<string[]>("get_network_interfaces");
}
export async function getNetworkInterfacesInfo() {
return invoke<INetworkInterface[]>("get_network_interfaces_info");
}
+481 -3
View File
@@ -197,11 +197,489 @@ interface IVergeTestItem {
icon?: string;
url: string;
}
interface IAddress {
V4?: {
ip: string;
broadcast?: string;
netmask?: string;
};
V6?: {
ip: string;
broadcast?: string;
netmask?: string;
};
}
interface INetworkInterface {
name: string;
addr: IAddress[];
mac_addr?: string;
index: number;
}
interface ISeqProfileConfig {
prepend: string[];
append: string[];
delete: string[];
prepend: [];
append: [];
delete: [];
}
interface IProxyGroupConfig {
name: string;
type: "select" | "url-test" | "fallback" | "load-balance" | "relay";
proxies?: string[];
use?: string[];
url?: string;
interval?: number;
lazy?: boolean;
timeout?: number;
"max-failed-times"?: number;
"disable-udp"?: boolean;
"interface-name": string;
"routing-mark"?: number;
"include-all"?: boolean;
"include-all-proxies"?: boolean;
"include-all-providers"?: boolean;
filter?: string;
"exclude-filter"?: string;
"exclude-type"?: string;
"expected-status"?: string;
hidden?: boolean;
icon?: string;
}
interface WsOptions {
path?: string;
headers?: {
[key: string]: string;
};
"max-early-data"?: number;
"early-data-header-name"?: string;
"v2ray-http-upgrade"?: boolean;
"v2ray-http-upgrade-fast-open"?: boolean;
}
interface HttpOptions {
method?: string;
path?: string[];
headers?: {
[key: string]: string[];
};
}
interface H2Options {
path?: string;
host?: string;
}
interface GrpcOptions {
"grpc-service-name"?: string;
}
interface RealityOptions {
"public-key"?: string;
"short-id"?: string;
}
type ClientFingerprint =
| "chrome"
| "firefox"
| "safari"
| "iOS"
| "android"
| "edge"
| "360"
| "qq"
| "random";
type NetworkType = "ws" | "http" | "h2" | "grpc" | "tcp";
type CipherType =
| "none"
| "auto"
| "dummy"
| "aes-128-gcm"
| "aes-192-gcm"
| "aes-256-gcm"
| "lea-128-gcm"
| "lea-192-gcm"
| "lea-256-gcm"
| "aes-128-gcm-siv"
| "aes-256-gcm-siv"
| "2022-blake3-aes-128-gcm"
| "2022-blake3-aes-256-gcm"
| "aes-128-cfb"
| "aes-192-cfb"
| "aes-256-cfb"
| "aes-128-ctr"
| "aes-192-ctr"
| "aes-256-ctr"
| "chacha20"
| "chacha20-ietf"
| "chacha20-ietf-poly1305"
| "2022-blake3-chacha20-poly1305"
| "rabbit128-poly1305"
| "xchacha20-ietf-poly1305"
| "xchacha20"
| "aegis-128l"
| "aegis-256"
| "aez-384"
| "deoxys-ii-256-128"
| "rc4-md5";
// base
interface IProxyBaseConfig {
tfo?: boolean;
mptcp?: boolean;
"interface-name"?: string;
"routing-mark"?: number;
"ip-version"?: "dual" | "ipv4" | "ipv6" | "ipv4-prefer" | "ipv6-prefer";
"dialer-proxy"?: string;
}
// direct
interface IProxyDirectConfig extends IProxyBaseConfig {
name: string;
type: "direct";
}
// dns
interface IProxyDnsConfig extends IProxyBaseConfig {
name: string;
type: "dns";
}
// http
interface IProxyHttpConfig extends IProxyBaseConfig {
name: string;
type: "http";
server?: string;
port?: number;
username?: string;
password?: string;
tls?: boolean;
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
headers?: {
[key: string]: string;
};
}
// socks5
interface IProxySocks5Config extends IProxyBaseConfig {
name: string;
type: "socks5";
server?: string;
port?: number;
username?: string;
password?: string;
tls?: boolean;
udp?: boolean;
"skip-cert-verify"?: boolean;
fingerprint?: string;
}
// ssh
interface IProxySshConfig extends IProxyBaseConfig {
name: string;
type: "ssh";
server?: string;
port?: number;
username?: string;
password?: string;
"private-key"?: string;
"private-key-passphrase"?: string;
"host-key"?: string;
"host-key-algorithms"?: string;
}
// trojan
interface IProxyTrojanConfig extends IProxyBaseConfig {
name: string;
type: "trojan";
server?: string;
port?: number;
password?: string;
alpn?: string[];
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
udp?: boolean;
network?: NetworkType;
"reality-opts"?: RealityOptions;
"grpc-opts"?: GrpcOptions;
"ws-opts"?: WsOptions;
"ss-opts"?: {
enabled?: boolean;
method?: string;
password?: string;
};
"client-fingerprint"?: ClientFingerprint;
}
// tuic
interface IProxyTuicConfig extends IProxyBaseConfig {
name: string;
type: "tuic";
server?: string;
port?: number;
token?: string;
uuid?: string;
password?: string;
ip?: string;
"heartbeat-interval"?: number;
alpn?: string[];
"reduce-rtt"?: boolean;
"request-timeout"?: number;
"udp-relay-mode"?: string;
"congestion-controller"?: string;
"disable-sni"?: boolean;
"max-udp-relay-packet-size"?: number;
"fast-open"?: boolean;
"max-open-streams"?: number;
cwnd?: number;
"skip-cert-verify"?: boolean;
fingerprint?: string;
ca?: string;
"ca-str"?: string;
"recv-window-conn"?: number;
"recv-window"?: number;
"disable-mtu-discovery"?: boolean;
"max-datagram-frame-size"?: number;
sni?: string;
"udp-over-stream"?: boolean;
"udp-over-stream-version"?: number;
}
// vless
interface IProxyVlessConfig extends IProxyBaseConfig {
name: string;
type: "vless";
server?: string;
port?: number;
uuid?: string;
flow?: string;
tls?: boolean;
alpn?: string[];
udp?: boolean;
"packet-addr"?: boolean;
xudp?: boolean;
"packet-encoding"?: string;
network?: NetworkType;
"reality-opts"?: RealityOptions;
"http-opts"?: HttpOptions;
"h2-opts"?: H2Options;
"grpc-opts"?: GrpcOptions;
"ws-opts"?: WsOptions;
"ws-path"?: string;
"ws-headers"?: {
[key: string]: string;
};
"skip-cert-verify"?: boolean;
fingerprint?: string;
servername?: string;
"client-fingerprint"?: ClientFingerprint;
}
// vmess
interface IProxyVmessConfig extends IProxyBaseConfig {
name: string;
type: "vmess";
server?: string;
port?: number;
uuid?: string;
alterId?: number;
cipher?: CipherType;
udp?: boolean;
network?: NetworkType;
tls?: boolean;
alpn?: string[];
"skip-cert-verify"?: boolean;
fingerprint?: string;
servername?: string;
"reality-opts"?: RealityOptions;
"http-opts"?: HttpOptions;
"h2-opts"?: H2Options;
"grpc-opts"?: GrpcOptions;
"ws-opts"?: WsOptions;
"packet-addr"?: boolean;
xudp?: boolean;
"packet-encoding"?: string;
"global-padding"?: boolean;
"authenticated-length"?: boolean;
"client-fingerprint"?: ClientFingerprint;
}
interface WireGuardPeerOptions {
server?: string;
port?: number;
"public-key"?: string;
"pre-shared-key"?: string;
reserved?: number[];
"allowed-ips"?: string[];
}
// wireguard
interface IProxyWireguardConfig extends IProxyBaseConfig, WireGuardPeerOptions {
name: string;
type: "wireguard";
ip?: string;
ipv6?: string;
"private-key"?: string;
workers?: number;
mtu?: number;
udp?: boolean;
"persistent-keepalive"?: number;
peers?: WireGuardPeerOptions[];
"remote-dns-resolve"?: boolean;
dns?: string[];
"refresh-server-ip-interval"?: number;
}
// hysteria
interface IProxyHysteriaConfig extends IProxyBaseConfig {
name: string;
type: "hysteria";
server?: string;
port?: number;
ports?: string;
protocol?: string;
"obfs-protocol"?: string;
up?: string;
"up-speed"?: number;
down?: string;
"down-speed"?: number;
auth?: string;
"auth-str"?: string;
obfs?: string;
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
alpn?: string[];
ca?: string;
"ca-str"?: string;
"recv-window-conn"?: number;
"recv-window"?: number;
"disable-mtu-discovery"?: boolean;
"fast-open"?: boolean;
"hop-interval"?: number;
}
// hysteria2
interface IProxyHysteria2Config extends IProxyBaseConfig {
name: string;
type: "hysteria2";
server?: string;
port?: number;
ports?: string;
"hop-interval"?: number;
protocol?: string;
"obfs-protocol"?: string;
up?: string;
down?: string;
password?: string;
obfs?: string;
"obfs-password"?: string;
sni?: string;
"skip-cert-verify"?: boolean;
fingerprint?: string;
alpn?: string[];
ca?: string;
"ca-str"?: string;
cwnd?: number;
"udp-mtu"?: number;
}
// shadowsocks
interface IProxyShadowsocksConfig extends IProxyBaseConfig {
name: string;
type: "ss";
server?: string;
port?: number;
password?: string;
cipher?: CipherType;
udp?: boolean;
plugin?: "obfs" | "v2ray-plugin" | "shadow-tls" | "restls";
"plugin-opts"?: {
mode?: string;
host?: string;
password?: string;
path?: string;
tls?: string;
fingerprint?: string;
headers?: {
[key: string]: string;
};
"skip-cert-verify"?: boolean;
version?: number;
mux?: boolean;
"v2ray-http-upgrade"?: boolean;
"v2ray-http-upgrade-fast-open"?: boolean;
"version-hint"?: string;
"restls-script"?: string;
};
"udp-over-tcp"?: boolean;
"udp-over-tcp-version"?: number;
"client-fingerprint"?: ClientFingerprint;
}
// shadowsocksR
interface IProxyshadowsocksRConfig extends IProxyBaseConfig {
name: string;
type: "ssr";
server?: string;
port?: number;
password?: string;
cipher?: CipherType;
obfs?: string;
"obfs-param"?: string;
protocol?: string;
"protocol-param"?: string;
udp?: boolean;
}
// sing-mux
interface IProxySmuxConfig {
smux?: {
enabled?: boolean;
protocol?: "smux" | "yamux" | "h2mux";
"max-connections"?: number;
"min-streams"?: number;
"max-streams"?: number;
padding?: boolean;
statistic?: boolean;
"only-tcp"?: boolean;
"brutal-opts"?: {
enabled?: boolean;
up?: string;
down?: string;
};
};
}
// snell
interface IProxySnellConfig extends IProxyBaseConfig {
name: string;
type: "snell";
server?: string;
port?: number;
psk?: string;
udp?: boolean;
version?: number;
"obfs-opts"?: {};
}
interface IProxyConfig
extends IProxyBaseConfig,
IProxyDirectConfig,
IProxyDnsConfig,
IProxyHttpConfig,
IProxySocks5Config,
IProxySshConfig,
IProxyTrojanConfig,
IProxyTuicConfig,
IProxyVlessConfig,
IProxyVmessConfig,
IProxyWireguardConfig,
IProxyHysteriaConfig,
IProxyHysteria2Config,
IProxyShadowsocksConfig,
IProxyshadowsocksRConfig,
IProxySmuxConfig,
IProxySnellConfig {
type:
| "ss"
| "ssr"
| "direct"
| "dns"
| "snell"
| "http"
| "trojan"
| "hysteria"
| "hysteria2"
| "tuic"
| "wireguard"
| "ssh"
| "socks5"
| "vmess"
| "vless";
}
interface IVergeConfig {
+12
View File
@@ -0,0 +1,12 @@
export default function debounce<T extends (...args: any[]) => void>(
func: T,
wait: number
): T {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function (this: any, ...args: Parameters<T>) {
if (timeout !== null) {
clearTimeout(timeout);
}
timeout = setTimeout(() => func.apply(this, args), wait);
} as T;
}
File diff suppressed because it is too large Load Diff