Compare commits

...
74 Commits
Author SHA1 Message Date
wonfen 7d2b18759e Release 1.7.7 2024-08-22 03:47:33 +08:00
wonfen aa8f6f4ad6 chore: Shorten text to prevent Windows tooltip truncation. 2024-08-22 03:41:16 +08:00
wonfen 592167ffb7 fix: profiles will not be selected after import 2024-08-22 00:57:04 +08:00
wonfen 12a6bfad00 Release 1.7.6 2024-08-18 00:49:23 +08:00
tony-sung 1ea3b956ad Update web-ui-viewer.tsx Fix yacd parameter hostname error (#1484) 2024-08-18 00:23:24 +08:00
MystiPanda 1b7dafe743 feat: auto select profile 2024-07-30 09:06:12 +08:00
MystiPanda 624eb2a2ba chore: prepend new added rules 2024-07-24 14:25:06 +08:00
MystiPanda 88d98517c8 fix: wrap the password in quotes
#1460
2024-07-24 14:22:23 +08:00
Avan 9528606906 style: AddressDisplay padding (#1457) 2024-07-23 22:17:51 +08:00
nhsmw 09b44a3af3 fix: update test url 2024-07-19 10:32:41 +08:00
MystiPanda dc861eca7d chore: remove unnecessary console log 2024-07-18 23:37:54 +08:00
MystiPanda e749fe70e2 fix: use group testUrl
#1384
2024-07-18 15:04:55 +08:00
MystiPanda d20c082deb fix: windows x86 updater url 2024-07-18 14:38:31 +08:00
MystiPanda c4abc3ade3 fix: error command status 2024-07-16 18:37:56 +08:00
MystiPanda ff28aab56e fix: MacOS service install error 2024-07-16 13:01:27 +08:00
MystiPanda 1a6f842492 fix(#1335): support cidr 2024-07-16 12:22:11 +08:00
MystiPanda af0cd4a342 fix: try to fix install service 2024-07-16 12:07:23 +08:00
MystiPanda d98b3224cf chore: disable okBtn 2024-07-15 23:38:49 +08:00
MystiPanda f2e5f0754c ci: fix winget uploader 2024-07-15 23:27:38 +08:00
MystiPanda ec1601f317 Release 1.7.5 2024-07-15 22:35:33 +08:00
MystiPanda 54116608dc ci: fix winget uploader 2024-07-15 22:33:57 +08:00
MystiPanda fea6735ea1 fix: copy necessary files 2024-07-15 22:25:04 +08:00
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
78 changed files with 5390 additions and 1031 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
+22
View File
@@ -256,3 +256,25 @@ jobs:
run: pnpm updater-fixed-webview2 run: pnpm updater-fixed-webview2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
submit-to-winget:
runs-on: ubuntu-latest
needs: [release-update]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Submit to Winget
uses: vedantmgoyal9/winget-releaser@main
with:
identifier: ClashVergeRev.ClashVergeRev
version: ${{env.VERSION}}
release-tag: v${{env.VERSION}}
installers-regex: '_(arm64|x64|x86)-setup\.exe$'
token: ${{ secrets.WINGET_TOKEN }}
+2 -1
View File
@@ -7,4 +7,5 @@ dist-ssr
update.json update.json
scripts/_env.sh scripts/_env.sh
.vscode .vscode
.tool-versions .tool-versions
.idea
+73
View File
@@ -1,3 +1,76 @@
## v1.7.7
### Bugs Fixes
- 修复导入订阅没有自动重载(不显示节点)的问题
- 英语状态下修复 Windows 工具栏提示文本超过限制的问题
---
## v1.7.6
### Notice
- Clash Verge Rev 目前已进入稳定周期,日后更新将着重于 bug 修复与内核常规升级
### Features
- Meta(mihomo)内核升级 1.18.7
- 界面细节调整
- 优化服务模式安装逻辑
- 移除无用的 console log
- 能自动选择第一个订阅
### Bugs Fixes
- 修复服务模式安装问题
- 修复 Mac 下的代理绕过 CIDR 写法过滤
- 修复 32 位升级 URL
- 修复不同分组 URL 测试地址配置无效的问题
- 修复 Web UI 下的一处 hostname 参数
---
## v1.7.5
### Features
- 展示局域网 IP 地址信息
- 在设置页面直接复制环境变量
- 优化服务模式安装逻辑
### Performance
- 优化切换订阅速度
- 优化更改端口速度
### Bugs Fixes
- 调整 MacOS 托盘图标大小
- Trojan URI 解析错误
- 卡片拖动显示层级错误
- 代理绕过格式检查错误
- MacOS 下编辑器最大化失败
- MacOS 服务安装失败
- 更改窗口大小导致闪退的问题
---
## v1.7.3
### Features
- 支持可视化编辑订阅代理组
- 支持可视化编辑订阅节点
- 支持可视化编辑订阅规则
- 扩展脚本支持订阅名称参数 `function main(config, profileName)`
### Bugs Fixes
- 代理绕过格式检查错误
---
## v1.7.2 ## v1.7.2
### Break Changes ### Break Changes
+8 -6
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "1.7.2", "version": "1.7.7",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "tauri dev", "dev": "tauri dev",
@@ -24,10 +24,10 @@
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5", "@emotion/styled": "^11.11.5",
"@juggle/resize-observer": "^3.4.0", "@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/lab": "5.0.0-alpha.149",
"@mui/material": "^5.15.21", "@mui/material": "^5.16.0",
"@mui/x-data-grid": "^7.8.0", "@mui/x-data-grid": "^7.9.0",
"@tauri-apps/api": "^1.6.0", "@tauri-apps/api": "^1.6.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.0", "ahooks": "^3.8.0",
@@ -35,12 +35,14 @@
"dayjs": "1.11.5", "dayjs": "1.11.5",
"foxact": "^0.2.35", "foxact": "^0.2.35",
"i18next": "^23.11.5", "i18next": "^23.11.5",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"meta-json-schema": "1.18.6", "meta-json-schema": "1.18.6",
"monaco-editor": "^0.49.0", "monaco-editor": "^0.49.0",
"monaco-yaml": "^5.2.0", "monaco-yaml": "^5.2.0",
"nanoid": "^5.0.7", "nanoid": "^5.0.7",
"peggy": "^4.0.3",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
@@ -48,7 +50,7 @@
"react-i18next": "^13.5.0", "react-i18next": "^13.5.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-monaco-editor": "^0.55.0", "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-transition-group": "^4.4.5",
"react-virtuoso": "^4.7.11", "react-virtuoso": "^4.7.11",
"sockette": "^2.0.6", "sockette": "^2.0.6",
@@ -79,7 +81,7 @@
"sass": "^1.77.6", "sass": "^1.77.6",
"terser": "^5.31.1", "terser": "^5.31.1",
"typescript": "^5.5.3", "typescript": "^5.5.3",
"vite": "^5.3.2", "vite": "^5.3.3",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0" "vite-plugin-svgr": "^4.2.0"
}, },
+196 -98
View File
@@ -26,17 +26,17 @@ importers:
specifier: ^3.4.0 specifier: ^3.4.0
version: 3.4.0 version: 3.4.0
"@mui/icons-material": "@mui/icons-material":
specifier: ^5.15.21 specifier: ^5.16.0
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) 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": "@mui/lab":
specifier: 5.0.0-alpha.149 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": "@mui/material":
specifier: ^5.15.21 specifier: ^5.16.0
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) 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": "@mui/x-data-grid":
specifier: ^7.8.0 specifier: ^7.9.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) 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": "@tauri-apps/api":
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0 version: 1.6.0
@@ -58,6 +58,9 @@ importers:
i18next: i18next:
specifier: ^23.11.5 specifier: ^23.11.5
version: 23.11.5 version: 23.11.5
js-base64:
specifier: ^3.7.7
version: 3.7.7
js-yaml: js-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0 version: 4.1.0
@@ -76,6 +79,9 @@ importers:
nanoid: nanoid:
specifier: ^5.0.7 specifier: ^5.0.7
version: 5.0.7 version: 5.0.7
peggy:
specifier: ^4.0.3
version: 4.0.3
react: react:
specifier: ^18.3.1 specifier: ^18.3.1
version: 18.3.1 version: 18.3.1
@@ -98,8 +104,8 @@ importers:
specifier: ^0.55.0 specifier: ^0.55.0
version: 0.55.0(@types/react@18.3.3)(monaco-editor@0.49.0)(react@18.3.1) version: 0.55.0(@types/react@18.3.3)(monaco-editor@0.49.0)(react@18.3.1)
react-router-dom: react-router-dom:
specifier: ^6.24.0 specifier: ^6.24.1
version: 6.24.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 6.24.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-transition-group: react-transition-group:
specifier: ^4.4.5 specifier: ^4.4.5
version: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 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 version: 4.4.10
"@vitejs/plugin-legacy": "@vitejs/plugin-legacy":
specifier: ^5.4.1 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": "@vitejs/plugin-react":
specifier: ^4.3.1 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: adm-zip:
specifier: ^0.5.14 specifier: ^0.5.14
version: 0.5.14 version: 0.5.14
@@ -186,14 +192,14 @@ importers:
specifier: ^5.5.3 specifier: ^5.5.3
version: 5.5.3 version: 5.5.3
vite: vite:
specifier: ^5.3.2 specifier: ^5.3.3
version: 5.3.2(@types/node@20.14.9)(sass@1.77.6)(terser@5.31.1) version: 5.3.3(@types/node@20.14.10)(sass@1.77.6)(terser@5.31.1)
vite-plugin-monaco-editor: vite-plugin-monaco-editor:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0(monaco-editor@0.49.0) version: 1.1.0(monaco-editor@0.49.0)
vite-plugin-svgr: vite-plugin-svgr:
specifier: ^4.2.0 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: packages:
"@actions/github@5.1.1": "@actions/github@5.1.1":
@@ -1574,16 +1580,16 @@ packages:
"@types/react": "@types/react":
optional: true optional: true
"@mui/core-downloads-tracker@5.15.21": "@mui/core-downloads-tracker@5.16.0":
resolution: 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: resolution:
{ {
integrity: sha512-yqkq1MbdkmX5ZHyvZTBuAaA6RkvoqkoAgwBSx9Oh0L0jAfj9T/Ih/NhMNjkl8PWVSonjfDUkKroBnjRyo/1M9Q==, integrity: sha512-6ISoOhkp9w5gD0PEW9JklrcbyARDkFWNTBdwXZ1Oy5IGlyu9B0zG0hnUIe4H17IaF1Vgj6C8VI+v4tkSdK0veg==,
} }
engines: { node: ">=12.0.0" } engines: { node: ">=12.0.0" }
peerDependencies: peerDependencies:
@@ -1615,10 +1621,10 @@ packages:
"@types/react": "@types/react":
optional: true optional: true
"@mui/material@5.15.21": "@mui/material@5.16.0":
resolution: resolution:
{ {
integrity: sha512-nTyCcgduKwHqiuQ/B03EQUa+utSMzn2sQp0QAibsnYe4tvc3zkMbO0amKpl48vhABIY3IvT6w9615BFIgMt0YA==, integrity: sha512-DbR1NckTLpjt9Zut9EGQ70th86HfN0BYQgyYro6aXQrNfjzSwe3BJS1AyBQ5mJ7TdL6YVRqohfukxj9JlqZZUg==,
} }
engines: { node: ">=12.0.0" } engines: { node: ">=12.0.0" }
peerDependencies: peerDependencies:
@@ -1635,10 +1641,10 @@ packages:
"@types/react": "@types/react":
optional: true optional: true
"@mui/private-theming@5.15.20": "@mui/private-theming@5.16.0":
resolution: resolution:
{ {
integrity: sha512-BK8F94AIqSrnaPYXf2KAOjGZJgWfvqAVQ2gVR3EryvQFtuBnG6RwodxrCvd3B48VuMy6Wsk897+lQMUxJyk+6g==, integrity: sha512-sYpubkO1MZOnxNyVOClrPNOTs0MfuRVVnAvCeMaOaXt6GimgQbnUcshYv2pSr6PFj+Mqzdff/FYOBceK8u5QgA==,
} }
engines: { node: ">=12.0.0" } engines: { node: ">=12.0.0" }
peerDependencies: peerDependencies:
@@ -1664,10 +1670,10 @@ packages:
"@emotion/styled": "@emotion/styled":
optional: true optional: true
"@mui/system@5.15.20": "@mui/system@5.16.0":
resolution: resolution:
{ {
integrity: sha512-LoMq4IlAAhxzL2VNUDBTQxAb4chnBe8JvRINVNDiMtHE2PiPOoHlhOPutSxEbaL5mkECPVWSv6p8JEV+uykwIA==, integrity: sha512-9YbkC2m3+pNumAvubYv+ijLtog6puJ0fJ6rYfzfLCM47pWrw3m+30nXNM8zMgDaKL6vpfWJcCXm+LPaWBpy7sw==,
} }
engines: { node: ">=12.0.0" } engines: { node: ">=12.0.0" }
peerDependencies: peerDependencies:
@@ -1694,10 +1700,10 @@ packages:
"@types/react": "@types/react":
optional: true optional: true
"@mui/utils@5.15.20": "@mui/utils@5.16.0":
resolution: resolution:
{ {
integrity: sha512-mAbYx0sovrnpAu1zHc3MDIhPqL8RPVC5W5xcO1b7PiSCJPtckIZmBkp8hefamAvUiAV8gpfMOM6Zb+eSisbI2A==, integrity: sha512-kLLi5J1xY+mwtUlMb8Ubdxf4qFAA1+U7WPBvjM/qQ4CIwLCohNb0sHo1oYPufjSIH/Z9+dhVxD7dJlfGjd1AVA==,
} }
engines: { node: ">=12.0.0" } engines: { node: ">=12.0.0" }
peerDependencies: peerDependencies:
@@ -1707,10 +1713,10 @@ packages:
"@types/react": "@types/react":
optional: true optional: true
"@mui/x-data-grid@7.8.0": "@mui/x-data-grid@7.9.0":
resolution: resolution:
{ {
integrity: sha512-X3t6EVSZ28vVKY9NfqKcClchw2o/KmHsywybp1tNFevIJiwjZSp7NDJ091GyTqMgyDt1Dy5z2hGxoTDUYYfeGg==, integrity: sha512-RkrVD+tfcR/h3j2p2uqohxA00C5tCJIV5gb5+2ap8XdM0Y8XMF81bB8UADWenU5W83UTErWvtU7n4gCl7hJO9g==,
} }
engines: { node: ">=14.0.0" } engines: { node: ">=14.0.0" }
peerDependencies: peerDependencies:
@@ -1718,6 +1724,15 @@ packages:
react: ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0
react-dom: ^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": "@mui/x-tree-view@6.0.0-alpha.1":
resolution: resolution:
{ {
@@ -1797,16 +1812,23 @@ packages:
integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==, integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==,
} }
"@peggyjs/from-mem@1.3.0":
resolution:
{
integrity: sha512-kzGoIRJjkg3KuGI4bopz9UvF3KguzfxalHRDEIdqEZUe45xezsQ6cx30e0RKuxPUexojQRBfu89Okn7f4/QXsw==,
}
engines: { node: ">=18" }
"@popperjs/core@2.11.8": "@popperjs/core@2.11.8":
resolution: resolution:
{ {
integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==, integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==,
} }
"@remix-run/router@1.17.0": "@remix-run/router@1.17.1":
resolution: resolution:
{ {
integrity: sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==, integrity: sha512-mCOMec4BKd6BRGBZeSnGiIgwsbLGp3yhVqAD8H+PxiRNEHgDpZb8J1TnrSDlg97t0ySKMQJTHCWBCmBpSmkF6Q==,
} }
engines: { node: ">=14.0.0" } engines: { node: ">=14.0.0" }
@@ -2255,10 +2277,10 @@ packages:
integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==, integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==,
} }
"@types/node@20.14.9": "@types/node@20.14.10":
resolution: resolution:
{ {
integrity: sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==, integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==,
} }
"@types/parse-json@4.0.2": "@types/parse-json@4.0.2":
@@ -2328,10 +2350,10 @@ packages:
peerDependencies: peerDependencies:
vite: ^4.2.0 || ^5.0.0 vite: ^4.2.0 || ^5.0.0
acorn@8.12.0: acorn@8.12.1:
resolution: resolution:
{ {
integrity: sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==, integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==,
} }
engines: { node: ">=0.4.0" } engines: { node: ">=0.4.0" }
hasBin: true hasBin: true
@@ -2486,10 +2508,10 @@ packages:
} }
engines: { node: ">=10" } engines: { node: ">=10" }
caniuse-lite@1.0.30001639: caniuse-lite@1.0.30001640:
resolution: resolution:
{ {
integrity: sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg==, integrity: sha512-lA4VMpW0PSUrFnkmVuEKBUovSWKhj7puyCg8StBChgu298N1AtuF1sKWEvfDuimSEDbhlb/KqPKC3fs1HbuQUA==,
} }
ccount@2.0.1: ccount@2.0.1:
@@ -2581,6 +2603,13 @@ packages:
integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==, integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==,
} }
commander@12.1.0:
resolution:
{
integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==,
}
engines: { node: ">=18" }
commander@2.20.3: commander@2.20.3:
resolution: resolution:
{ {
@@ -2720,10 +2749,10 @@ packages:
integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==, integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==,
} }
electron-to-chromium@1.4.816: electron-to-chromium@1.4.818:
resolution: resolution:
{ {
integrity: sha512-EKH5X5oqC6hLmiS7/vYtZHZFTNdhsYG5NVPRN6Yn0kQHNBlT59+xSM8HBy66P5fxWpKgZbPqb+diC64ng295Jw==, integrity: sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==,
} }
end-of-stream@1.4.4: end-of-stream@1.4.4:
@@ -3128,6 +3157,12 @@ packages:
integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==,
} }
js-base64@3.7.7:
resolution:
{
integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==,
}
js-cookie@2.2.1: js-cookie@2.2.1:
resolution: resolution:
{ {
@@ -3244,6 +3279,13 @@ packages:
integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, 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: magic-string@0.30.10:
resolution: resolution:
{ {
@@ -3708,6 +3750,14 @@ packages:
} }
engines: { node: ">=8" } engines: { node: ">=8" }
peggy@4.0.3:
resolution:
{
integrity: sha512-v7/Pt6kGYsfXsCrfb52q7/yg5jaAwiVaUMAPLPvy4DJJU6Wwr72t6nDIqIDkGfzd1B4zeVuTnQT0RGeOhe/uSA==,
}
engines: { node: ">=18" }
hasBin: true
picocolors@1.0.1: picocolors@1.0.1:
resolution: resolution:
{ {
@@ -3863,20 +3913,20 @@ packages:
} }
engines: { node: ">=0.10.0" } engines: { node: ">=0.10.0" }
react-router-dom@6.24.0: react-router-dom@6.24.1:
resolution: resolution:
{ {
integrity: sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==, integrity: sha512-U19KtXqooqw967Vw0Qcn5cOvrX5Ejo9ORmOtJMzYWtCT4/WOfFLIZGGsVLxcd9UkBO0mSTZtXqhZBsWlHr7+Sg==,
} }
engines: { node: ">=14.0.0" } engines: { node: ">=14.0.0" }
peerDependencies: peerDependencies:
react: ">=16.8" react: ">=16.8"
react-dom: ">=16.8" react-dom: ">=16.8"
react-router@6.24.0: react-router@6.24.1:
resolution: resolution:
{ {
integrity: sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==, integrity: sha512-PTXFXGK2pyXpHzVo3rR9H7ip4lSPZZc0bHG5CARmj65fTT6qG7sTngmb6lcYu1gf3y/8KxORoy9yn59pGpCnpg==,
} }
engines: { node: ">=14.0.0" } engines: { node: ">=14.0.0" }
peerDependencies: peerDependencies:
@@ -4028,6 +4078,14 @@ packages:
} }
hasBin: true 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: server-only@0.0.1:
resolution: resolution:
{ {
@@ -4066,6 +4124,13 @@ packages:
integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==, integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==,
} }
source-map-generator@0.8.0:
resolution:
{
integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==,
}
engines: { node: ">= 10" }
source-map-js@1.2.0: source-map-js@1.2.0:
resolution: resolution:
{ {
@@ -4328,10 +4393,10 @@ packages:
} }
engines: { node: ">= 10.0.0" } engines: { node: ">= 10.0.0" }
update-browserslist-db@1.0.16: update-browserslist-db@1.1.0:
resolution: resolution:
{ {
integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==, integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==,
} }
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -4373,10 +4438,10 @@ packages:
peerDependencies: peerDependencies:
vite: ^2.6.0 || 3 || 4 || 5 vite: ^2.6.0 || 3 || 4 || 5
vite@5.3.2: vite@5.3.3:
resolution: resolution:
{ {
integrity: sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==, integrity: sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==,
} }
engines: { node: ^18.0.0 || >=20.0.0 } engines: { node: ^18.0.0 || >=20.0.0 }
hasBin: true hasBin: true
@@ -5531,7 +5596,7 @@ snapshots:
"@babel/runtime": 7.24.7 "@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) "@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/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 "@popperjs/core": 2.11.8
clsx: 2.1.1 clsx: 2.1.1
prop-types: 15.8.1 prop-types: 15.8.1
@@ -5545,7 +5610,7 @@ snapshots:
"@babel/runtime": 7.24.7 "@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) "@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/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 "@popperjs/core": 2.11.8
clsx: 2.1.1 clsx: 2.1.1
prop-types: 15.8.1 prop-types: 15.8.1
@@ -5554,25 +5619,25 @@ snapshots:
optionalDependencies: optionalDependencies:
"@types/react": 18.3.3 "@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: dependencies:
"@babel/runtime": 7.24.7 "@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 react: 18.3.1
optionalDependencies: optionalDependencies:
"@types/react": 18.3.3 "@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: dependencies:
"@babel/runtime": 7.24.7 "@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/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/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.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)
"@mui/types": 7.2.14(@types/react@18.3.3) "@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)
"@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-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 clsx: 2.1.1
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.3.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) "@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 "@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: dependencies:
"@babel/runtime": 7.24.7 "@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/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/core-downloads-tracker": 5.16.0
"@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)
"@mui/types": 7.2.14(@types/react@18.3.3) "@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 "@types/react-transition-group": 4.4.10
clsx: 2.1.1 clsx: 2.1.1
csstype: 3.1.3 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) "@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 "@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: dependencies:
"@babel/runtime": 7.24.7 "@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 prop-types: 15.8.1
react: 18.3.1 react: 18.3.1
optionalDependencies: optionalDependencies:
@@ -5623,13 +5688,13 @@ snapshots:
"@emotion/react": 11.11.4(@types/react@18.3.3)(react@18.3.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) "@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: dependencies:
"@babel/runtime": 7.24.7 "@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/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/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 clsx: 2.1.1
csstype: 3.1.3 csstype: 3.1.3
prop-types: 15.8.1 prop-types: 15.8.1
@@ -5643,7 +5708,7 @@ snapshots:
optionalDependencies: optionalDependencies:
"@types/react": 18.3.3 "@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: dependencies:
"@babel/runtime": 7.24.7 "@babel/runtime": 7.24.7
"@types/prop-types": 15.7.12 "@types/prop-types": 15.7.12
@@ -5653,12 +5718,13 @@ snapshots:
optionalDependencies: optionalDependencies:
"@types/react": 18.3.3 "@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: dependencies:
"@babel/runtime": 7.24.7 "@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)
"@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)
"@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)
"@mui/x-internals": 7.9.0(@types/react@18.3.3)(react@18.3.1)
clsx: 2.1.1 clsx: 2.1.1
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.3.1 react: 18.3.1
@@ -5669,15 +5735,23 @@ snapshots:
- "@emotion/styled" - "@emotion/styled"
- "@types/react" - "@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: dependencies:
"@babel/runtime": 7.24.7 "@babel/runtime": 7.24.7
"@emotion/react": 11.11.4(@types/react@18.3.3)(react@18.3.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) "@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/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/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.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)
"@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 "@types/react-transition-group": 4.4.10
clsx: 2.1.1 clsx: 2.1.1
prop-types: 15.8.1 prop-types: 15.8.1
@@ -5751,9 +5825,13 @@ snapshots:
dependencies: dependencies:
"@octokit/openapi-types": 12.11.0 "@octokit/openapi-types": 12.11.0
"@peggyjs/from-mem@1.3.0":
dependencies:
semver: 7.6.0
"@popperjs/core@2.11.8": {} "@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)": "@rollup/pluginutils@5.1.0(rollup@4.18.0)":
dependencies: dependencies:
@@ -5959,7 +6037,7 @@ snapshots:
"@types/fs-extra@9.0.13": "@types/fs-extra@9.0.13":
dependencies: dependencies:
"@types/node": 20.14.9 "@types/node": 20.14.10
"@types/hast@3.0.4": "@types/hast@3.0.4":
dependencies: dependencies:
@@ -5983,7 +6061,7 @@ snapshots:
"@types/ms@0.7.34": {} "@types/ms@0.7.34": {}
"@types/node@20.14.9": "@types/node@20.14.10":
dependencies: dependencies:
undici-types: 5.26.5 undici-types: 5.26.5
@@ -6010,7 +6088,7 @@ snapshots:
"@ungap/structured-clone@1.2.0": {} "@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: dependencies:
"@babel/core": 7.24.7 "@babel/core": 7.24.7
"@babel/preset-env": 7.24.7(@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 regenerator-runtime: 0.14.1
systemjs: 6.15.1 systemjs: 6.15.1
terser: 5.31.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: transitivePeerDependencies:
- supports-color - 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: dependencies:
"@babel/core": 7.24.7 "@babel/core": 7.24.7
"@babel/plugin-transform-react-jsx-self": 7.24.7(@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) "@babel/plugin-transform-react-jsx-source": 7.24.7(@babel/core@7.24.7)
"@types/babel__core": 7.20.5 "@types/babel__core": 7.20.5
react-refresh: 0.14.2 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: transitivePeerDependencies:
- supports-color - supports-color
acorn@8.12.0: {} acorn@8.12.1: {}
adm-zip@0.5.14: {} adm-zip@0.5.14: {}
@@ -6127,10 +6205,10 @@ snapshots:
browserslist@4.23.1: browserslist@4.23.1:
dependencies: dependencies:
caniuse-lite: 1.0.30001639 caniuse-lite: 1.0.30001640
electron-to-chromium: 1.4.816 electron-to-chromium: 1.4.818
node-releases: 2.0.14 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: {} buffer-from@1.1.2: {}
@@ -6138,7 +6216,7 @@ snapshots:
camelcase@6.3.0: {} camelcase@6.3.0: {}
caniuse-lite@1.0.30001639: {} caniuse-lite@1.0.30001640: {}
ccount@2.0.1: {} ccount@2.0.1: {}
@@ -6186,6 +6264,8 @@ snapshots:
comma-separated-tokens@2.0.3: {} comma-separated-tokens@2.0.3: {}
commander@12.1.0: {}
commander@2.20.3: {} commander@2.20.3: {}
convert-source-map@1.9.0: {} convert-source-map@1.9.0: {}
@@ -6259,7 +6339,7 @@ snapshots:
no-case: 3.0.4 no-case: 3.0.4
tslib: 2.6.3 tslib: 2.6.3
electron-to-chromium@1.4.816: {} electron-to-chromium@1.4.818: {}
end-of-stream@1.4.4: end-of-stream@1.4.4:
dependencies: dependencies:
@@ -6492,6 +6572,8 @@ snapshots:
isexe@2.0.0: {} isexe@2.0.0: {}
js-base64@3.7.7: {}
js-cookie@2.2.1: {} js-cookie@2.2.1: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
@@ -6542,6 +6624,10 @@ snapshots:
dependencies: dependencies:
yallist: 3.1.1 yallist: 3.1.1
lru-cache@6.0.0:
dependencies:
yallist: 4.0.0
magic-string@0.30.10: magic-string@0.30.10:
dependencies: dependencies:
"@jridgewell/sourcemap-codec": 1.4.15 "@jridgewell/sourcemap-codec": 1.4.15
@@ -6914,6 +7000,12 @@ snapshots:
path-type@4.0.0: {} 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: {} picocolors@1.0.1: {}
picomatch@2.3.1: {} picomatch@2.3.1: {}
@@ -7010,16 +7102,16 @@ snapshots:
react-refresh@0.14.2: {} 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: dependencies:
"@remix-run/router": 1.17.0 "@remix-run/router": 1.17.1
react: 18.3.1 react: 18.3.1
react-dom: 18.3.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: dependencies:
"@remix-run/router": 1.17.0 "@remix-run/router": 1.17.1
react: 18.3.1 react: 18.3.1
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.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@6.3.1: {}
semver@7.6.0:
dependencies:
lru-cache: 6.0.0
server-only@0.0.1: {} server-only@0.0.1: {}
shebang-command@2.0.0: shebang-command@2.0.0:
@@ -7151,6 +7247,8 @@ snapshots:
sockette@2.0.6: {} sockette@2.0.6: {}
source-map-generator@0.8.0: {}
source-map-js@1.2.0: {} source-map-js@1.2.0: {}
source-map-support@0.5.21: source-map-support@0.5.21:
@@ -7205,7 +7303,7 @@ snapshots:
terser@5.31.1: terser@5.31.1:
dependencies: dependencies:
"@jridgewell/source-map": 0.3.6 "@jridgewell/source-map": 0.3.6
acorn: 8.12.0 acorn: 8.12.1
commander: 2.20.3 commander: 2.20.3
source-map-support: 0.5.21 source-map-support: 0.5.21
@@ -7288,7 +7386,7 @@ snapshots:
universalify@2.0.1: {} 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: dependencies:
browserslist: 4.23.1 browserslist: 4.23.1
escalade: 3.1.2 escalade: 3.1.2
@@ -7313,24 +7411,24 @@ snapshots:
dependencies: dependencies:
monaco-editor: 0.49.0 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: dependencies:
"@rollup/pluginutils": 5.1.0(rollup@4.18.0) "@rollup/pluginutils": 5.1.0(rollup@4.18.0)
"@svgr/core": 8.1.0(typescript@5.5.3) "@svgr/core": 8.1.0(typescript@5.5.3)
"@svgr/plugin-jsx": 8.1.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: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
- typescript - 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: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.4.39 postcss: 8.4.39
rollup: 4.18.0 rollup: 4.18.0
optionalDependencies: optionalDependencies:
"@types/node": 20.14.9 "@types/node": 20.14.10
fsevents: 2.3.3 fsevents: 2.3.3
sass: 1.77.6 sass: 1.77.6
terser: 5.31.1 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 = () => const resolveMmdb = () =>
resolveResource({ resolveResource({
file: "Country.mmdb", file: "Country.mmdb",
@@ -449,8 +439,6 @@ const tasks = [
{ name: "service", func: resolveService, retry: 5 }, { name: "service", func: resolveService, retry: 5 },
{ name: "install", func: resolveInstall, retry: 5 }, { name: "install", func: resolveInstall, retry: 5 },
{ name: "uninstall", func: resolveUninstall, 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: "mmdb", func: resolveMmdb, retry: 5 },
{ name: "geosite", func: resolveGeosite, retry: 5 }, { name: "geosite", func: resolveGeosite, retry: 5 },
{ name: "geoip", func: resolveGeoIP, retry: 5 }, { name: "geoip", func: resolveGeoIP, retry: 5 },
+2 -2
View File
@@ -72,12 +72,12 @@ async function resolveUpdater() {
} }
// win32 url // win32 url
if (name.endsWith("x64-setup.nsis.zip")) { if (name.endsWith("x86-setup.nsis.zip")) {
updateData.platforms["windows-x86"].url = browser_download_url; updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url; updateData.platforms["windows-i686"].url = browser_download_url;
} }
// win32 signature // win32 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) { if (name.endsWith("x86-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig; updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig; updateData.platforms["windows-i686"].signature = sig;
+273 -230
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "1.7.2" version = "1.7.7"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -38,6 +38,7 @@ serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] } reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" } 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"] } 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] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
deelevate = "0.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

+41 -4
View File
@@ -6,12 +6,19 @@ use crate::{
}; };
use crate::{ret_err, wrap_err}; use crate::{ret_err, wrap_err};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use network_interface::NetworkInterface;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use sysproxy::{Autoproxy, Sysproxy}; use sysproxy::{Autoproxy, Sysproxy};
use tauri::{api, Manager}; use tauri::{api, Manager};
type CmdResult<T = ()> = Result<T, String>; 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] #[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> { pub fn get_profiles() -> CmdResult<IProfiles> {
Ok(Config::profiles().data().clone()) 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] #[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) { pub fn open_devtools(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_window("main") { if let Some(window) = app_handle.get_window("main") {
@@ -352,13 +389,13 @@ pub mod service {
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult { pub async fn install_service(passwd: String) -> CmdResult {
wrap_err!(service::install_service().await) wrap_err!(service::install_service(passwd).await)
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult { pub async fn uninstall_service(passwd: String) -> CmdResult {
wrap_err!(service::uninstall_service().await) wrap_err!(service::uninstall_service(passwd).await)
} }
} }
+17 -4
View File
@@ -111,6 +111,7 @@ impl IProfiles {
if item.uid.is_none() { if item.uid.is_none() {
bail!("the uid should not be null"); bail!("the uid should not be null");
} }
let uid = item.uid.clone();
// save the file data // save the file data
// move the field value after save // move the field value after save
@@ -128,6 +129,12 @@ impl IProfiles {
.with_context(|| format!("failed to write to file \"{}\"", file))?; .with_context(|| format!("failed to write to file \"{}\"", file))?;
} }
if self.current.is_none()
&& (item.itype == Some("remote".to_string()) || item.itype == Some("local".to_string()))
{
self.current = uid;
}
if self.items.is_none() { if self.items.is_none() {
self.items = Some(vec![]); self.items = Some(vec![]);
} }
@@ -135,6 +142,7 @@ impl IProfiles {
if let Some(items) = self.items.as_mut() { if let Some(items) = self.items.as_mut() {
items.push(item) items.push(item)
} }
self.save_file() self.save_file()
} }
@@ -355,10 +363,15 @@ impl IProfiles {
} }
// delete the original uid // delete the original uid
if current == uid { if current == uid {
self.current = match !items.is_empty() { self.current = None;
true => items[0].uid.clone(), for item in items.iter() {
false => None, if item.itype == Some("remote".to_string())
}; || item.itype == Some("local".to_string())
{
self.current = item.uid.clone();
break;
}
}
} }
self.items = Some(items); self.items = Some(items);
+4 -4
View File
@@ -227,16 +227,16 @@ impl IVerge {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
verge_redir_port: Some(7895), verge_redir_port: Some(7895),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
verge_redir_enabled: Some(true), verge_redir_enabled: Some(false),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
verge_tproxy_port: Some(7896), verge_tproxy_port: Some(7896),
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
verge_tproxy_enabled: Some(true), verge_tproxy_enabled: Some(false),
verge_mixed_port: Some(7897), verge_mixed_port: Some(7897),
verge_socks_port: Some(7898), verge_socks_port: Some(7898),
verge_socks_enabled: Some(true), verge_socks_enabled: Some(false),
verge_port: Some(7899), verge_port: Some(7899),
verge_http_enabled: Some(true), verge_http_enabled: Some(false),
enable_proxy_guard: Some(false), enable_proxy_guard: Some(false),
use_default_bypass: Some(true), use_default_bypass: Some(true),
proxy_guard_duration: Some(30), proxy_guard_duration: Some(30),
+34 -31
View File
@@ -7,7 +7,7 @@ use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use sysinfo::System; use sysinfo::{ProcessRefreshKind, RefreshKind, System};
use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep; use tokio::time::sleep;
@@ -44,13 +44,27 @@ impl CoreManager {
let config_path = dirs::path_to_str(&config_path)?; let config_path = dirs::path_to_str(&config_path)?;
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into()); let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
let app_dir = dirs::app_home_dir()?; // compatibility
let app_dir = dirs::path_to_str(&app_dir)?; 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)? let output = Command::new_sidecar(clash_core)?
.args(["-t", "-d", app_dir, "-f", config_path]) .args(["-t", "-d", test_dir, "-f", config_path])
.output()?; .output()?;
if !output.status.success() { if !output.status.success() {
@@ -70,12 +84,6 @@ impl CoreManager {
pub async fn run_core(&self) -> Result<()> { pub async fn run_core(&self) -> Result<()> {
let config_path = Config::generate_file(ConfigType::Run)?; 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模式 // 关闭tun模式
let mut disable = Mapping::new(); let mut disable = Mapping::new();
let mut tun = Mapping::new(); let mut tun = Mapping::new();
@@ -84,23 +92,19 @@ impl CoreManager {
log::debug!(target: "app", "disable tun mode"); log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await; 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() { if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service"); log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await); 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");
// 这里得等一会儿 for proc in procs {
if should_kill { log::debug!(target: "app", "kill all clash process");
sleep(Duration::from_millis(500)).await; proc.kill();
}
} }
// 服务模式 // 服务模式
@@ -237,8 +241,9 @@ impl CoreManager {
let mut sidecar = self.sidecar.lock(); let mut sidecar = self.sidecar.lock();
let _ = sidecar.take(); let _ = sidecar.take();
let mut system = System::new(); let system = System::new_with_specifics(
system.refresh_all(); RefreshKind::new().with_processes(ProcessRefreshKind::everything()),
);
let procs = system.processes_by_name("verge-mihomo"); let procs = system.processes_by_name("verge-mihomo");
for proc in procs { for proc in procs {
log::debug!(target: "app", "kill all clash process"); log::debug!(target: "app", "kill all clash process");
@@ -287,7 +292,6 @@ impl CoreManager {
/// 如果涉及端口和外部控制则需要重启 /// 如果涉及端口和外部控制则需要重启
pub async fn update_config(&self) -> Result<()> { pub async fn update_config(&self) -> Result<()> {
log::debug!(target: "app", "try to update clash config"); log::debug!(target: "app", "try to update clash config");
// 更新订阅 // 更新订阅
Config::generate().await?; Config::generate().await?;
@@ -299,20 +303,19 @@ impl CoreManager {
let path = dirs::path_to_str(&path)?; let path = dirs::path_to_str(&path)?;
// 发送请求 发送5次 // 发送请求 发送5次
for i in 0..5 { for i in 0..10 {
match clash_api::put_configs(path).await { match clash_api::put_configs(path).await {
Ok(_) => break, Ok(_) => break,
Err(err) => { Err(err) => {
if i < 4 { if i < 9 {
log::info!(target: "app", "{err}"); log::info!(target: "app", "{err}");
} else { } else {
bail!(err); bail!(err);
} }
} }
} }
sleep(Duration::from_millis(250)).await; sleep(Duration::from_millis(100)).await;
} }
Ok(()) Ok(())
} }
} }
+94 -47
View File
@@ -28,11 +28,19 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>, pub data: Option<ResponseBody>,
} }
#[cfg(not(target_os = "windows"))]
pub fn sudo(passwd: &String, cmd: String) -> StdCommand {
let shell = format!("echo \"{}\" | sudo -S {}", passwd, cmd);
let mut command = StdCommand::new("bash");
command.arg("-c").arg(shell);
command
}
/// Install the Clash Verge Service /// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
/// ///
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> { pub async fn install_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@@ -65,30 +73,45 @@ pub async fn install_service() -> Result<()> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> { pub async fn install_service(passwd: String) -> Result<()> {
use users::get_effective_uid; use users::get_effective_uid;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service"); let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() { if !installer_path.exists() {
bail!("installer not found"); bail!("installer not found");
} }
let elevator = crate::utils::unix_helper::linux_elevator(); let output = match get_effective_uid() {
let status = match get_effective_uid() { 0 => {
0 => StdCommand::new(installer_path).status()?, StdCommand::new("chmod")
_ => StdCommand::new(elevator) .arg("+x")
.arg("sh") .arg(installer_path.clone())
.arg("-c") .output()?;
.arg(installer_path) StdCommand::new("chmod")
.status()?, .arg("+x")
.arg(binary_path)
.output()?;
StdCommand::new(installer_path.clone()).output()?
}
_ => {
sudo(
&passwd,
format!("chmod +x {}", installer_path.to_string_lossy()),
)
.output()?;
sudo(
&passwd,
format!("chmod +x {}", binary_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", installer_path.to_string_lossy())).output()?
}
}; };
if !output.status.success() {
if !status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@@ -96,24 +119,32 @@ pub async fn install_service() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> { pub async fn install_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service"); let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() { if !installer_path.exists() {
bail!("installer not found"); bail!("installer not found");
} }
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript") sudo(
.args(vec!["-e", &command]) &passwd,
.status()?; format!(
"chmod +x {}",
installer_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", installer_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
if !status.success() { if !output.status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@@ -122,7 +153,7 @@ pub async fn install_service() -> Result<()> {
/// Uninstall the Clash Verge Service /// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@@ -155,7 +186,7 @@ pub async fn uninstall_service() -> Result<()> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(passwd: String) -> Result<()> {
use users::get_effective_uid; use users::get_effective_uid;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
@@ -165,20 +196,29 @@ pub async fn uninstall_service() -> Result<()> {
bail!("uninstaller not found"); bail!("uninstaller not found");
} }
let elevator = crate::utils::unix_helper::linux_elevator(); let output = match get_effective_uid() {
let status = match get_effective_uid() { 0 => {
0 => StdCommand::new(uninstaller_path).status()?, StdCommand::new("chmod")
_ => StdCommand::new(elevator) .arg("+x")
.arg("sh") .arg(uninstaller_path.clone())
.arg("-c") .output()?;
.arg(uninstaller_path) StdCommand::new(uninstaller_path.clone()).output()?
.status()?, }
_ => {
sudo(
&passwd,
format!("chmod +x {}", uninstaller_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", uninstaller_path.to_string_lossy())).output()?
}
}; };
if !status.success() { if !output.status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@@ -186,7 +226,7 @@ pub async fn uninstall_service() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service"); let uninstaller_path = binary_path.with_file_name("uninstall-service");
@@ -194,17 +234,24 @@ pub async fn uninstall_service() -> Result<()> {
bail!("uninstaller not found"); bail!("uninstaller not found");
} }
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ "); sudo(
let command = format!(r#"do shell script "{shell}" with administrator privileges"#); &passwd,
format!(
"chmod +x {}",
uninstaller_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", uninstaller_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
let status = StdCommand::new("osascript") if !output.status.success() {
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to uninstall service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
+3 -3
View File
@@ -297,11 +297,11 @@ impl Tray {
}; };
let _ = tray.set_tooltip(&format!( let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}", "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"), t!("SysProxy", "系统代理"),
switch_map[system_proxy], switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"), t!("TUN", "Tun模式"),
switch_map[tun_mode], switch_map[tun_mode],
t!("Curent Profile", "当前订阅"), t!("Profile", "当前订阅"),
current_profile_name current_profile_name
)); ));
+1 -1
View File
@@ -1,4 +1,4 @@
function main(config) { function main(config, _name) {
if (config.mode === "script") { if (config.mode === "script") {
config.mode = "rule"; config.mode = "rule";
} }
@@ -1,4 +1,4 @@
function main(config) { function main(config, _name) {
if (Array.isArray(config.proxies)) { if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => { config.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") { 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.clash_core.clone(),
verge.enable_tun_mode.unwrap_or(false), verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true), verge.enable_builtin_enhanced.unwrap_or(true),
verge.verge_socks_enabled.unwrap_or(true), verge.verge_socks_enabled.unwrap_or(false),
verge.verge_http_enabled.unwrap_or(true), verge.verge_http_enabled.unwrap_or(false),
) )
}; };
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let redir_enabled = { let redir_enabled = {
let verge = Config::verge(); let verge = Config::verge();
let verge = verge.latest(); let verge = verge.latest();
verge.verge_redir_enabled.unwrap_or(true) verge.verge_redir_enabled.unwrap_or(false)
}; };
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let tproxy_enabled = { let tproxy_enabled = {
let verge = Config::verge(); let verge = Config::verge();
let verge = verge.latest(); let verge = verge.latest();
verge.verge_tproxy_enabled.unwrap_or(true) verge.verge_tproxy_enabled.unwrap_or(false)
}; };
// 从profiles里拿东西 // 从profiles里拿东西
@@ -59,6 +59,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups_item, groups_item,
global_merge, global_merge,
global_script, global_script,
profile_name,
) = { ) = {
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles = profiles.latest(); 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()), 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, current,
merge, merge,
@@ -132,6 +139,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
groups, groups,
global_merge, global_merge,
global_script, 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 { if let ChainType::Script(script) = global_script.data {
let mut logs = vec![]; 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)) => { Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config)); exists_keys.extend(use_keys(&res_config));
config = 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 { if let ChainType::Script(script) = script_item.data {
let mut logs = vec![]; 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)) => { Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config)); exists_keys.extend(use_keys(&res_config));
config = res_config; config = res_config;
@@ -239,7 +247,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.for_each(|item| { .for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid); log::debug!(target: "app", "run builtin script {}", item.uid);
if let ChainType::Script(script) = item.data { 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, _)) => { Ok((res_config, _)) => {
config = res_config; config = res_config;
} }
+7 -3
View File
@@ -2,7 +2,11 @@ use super::use_lowercase;
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use serde_yaml::Mapping; 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 boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
let mut context = Context::default(); let mut context = Context::default();
@@ -42,7 +46,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
let code = format!( let code = format!(
r#"try{{ r#"try{{
{script}; {script};
JSON.stringify(main({config_str})||'') JSON.stringify(main({config_str},'{name}')||'')
}} catch(err) {{ }} catch(err) {{
`__error_flag__ ${{err.toString()}}` `__error_flag__ ${{err.toString()}}`
}}"# }}"#
@@ -97,7 +101,7 @@ fn test_script() {
"#; "#;
let config = serde_yaml::from_str(config).unwrap(); 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(); 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() val.as_sequence().unwrap_or(&Sequence::default()).clone()
}); });
let mut seq = origin_seq.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(); let mut delete_names = Vec::new();
for item in delete { for item in delete {
let item = item.clone(); 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(); let mut config = config.clone();
config.insert(Value::from(name), Value::from(seq)); config.insert(Value::from(name), Value::from(seq));
return config; 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()); Config::clash().draft().patch_config(patch.clone());
let res = { 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() if patch.get("secret").is_some() || patch.get("external-controller").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()
{
Config::generate().await?; Config::generate().await?;
CoreManager::global().run_core().await?; CoreManager::global().run_core().await?;
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
// 更新系统代理
if mixed_port.is_some() {
log_err!(sysopt::Sysopt::global().init_sysproxy());
}
if patch.get("mode").is_some() { if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part()); 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<()> { pub async fn patch_verge(patch: IVerge) -> Result<()> {
Config::verge().draft().patch_config(patch.clone()); Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode; let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch; let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy; 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 pac_content = patch.pac_file_content;
let proxy_bypass = patch.system_proxy_bypass; let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language; let language = patch.language;
let port = patch.verge_mixed_port; let mixed_port = patch.verge_mixed_port;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon; let tray_icon = patch.tray_icon;
let common_tray_icon = patch.common_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; let tun_tray_icon = patch.tun_tray_icon;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let redir_enabled = patch.verge_redir_enabled; let redir_enabled = patch.verge_redir_enabled;
#[cfg(not(target_os = "windows"))]
let redir_port = patch.verge_redir_port;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let tproxy_enabled = patch.verge_tproxy_enabled; 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_enabled = patch.verge_socks_enabled;
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled; let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let res = { let res = {
let service_mode = patch.enable_service_mode; let service_mode = patch.enable_service_mode;
let mut generated = false;
if service_mode.is_some() { if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap()); log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
if !generated {
Config::generate().await?; Config::generate().await?;
CoreManager::global().run_core().await?; CoreManager::global().run_core().await?;
generated = true;
}
} else if tun_mode.is_some() { } else if tun_mode.is_some() {
update_core_config().await?; update_core_config().await?;
} }
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() { if redir_enabled.is_some() || redir_port.is_some() {
Config::generate().await?; if !generated {
CoreManager::global().run_core().await?; Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if tproxy_enabled.is_some() { if tproxy_enabled.is_some() || tproxy_port.is_some() {
Config::generate().await?; if !generated {
CoreManager::global().run_core().await?; Config::generate().await?;
CoreManager::global().run_core().await?;
generated = true;
}
} }
if socks_enabled.is_some() || http_enabled.is_some() { if socks_enabled.is_some()
Config::generate().await?; || http_enabled.is_some()
CoreManager::global().run_core().await?; || 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() { if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?; sysopt::Sysopt::global().update_launch()?;
} }
if system_proxy.is_some() if system_proxy.is_some()
|| proxy_bypass.is_some() || proxy_bypass.is_some()
|| port.is_some() || mixed_port.is_some()
|| pac.is_some() || pac.is_some()
|| pac_content.is_some() || pac_content.is_some()
{ {
+3
View File
@@ -51,6 +51,7 @@ fn main() -> std::io::Result<()> {
cmds::open_web_url, cmds::open_web_url,
cmds::open_core_dir, cmds::open_core_dir,
cmds::get_portable_flag, cmds::get_portable_flag,
cmds::get_network_interfaces,
// cmds::kill_sidecar, // cmds::kill_sidecar,
cmds::restart_sidecar, cmds::restart_sidecar,
// clash // clash
@@ -63,6 +64,7 @@ fn main() -> std::io::Result<()> {
cmds::get_runtime_exists, cmds::get_runtime_exists,
cmds::get_runtime_logs, cmds::get_runtime_logs,
cmds::uwp::invoke_uwp_tool, cmds::uwp::invoke_uwp_tool,
cmds::copy_clash_env,
// verge // verge
cmds::get_verge_config, cmds::get_verge_config,
cmds::patch_verge_config, cmds::patch_verge_config,
@@ -72,6 +74,7 @@ fn main() -> std::io::Result<()> {
cmds::download_icon_cache, cmds::download_icon_cache,
cmds::open_devtools, cmds::open_devtools,
cmds::exit_app, cmds::exit_app,
cmds::get_network_interfaces_info,
// cmds::update_hotkeys, // cmds::update_hotkeys,
// profile // profile
cmds::get_profiles, cmds::get_profiles,
+13 -5
View File
@@ -191,11 +191,15 @@ pub fn init_config() -> Result<()> {
/// after tauri setup /// after tauri setup
pub fn init_resources() -> Result<()> { pub fn init_resources() -> Result<()> {
let app_dir = dirs::app_home_dir()?; let app_dir = dirs::app_home_dir()?;
let test_dir = app_dir.join("test");
let res_dir = dirs::app_resources_dir()?; let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() { if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir); let _ = fs::create_dir_all(&app_dir);
} }
if !test_dir.exists() {
let _ = fs::create_dir_all(&test_dir);
}
if !res_dir.exists() { if !res_dir.exists() {
let _ = fs::create_dir_all(&res_dir); let _ = fs::create_dir_all(&res_dir);
} }
@@ -210,9 +214,10 @@ pub fn init_resources() -> Result<()> {
for file in file_list.iter() { for file in file_list.iter() {
let src_path = res_dir.join(file); let src_path = res_dir.join(file);
let dest_path = app_dir.join(file); let dest_path = app_dir.join(file);
let test_dest_path = test_dir.join(file);
let handle_copy = || { let handle_copy = |dest: &PathBuf| {
match fs::copy(&src_path, &dest_path) { match fs::copy(&src_path, dest) {
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"), Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
Err(err) => { Err(err) => {
log::error!(target: "app", "failed to copy resources '{file}', {err}") log::error!(target: "app", "failed to copy resources '{file}', {err}")
@@ -220,8 +225,11 @@ pub fn init_resources() -> Result<()> {
}; };
}; };
if src_path.exists() && !test_dest_path.exists() {
handle_copy(&test_dest_path);
}
if src_path.exists() && !dest_path.exists() { if src_path.exists() && !dest_path.exists() {
handle_copy(); handle_copy(&dest_path);
continue; continue;
} }
@@ -231,14 +239,14 @@ pub fn init_resources() -> Result<()> {
match (src_modified, dest_modified) { match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => { (Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified { if src_modified > dest_modified {
handle_copy(); handle_copy(&dest_path);
} else { } else {
log::debug!(target: "app", "skipping resource copy '{file}'"); log::debug!(target: "app", "skipping resource copy '{file}'");
} }
} }
_ => { _ => {
log::debug!(target: "app", "failed to get modified '{file}'"); log::debug!(target: "app", "failed to get modified '{file}'");
handle_copy(); handle_copy(&dest_path);
} }
}; };
} }
-1
View File
@@ -4,4 +4,3 @@ pub mod init;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;
pub mod tmpl; pub mod tmpl;
pub mod unix_helper;
+1
View File
@@ -8,6 +8,7 @@ use serde_yaml::Mapping;
use std::net::TcpListener; use std::net::TcpListener;
use tauri::api::notification; use tauri::api::notification;
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
#[cfg(not(target_os = "linux"))]
use window_shadows::set_shadow; use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new(); pub static VERSION: OnceCell<String> = OnceCell::new();
+1 -1
View File
@@ -27,7 +27,7 @@ pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Cla
/// enhanced profile /// enhanced profile
pub const ITEM_SCRIPT: &str = "// Define main function (script entry) pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
function main(config) { function main(config, profileName) {
return config; return config;
} }
"; ";
-14
View File
@@ -1,14 +0,0 @@
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> &'static str {
use std::process::Command;
match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": { "package": {
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "1.7.2" "version": "1.7.7"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
+6 -2
View File
@@ -1,7 +1,11 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { ReactNode, useState, useEffect } from "react"; import { ReactNode, useState, useEffect } from "react";
import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material"; import { Box, IconButton, Slide, Snackbar, Typography } from "@mui/material";
import { Close, CheckCircleRounded, ErrorRounded } from "@mui/icons-material"; import {
CloseRounded,
CheckCircleRounded,
ErrorRounded,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
interface InnerProps { interface InnerProps {
@@ -81,7 +85,7 @@ const NoticeInner = (props: InnerProps) => {
transitionDuration={200} transitionDuration={200}
action={ action={
<IconButton size="small" color="inherit" onClick={onBtnClose}> <IconButton size="small" color="inherit" onClick={onBtnClose}>
<Close fontSize="inherit" /> <CloseRounded fontSize="inherit" />
</IconButton> </IconButton>
} }
/> />
+2 -2
View File
@@ -26,7 +26,7 @@ type SearchProps = {
export const BaseSearchBox = styled((props: SearchProps) => { export const BaseSearchBox = styled((props: SearchProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? true); const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
const [matchWholeWord, setMatchWholeWord] = useState( const [matchWholeWord, setMatchWholeWord] = useState(
props.matchWholeWord ?? false props.matchWholeWord ?? false
); );
@@ -96,7 +96,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
return ( return (
<Tooltip title={errorMessage} placement="bottom-start"> <Tooltip title={errorMessage} placement="bottom-start">
<TextField <TextField
autoComplete="off" autoComplete="new-password"
inputRef={inputRef} inputRef={inputRef}
hiddenLabel hiddenLabel
fullWidth fullWidth
+1 -1
View File
@@ -4,7 +4,7 @@ export const BaseStyledSelect = styled((props: SelectProps<string>) => {
return ( return (
<Select <Select
size="small" size="small"
autoComplete="off" autoComplete="new-password"
sx={{ sx={{
width: 120, width: 120,
height: 33.375, height: 33.375,
@@ -6,7 +6,7 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
return ( return (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
hiddenLabel hiddenLabel
fullWidth fullWidth
size="small" size="small"
+1
View File
@@ -11,6 +11,7 @@ export const Switch = styled((props: SwitchProps) => (
width: 42, width: 42,
height: 26, height: 26,
padding: 0, padding: 0,
marginRight: 1,
"& .MuiSwitch-switchBase": { "& .MuiSwitch-switchBase": {
padding: 0, padding: 0,
margin: 2, margin: 2,
+6 -6
View File
@@ -1,9 +1,9 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { import {
ArrowDownward, ArrowDownwardRounded,
ArrowUpward, ArrowUpwardRounded,
MemoryOutlined, MemoryRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -153,7 +153,7 @@ export const LayoutTraffic = () => {
<Box display="flex" flexDirection="column" gap={0.75}> <Box display="flex" flexDirection="column" gap={0.75}>
<Box title={t("Upload Speed")} {...boxStyle}> <Box title={t("Upload Speed")} {...boxStyle}>
<ArrowUpward <ArrowUpwardRounded
{...iconStyle} {...iconStyle}
color={+up > 0 ? "secondary" : "disabled"} color={+up > 0 ? "secondary" : "disabled"}
/> />
@@ -164,7 +164,7 @@ export const LayoutTraffic = () => {
</Box> </Box>
<Box title={t("Download Speed")} {...boxStyle}> <Box title={t("Download Speed")} {...boxStyle}>
<ArrowDownward <ArrowDownwardRounded
{...iconStyle} {...iconStyle}
color={+down > 0 ? "primary" : "disabled"} color={+down > 0 ? "primary" : "disabled"}
/> />
@@ -184,7 +184,7 @@ export const LayoutTraffic = () => {
isDebug && (await gc()); isDebug && (await gc());
}} }}
> >
<MemoryOutlined {...iconStyle} /> <MemoryRounded {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography> <Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography> <Typography {...unitStyle}>{inuseUnit}</Typography>
</Box> </Box>
+25 -11
View File
@@ -10,14 +10,17 @@ import {
DialogTitle, DialogTitle,
IconButton, IconButton,
} from "@mui/material"; } from "@mui/material";
import FormatPaintIcon from "@mui/icons-material/FormatPaint"; import {
import OpenInFullIcon from "@mui/icons-material/OpenInFull"; FormatPaintRounded,
import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen"; OpenInFullRounded,
CloseFullscreenRounded,
} from "@mui/icons-material";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import debounce from "@/utils/debounce";
import * as monaco from "monaco-editor"; import * as monaco from "monaco-editor";
import MonacoEditor from "react-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(() => { useEffect(() => {
const unlistenResized = appWindow.onResized(() => { const onResized = debounce(() => {
editorResize();
appWindow.isMaximized().then((maximized) => { appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized); setIsMaximized(() => maximized);
}); });
}); }, 100);
const unlistenResized = appWindow.onResized(onResized);
return () => { return () => {
unlistenResized.then((fn) => fn()); 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> <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ width: "auto", height: "calc(100vh - 185px)" }}> <DialogContent
sx={{
width: "auto",
height: "calc(100vh - 185px)",
overflow: "hidden",
}}
>
<MonacoEditor <MonacoEditor
language={language} language={language}
theme={themeMode === "light" ? "vs" : "vs-dark"} theme={themeMode === "light" ? "vs" : "vs-dark"}
@@ -209,17 +225,15 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
?.run() ?.run()
} }
> >
<FormatPaintIcon fontSize="inherit" /> <FormatPaintRounded fontSize="inherit" />
</IconButton> </IconButton>
<IconButton <IconButton
size="medium" size="medium"
color="inherit" color="inherit"
title={t(isMaximized ? "Minimize" : "Maximize")} title={t(isMaximized ? "Minimize" : "Maximize")}
onClick={() => onClick={() => appWindow.toggleMaximize().then(editorResize)}
appWindow.toggleMaximize().then(() => editorRef.current?.layout())
}
> >
{isMaximized ? <CloseFullscreenIcon /> : <OpenInFullIcon />} {isMaximized ? <CloseFullscreenRounded /> : <OpenInFullRounded />}
</IconButton> </IconButton>
</ButtonGroup> </ButtonGroup>
</DialogContent> </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([formIns.getValues(), ...prependSeq]);
} 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, Menu,
CircularProgress, CircularProgress,
} from "@mui/material"; } from "@mui/material";
import { RefreshRounded, DragIndicator } from "@mui/icons-material"; import { RefreshRounded, DragIndicatorRounded } from "@mui/icons-material";
import { useLoadingCache, useSetLoadingCache } from "@/services/states"; import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import { import {
viewProfile, viewProfile,
@@ -24,12 +24,14 @@ import {
saveProfileFile, saveProfileFile,
} from "@/services/cmds"; } from "@/services/cmds";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { GroupsEditorViewer } from "@/components/profile/groups-editor-viewer";
import { RulesEditorViewer } from "@/components/profile/rules-editor-viewer"; import { RulesEditorViewer } from "@/components/profile/rules-editor-viewer";
import { EditorViewer } from "@/components/profile/editor-viewer"; import { EditorViewer } from "@/components/profile/editor-viewer";
import { ProfileBox } from "./profile-box"; import { ProfileBox } from "./profile-box";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { ConfirmViewer } from "@/components/profile/confirm-viewer"; import { ConfirmViewer } from "@/components/profile/confirm-viewer";
import { open } from "@tauri-apps/api/shell"; import { open } from "@tauri-apps/api/shell";
import { ProxiesEditorViewer } from "./proxies-editor-viewer";
const round = keyframes` const round = keyframes`
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
@@ -49,8 +51,14 @@ interface Props {
export const ProfileItem = (props: Props) => { export const ProfileItem = (props: Props) => {
const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } = const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } =
props; props;
const { attributes, listeners, setNodeRef, transform, transition } = const {
useSortable({ id: props.id }); attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.id });
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -71,7 +79,10 @@ export const ProfileItem = (props: Props) => {
const from = parseUrl(itemData.url); const from = parseUrl(itemData.url);
const description = itemData.desc; const description = itemData.desc;
const expire = parseExpire(extra?.expire); 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; const loading = loadingCache[itemData.uid] ?? false;
@@ -295,8 +306,10 @@ export const ProfileItem = (props: Props) => {
return ( return (
<Box <Box
sx={{ sx={{
position: "relative",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
}} }}
> >
<ProfileBox <ProfileBox
@@ -335,7 +348,7 @@ export const ProfileItem = (props: Props) => {
{...attributes} {...attributes}
{...listeners} {...listeners}
> >
<DragIndicator <DragIndicatorRounded
sx={[ sx={[
{ cursor: "move", marginLeft: "-6px" }, { cursor: "move", marginLeft: "-6px" },
({ palette: { text } }) => { ({ palette: { text } }) => {
@@ -470,66 +483,78 @@ export const ProfileItem = (props: Props) => {
</MenuItem> </MenuItem>
))} ))}
</Menu> </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 <ConfirmViewer
title={t("Confirm deletion")} title={t("Confirm deletion")}
message={t("This operation is not reversible")} message={t("This operation is not reversible")}
+21 -19
View File
@@ -167,25 +167,27 @@ export const ProfileMore = (props: Props) => {
</MenuItem> </MenuItem>
))} ))}
</Menu> </Menu>
{fileOpen && (
<EditorViewer <EditorViewer
open={fileOpen} open={true}
title={`${t("Global " + id)}`} title={`${t("Global " + id)}`}
initialData={readProfileFile(id)} initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"} language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined} schema={id === "Merge" ? "clash" : undefined}
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
await saveProfileFile(id, curr ?? ""); await saveProfileFile(id, curr ?? "");
onSave && onSave(prev, curr); onSave && onSave(prev, curr);
}} }}
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
)}
<LogViewer {logOpen && (
open={logOpen} <LogViewer
logInfo={logInfo} open={logOpen}
onClose={() => setLogOpen(false)} 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([...proxies, ...prependSeq]);
}}
>
{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 { import {
Box, Box,
Divider,
IconButton, IconButton,
ListItem, ListItem,
ListItemText, ListItemText,
Typography,
alpha, alpha,
styled,
} from "@mui/material"; } from "@mui/material";
import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material"; import { DeleteForeverRounded, UndoRounded } from "@mui/icons-material";
import { useSortable } from "@dnd-kit/sortable"; import { useSortable } from "@dnd-kit/sortable";
@@ -19,8 +18,20 @@ interface Props {
export const RuleItem = (props: Props) => { export const RuleItem = (props: Props) => {
let { type, ruleRaw, onDelete } = props; let { type, ruleRaw, onDelete } = props;
const sortable = type === "prepend" || type === "append"; const sortable = type === "prepend" || type === "append";
const rule = ruleRaw.replace(",no-resolve", "").split(","); const rule = ruleRaw.replace(",no-resolve", "");
const { attributes, listeners, setNodeRef, transform, transition } = sortable
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 }) ? useSortable({ id: ruleRaw })
: { : {
attributes: {}, attributes: {},
@@ -28,56 +39,104 @@ export const RuleItem = (props: Props) => {
setNodeRef: null, setNodeRef: null,
transform: null, transform: null,
transition: null, transition: null,
isDragging: false,
}; };
return ( return (
<ListItem <ListItem
dense
sx={({ palette }) => ({ sx={({ palette }) => ({
p: 0, position: "relative",
borderRadius: "10px", background:
border: "solid 2px",
borderColor:
type === "original" type === "original"
? "var(--divider-color)" ? palette.mode === "dark"
? alpha(palette.background.paper, 0.3)
: alpha(palette.grey[400], 0.3)
: type === "delete" : type === "delete"
? alpha(palette.error.main, 0.5) ? alpha(palette.error.main, 0.3)
: alpha(palette.success.main, 0.5), : alpha(palette.success.main, 0.3),
mb: 1, height: "100%",
margin: "8px 0",
borderRadius: "8px",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
})} })}
> >
<ListItemText <ListItemText
{...attributes} {...attributes}
{...listeners} {...listeners}
ref={setNodeRef} ref={setNodeRef}
sx={{ px: 1 }} sx={{ cursor: sortable ? "move" : "" }}
primary={ primary={
<> <StyledPrimary
<Typography title={ruleContent || "-"}
sx={{ sx={{ textDecoration: type === "delete" ? "line-through" : "" }}
textDecoration: type === "delete" ? "line-through" : "", >
overflow: "hidden", {ruleContent || "-"}
whiteSpace: "nowrap", </StyledPrimary>
textOverflow: "ellipsis",
}}
variant="h6"
title={rule.length === 3 ? rule[1] : "-"}
>
{rule.length === 3 ? rule[1] : "-"}
</Typography>
</>
} }
secondary={ secondary={
<Box sx={{ display: "flex", justifyContent: "space-between" }}> <ListItemTextChild
<Box>{rule[0]}</Box> sx={{
<Box>{rule.length === 3 ? rule[2] : rule[1]}</Box> width: "62%",
</Box> 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 onClick={onDelete}>
<IconButton size="small" color="inherit" onClick={onDelete}>
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />} {type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
</IconButton> </IconButton>
</ListItem> </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",
}));
+104 -54
View File
@@ -29,7 +29,10 @@ import {
TextField, TextField,
styled, styled,
} from "@mui/material"; } from "@mui/material";
import {
VerticalAlignTopRounded,
VerticalAlignBottomRounded,
} from "@mui/icons-material";
import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice, Switch } from "@/components/base"; import { Notice, Switch } from "@/components/base";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
@@ -40,8 +43,9 @@ import MonacoEditor from "react-monaco-editor";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
interface Props { interface Props {
groupsUid: string;
mergeUid: string;
profileUid: string; profileUid: string;
title?: string | ReactNode;
property: string; property: string;
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -230,13 +234,14 @@ const rules: {
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"]; const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
export const RulesEditorViewer = (props: Props) => { 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 { t } = useTranslation();
const themeMode = useThemeMode(); const themeMode = useThemeMode();
const [prevData, setPrevData] = useState(""); const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState(""); const [currData, setCurrData] = useState("");
const [visible, setVisible] = useState(true); const [visualization, setVisualization] = useState(true);
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]); const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]);
@@ -252,10 +257,18 @@ export const RulesEditorViewer = (props: Props) => {
const [appendSeq, setAppendSeq] = useState<string[]>([]); const [appendSeq, setAppendSeq] = useState<string[]>([]);
const [deleteSeq, setDeleteSeq] = useState<string[]>([]); const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
const filteredPrependSeq = useMemo(
() => prependSeq.filter((rule) => match(rule)),
[prependSeq, match]
);
const filteredRuleList = useMemo( const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)), () => ruleList.filter((rule) => match(rule)),
[ruleList, match] [ruleList, match]
); );
const filteredAppendSeq = useMemo(
() => appendSeq.filter((rule) => match(rule)),
[appendSeq, match]
);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
@@ -291,11 +304,11 @@ export const RulesEditorViewer = (props: Props) => {
}; };
const fetchContent = async () => { const fetchContent = async () => {
let data = await readProfileFile(property); 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 || []); setPrependSeq(obj?.prepend || []);
setAppendSeq(obj.append || []); setAppendSeq(obj?.append || []);
setDeleteSeq(obj.delete || []); setDeleteSeq(obj?.delete || []);
setPrevData(data); setPrevData(data);
setCurrData(data); setCurrData(data);
@@ -303,46 +316,83 @@ export const RulesEditorViewer = (props: Props) => {
useEffect(() => { useEffect(() => {
if (currData === "") return; if (currData === "") return;
if (visible !== true) return; if (visualization !== true) return;
let obj = yaml.load(currData) as { prepend: []; append: []; delete: [] }; let obj = yaml.load(currData) as ISeqProfileConfig | null;
setPrependSeq(obj.prepend || []); setPrependSeq(obj?.prepend || []);
setAppendSeq(obj.append || []); setAppendSeq(obj?.append || []);
setDeleteSeq(obj.delete || []); setDeleteSeq(obj?.delete || []);
}, [visible]); }, [visualization]);
useEffect(() => { useEffect(() => {
if (prependSeq && appendSeq && deleteSeq) if (prependSeq && appendSeq && deleteSeq)
setCurrData( setCurrData(
yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq }) yaml.dump(
{ prepend: prependSeq, append: appendSeq, delete: deleteSeq },
{
forceQuotes: true,
}
)
); );
}, [prependSeq, appendSeq, deleteSeq]); }, [prependSeq, appendSeq, deleteSeq]);
const fetchProfile = async () => { const fetchProfile = async () => {
let data = await readProfileFile(profileUid); let data = await readProfileFile(profileUid); // 原配置文件
let groupsObj = yaml.load(data) as { "proxy-groups": [] }; let groupsData = await readProfileFile(groupsUid); // groups配置文件
let rulesObj = yaml.load(data) as { rules: [] }; let mergeData = await readProfileFile(mergeUid); // merge配置文件
let ruleSetObj = yaml.load(data) as { "rule-providers": [] }; let globalMergeData = await readProfileFile("Merge"); // global merge配置文件
let subRuleObj = yaml.load(data) as { "sub-rules": [] };
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( setProxyPolicyList(
builtinProxyPolicies.concat( builtinProxyPolicies.concat(groups.map((group: any) => group.name))
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"]) : []
); );
setRuleSetList(Object.keys(ruleSet));
setSubRuleList(Object.keys(subRule));
setRuleList(rulesObj?.rules || []);
}; };
useEffect(() => { useEffect(() => {
if (!open) return;
fetchContent(); fetchContent();
fetchProfile(); fetchProfile();
}, [open]); }, [open]);
@@ -355,9 +405,10 @@ export const RulesEditorViewer = (props: Props) => {
throw new Error(t("Invalid Rule")); throw new Error(t("Invalid Rule"));
} }
return `${ruleType.name}${ const condition = ruleType.required ?? true ? ruleContent : "";
ruleContent ? "," + ruleContent : "" return `${ruleType.name}${condition ? "," + condition : ""},${proxyPolicy}${
},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""}`; ruleType.noResolve && noResolve ? ",no-resolve" : ""
}`;
}; };
const handleSave = useLockFn(async () => { const handleSave = useLockFn(async () => {
@@ -381,10 +432,10 @@ export const RulesEditorViewer = (props: Props) => {
variant="contained" variant="contained"
size="small" size="small"
onClick={() => { onClick={() => {
setVisible((prev) => !prev); setVisualization((prev) => !prev);
}} }}
> >
{visible ? t("Advanced") : t("Visible")} {visualization ? t("Advanced") : t("Visualization")}
</Button> </Button>
</Box> </Box>
</Box> </Box>
@@ -394,7 +445,7 @@ export const RulesEditorViewer = (props: Props) => {
<DialogContent <DialogContent
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }} sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
> >
{visible ? ( {visualization ? (
<> <>
<List <List
sx={{ sx={{
@@ -447,7 +498,7 @@ export const RulesEditorViewer = (props: Props) => {
{ruleType.name !== "RULE-SET" && {ruleType.name !== "RULE-SET" &&
ruleType.name !== "SUB-RULE" && ( ruleType.name !== "SUB-RULE" && (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ minWidth: "240px" }} sx={{ minWidth: "240px" }}
value={ruleContent} value={ruleContent}
@@ -487,11 +538,12 @@ export const RulesEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignTopRounded />}
onClick={() => { onClick={() => {
try { try {
let raw = validateRule(); let raw = validateRule();
if (prependSeq.includes(raw)) return; if (prependSeq.includes(raw)) return;
setPrependSeq([...prependSeq, raw]); setPrependSeq([raw, ...prependSeq]);
} catch (err: any) { } catch (err: any) {
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
} }
@@ -504,6 +556,7 @@ export const RulesEditorViewer = (props: Props) => {
<Button <Button
fullWidth fullWidth
variant="contained" variant="contained"
startIcon={<VerticalAlignBottomRounded />}
onClick={() => { onClick={() => {
try { try {
let raw = validateRule(); let raw = validateRule();
@@ -525,21 +578,18 @@ export const RulesEditorViewer = (props: Props) => {
padding: "0 10px", padding: "0 10px",
}} }}
> >
<BaseSearchBox <BaseSearchBox onSearch={(match) => setMatch(() => match)} />
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<Virtuoso <Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }} style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={ totalCount={
filteredRuleList.length + filteredRuleList.length +
(prependSeq.length > 0 ? 1 : 0) + (filteredPrependSeq.length > 0 ? 1 : 0) +
(appendSeq.length > 0 ? 1 : 0) (filteredAppendSeq.length > 0 ? 1 : 0)
} }
increaseViewportBy={256} increaseViewportBy={256}
itemContent={(index) => { itemContent={(index) => {
let shift = prependSeq.length > 0 ? 1 : 0; let shift = filteredPrependSeq.length > 0 ? 1 : 0;
if (prependSeq.length > 0 && index === 0) { if (filteredPrependSeq.length > 0 && index === 0) {
return ( return (
<DndContext <DndContext
sensors={sensors} sensors={sensors}
@@ -547,11 +597,11 @@ export const RulesEditorViewer = (props: Props) => {
onDragEnd={onPrependDragEnd} onDragEnd={onPrependDragEnd}
> >
<SortableContext <SortableContext
items={prependSeq.map((x) => { items={filteredPrependSeq.map((x) => {
return x; return x;
})} })}
> >
{prependSeq.map((item, index) => { {filteredPrependSeq.map((item, index) => {
return ( return (
<RuleItem <RuleItem
key={`${item}-${index}`} key={`${item}-${index}`}
@@ -603,11 +653,11 @@ export const RulesEditorViewer = (props: Props) => {
onDragEnd={onAppendDragEnd} onDragEnd={onAppendDragEnd}
> >
<SortableContext <SortableContext
items={appendSeq.map((x) => { items={filteredAppendSeq.map((x) => {
return x; return x;
})} })}
> >
{appendSeq.map((item, index) => { {filteredAppendSeq.map((item, index) => {
return ( return (
<RuleItem <RuleItem
key={`${item}-${index}`} key={`${item}-${index}`}
+4 -2
View File
@@ -105,8 +105,9 @@ export const ProviderButton = () => {
const download = sub?.Download || 0; const download = sub?.Download || 0;
const total = sub?.Total || 0; const total = sub?.Total || 0;
const expire = sub?.Expire || 0; const expire = sub?.Expire || 0;
const progress = Math.round( const progress = Math.min(
((download + upload) * 100) / (total + 0.1) Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
); );
return ( return (
<> <>
@@ -159,6 +160,7 @@ export const ProviderButton = () => {
<LinearProgress <LinearProgress
variant="determinate" variant="determinate"
value={progress} value={progress}
style={{ opacity: total > 0 ? 1 : 0 }}
/> />
</> </>
)} )}
+8 -4
View File
@@ -21,6 +21,7 @@ import delayManager from "@/services/delay";
interface Props { interface Props {
sx?: SxProps; sx?: SxProps;
url?: string;
groupName: string; groupName: string;
headState: HeadState; headState: HeadState;
onLocation: () => void; onLocation: () => void;
@@ -29,7 +30,7 @@ interface Props {
} }
export const ProxyHead = (props: Props) => { export const ProxyHead = (props: Props) => {
const { sx = {}, groupName, headState, onHeadState } = props; const { sx = {}, url, groupName, headState, onHeadState } = props;
const { showType, sortType, filterText, textState, testUrl } = headState; const { showType, sortType, filterText, textState, testUrl } = headState;
@@ -45,7 +46,10 @@ export const ProxyHead = (props: Props) => {
const { verge } = useVerge(); const { verge } = useVerge();
useEffect(() => { useEffect(() => {
delayManager.setUrl(groupName, testUrl || verge?.default_latency_test!); delayManager.setUrl(
groupName,
testUrl || url || verge?.default_latency_test!
);
}, [groupName, testUrl, verge?.default_latency_test]); }, [groupName, testUrl, verge?.default_latency_test]);
return ( return (
@@ -132,7 +136,7 @@ export const ProxyHead = (props: Props) => {
{textState === "filter" && ( {textState === "filter" && (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
autoFocus={autoFocus} autoFocus={autoFocus}
hiddenLabel hiddenLabel
value={filterText} value={filterText}
@@ -146,7 +150,7 @@ export const ProxyHead = (props: Props) => {
{textState === "url" && ( {textState === "url" && (
<TextField <TextField
autoComplete="off" autoComplete="new-password"
autoFocus={autoFocus} autoFocus={autoFocus}
hiddenLabel hiddenLabel
autoSave="off" autoSave="off"
+1
View File
@@ -129,6 +129,7 @@ export const ProxyRender = (props: RenderProps) => {
return ( return (
<ProxyHead <ProxyHead
sx={{ pl: 2, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }} sx={{ pl: 2, pr: 3, mt: indent ? 1 : 0.5, mb: 1 }}
url={group.testUrl}
groupName={group.name} groupName={group.name}
headState={headState!} headState={headState!}
onLocation={() => onLocation(group)} onLocation={() => onLocation(group)}
@@ -5,7 +5,10 @@ import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { SwitchAccessShortcut, RestartAlt } from "@mui/icons-material"; import {
SwitchAccessShortcutRounded,
RestartAltRounded,
} from "@mui/icons-material";
import { import {
Box, Box,
Button, Button,
@@ -85,7 +88,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
<LoadingButton <LoadingButton
variant="contained" variant="contained"
size="small" size="small"
startIcon={<SwitchAccessShortcut />} startIcon={<SwitchAccessShortcutRounded />}
loadingPosition="start" loadingPosition="start"
loading={upgrading} loading={upgrading}
sx={{ marginRight: "8px" }} sx={{ marginRight: "8px" }}
@@ -97,7 +100,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
variant="contained" variant="contained"
size="small" size="small"
onClick={onRestart} onClick={onRestart}
startIcon={<RestartAlt />} startIcon={<RestartAltRounded />}
> >
{t("Restart")} {t("Restart")}
</Button> </Button>
@@ -13,19 +13,18 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { clashInfo, patchInfo } = useClashInfo(); const { clashInfo, patchInfo } = useClashInfo();
const { verge, patchVerge } = useVerge(); const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [redirPort, setRedirPort] = useState( const [redirPort, setRedirPort] = useState(
verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895 verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895
); );
const [redirEnabled, setRedirEnabled] = useState( const [redirEnabled, setRedirEnabled] = useState(
verge?.verge_redir_enabled ?? true verge?.verge_redir_enabled ?? false
); );
const [tproxyPort, setTproxyPort] = useState( const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? clashInfo?.tproxy_port ?? 7896 verge?.verge_tproxy_port ?? clashInfo?.tproxy_port ?? 7896
); );
const [tproxyEnabled, setTproxyEnabled] = useState( const [tproxyEnabled, setTproxyEnabled] = useState(
verge?.verge_tproxy_enabled ?? true verge?.verge_tproxy_enabled ?? false
); );
const [mixedPort, setMixedPort] = useState( const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 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 verge?.verge_socks_port ?? clashInfo?.socks_port ?? 7898
); );
const [socksEnabled, setSocksEnabled] = useState( const [socksEnabled, setSocksEnabled] = useState(
verge?.verge_socks_enabled ?? true verge?.verge_socks_enabled ?? false
); );
const [port, setPort] = useState( const [port, setPort] = useState(
verge?.verge_port ?? clashInfo?.port ?? 7899 verge?.verge_port ?? clashInfo?.port ?? 7899
); );
const [httpEnabled, setHttpEnabled] = useState( const [httpEnabled, setHttpEnabled] = useState(
verge?.verge_http_enabled ?? true verge?.verge_http_enabled ?? false
); );
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
if (verge?.verge_redir_port) setRedirPort(verge?.verge_redir_port); 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); 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_mixed_port) setMixedPort(verge?.verge_mixed_port);
if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_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); if (verge?.verge_port) setPort(verge?.verge_port);
setHttpEnabled(verge?.verge_http_enabled ?? true); setHttpEnabled(verge?.verge_http_enabled ?? false);
setOpen(true); setOpen(true);
}, },
close: () => setOpen(false), close: () => setOpen(false),
@@ -94,24 +93,57 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
return; return;
} }
try { try {
if (OS !== "windows") { if (OS === "windows") {
await patchInfo({ "redir-port": redirPort }); await patchInfo({
await patchVerge({ verge_redir_port: redirPort }); "mixed-port": mixedPort,
await patchVerge({ verge_redir_enabled: redirEnabled }); "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") { if (OS === "linux") {
await patchInfo({ "tproxy-port": tproxyPort }); await patchInfo({
await patchVerge({ verge_tproxy_port: tproxyPort }); "redir-port": redirPort,
await patchVerge({ verge_tproxy_enabled: tproxyEnabled }); "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); setOpen(false);
Notice.success(t("Clash Port Modified"), 1000); Notice.success(t("Clash Port Modified"), 1000);
} catch (err: any) { } catch (err: any) {
@@ -134,7 +166,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Mixed Port")} /> <ListItemText primary={t("Mixed Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={mixedPort} value={mixedPort}
@@ -146,7 +178,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Socks Port")} /> <ListItemText primary={t("Socks Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={socksPort} value={socksPort}
@@ -169,7 +201,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Http Port")} /> <ListItemText primary={t("Http Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={port} value={port}
@@ -193,7 +225,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Redir Port")} /> <ListItemText primary={t("Redir Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={redirPort} value={redirPort}
@@ -218,7 +250,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Tproxy Port")} /> <ListItemText primary={t("Tproxy Port")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 135 }} sx={{ width: 135 }}
value={tproxyPort} value={tproxyPort}
@@ -20,11 +20,12 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
close: () => setOpen(false), close: () => setOpen(false),
})); }));
if (!open) return null;
return ( return (
<EditorViewer <EditorViewer
open={open} open={true}
title={ title={
<Box> <Box display="flex" alignItems="center" gap={2}>
{t("Runtime Config")} {t("Runtime Config")}
<Chip label={t("ReadOnly")} size="small" /> <Chip label={t("ReadOnly")} size="small" />
</Box> </Box>
@@ -48,7 +48,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("External Controller")} /> <ListItemText primary={t("External Controller")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 175 }} sx={{ width: 175 }}
value={controller} value={controller}
@@ -60,7 +60,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Core Secret")} /> <ListItemText primary={t("Core Secret")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
sx={{ width: 175 }} sx={{ width: 175 }}
value={secret} value={secret}
+2 -2
View File
@@ -198,7 +198,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
/> />
<TooltipIcon title={t("Default Latency Test Info")} /> <TooltipIcon title={t("Default Latency Test Info")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
@@ -215,7 +215,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} /> <ListItemText primary={t("Default Latency Timeout")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
type="number" type="number"
autoCorrect="off" autoCorrect="off"
@@ -0,0 +1,139 @@
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) => {
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 }}
disableOk
cancelBtn={t("Close")}
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 2px 2px 8px",
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,54 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
interface Props {
onConfirm: (passwd: string) => Promise<void>;
}
export const PasswordInput = (props: Props) => {
const { onConfirm } = props;
const { t } = useTranslation();
const [passwd, setPasswd] = useState("");
useEffect(() => {
if (!open) return;
}, [open]);
return (
<Dialog open={true} maxWidth="xs" fullWidth>
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
<DialogContent>
<TextField
sx={{ mt: 1 }}
autoFocus
label={t("Password")}
fullWidth
size="small"
type="password"
value={passwd}
onKeyDown={(e) => e.key === "Enter" && onConfirm(passwd)}
onChange={(e) => setPasswd(e.target.value)}
></TextField>
</DialogContent>
<DialogActions>
<Button
onClick={async () => await onConfirm(passwd)}
variant="contained"
>
{t("Confirm")}
</Button>
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,134 @@
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";
import { PasswordInput } from "./password-input";
import getSystem from "@/utils/get-system";
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 isWindows = getSystem() === "windows";
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 [openInstall, setOpenInstall] = useState(false);
const [openUninstall, setOpenUninstall] = useState(false);
async function install(passwd: string) {
try {
setOpenInstall(false);
await installService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Installed Successfully"));
setServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
async function uninstall(passwd: string) {
try {
setOpenUninstall(false);
await uninstallService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Uninstalled Successfully"));
setUninstallServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setUninstallServiceLoading(false);
}
}
const onInstallOrEnableService = useLockFn(async () => {
setServiceLoading(true);
if (isUninstall) {
// install service
if (isWindows) {
await install("");
} else {
setOpenInstall(true);
}
} else {
try {
// 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);
if (isWindows) {
await uninstall("");
} else {
setOpenUninstall(true);
}
});
return (
<>
{openInstall && <PasswordInput onConfirm={install} />}
{openUninstall && <PasswordInput onConfirm={uninstall} />}
<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>
);
});
+57 -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 { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@@ -14,7 +14,7 @@ import {
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds"; import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; 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 { EditorViewer } from "@/components/profile/editor-viewer";
import { BaseFieldset } from "@/components/base/base-fieldset"; import { BaseFieldset } from "@/components/base/base-fieldset";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
@@ -23,16 +23,43 @@ const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; 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}`;
const ipv6_part = "(?:[a-fA-F0-9:])+";
const rLocal = `localhost|<local>|localdomain`;
const getValidReg = (isWindows: boolean) => {
// 127.0.0.1 (full ipv4)
const rIPv4Unix = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}(?:\/\d{1,2})?`;
const rIPv4Windows = String.raw`(?:${ipv4_part}\.){3}${ipv4_part}`;
const rIPv6Unix = String.raw`(?:${ipv6_part}:+)+${ipv6_part}(?:\/\d{1,3})?`;
const rIPv6Windows = String.raw`(?:${ipv6_part}:+)+${ipv6_part}`;
const rValidPart = `${rDomainSimple}|${
isWindows ? rIPv4Windows : rIPv4Unix
}|${isWindows ? rIPv6Windows : rIPv6Unix}|${rLocal}`;
const separator = isWindows ? ";" : ",";
const rValid = String.raw`^(${rValidPart})(?:${separator}\s?(${rValidPart}))*${separator}?$`;
return new RegExp(rValid);
};
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => { export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
let validReg; const isWindows = getSystem() === "windows";
if (getSystem() === "windows") { const validReg = useMemo(() => getValidReg(isWindows), [isWindows]);
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 [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [editorOpen, setEditorOpen] = useState(false); const [editorOpen, setEditorOpen] = useState(false);
@@ -188,7 +215,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} /> <ListItemText primary={t("Guard Duration")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
disabled={!enabled} disabled={!enabled}
size="small" size="small"
value={value.duration} value={value.duration}
@@ -219,7 +246,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<> <>
<ListItemText primary={t("Proxy Bypass")} /> <ListItemText primary={t("Proxy Bypass")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
error={value.bypass ? !validReg.test(value.bypass) : false} error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled} disabled={!enabled}
size="small" size="small"
@@ -234,7 +261,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItemText primary={t("Bypass")} /> <ListItemText primary={t("Bypass")} />
<FlexBox> <FlexBox>
<TextField <TextField
autoComplete="off" autoComplete="new-password"
disabled={true} disabled={true}
size="small" size="small"
multiline multiline
@@ -253,7 +280,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
sx={{ padding: "3px 0" }} sx={{ padding: "3px 0" }}
/> />
<Button <Button
startIcon={<Edit />} startIcon={<EditRounded />}
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
setEditorOpen(true); setEditorOpen(true);
@@ -261,20 +288,22 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
> >
{t("Edit")} PAC {t("Edit")} PAC
</Button> </Button>
<EditorViewer {editorOpen && (
open={editorOpen} <EditorViewer
title={`${t("Edit")} PAC`} open={true}
initialData={Promise.resolve(value.pac_content ?? "")} title={`${t("Edit")} PAC`}
language="javascript" initialData={Promise.resolve(value.pac_content ?? "")}
onSave={(_prev, curr) => { language="javascript"
let pac = DEFAULT_PAC; onSave={(_prev, curr) => {
if (curr && curr.trim().length > 0) { let pac = DEFAULT_PAC;
pac = curr; if (curr && curr.trim().length > 0) {
} pac = curr;
setValue((v) => ({ ...v, pac_content: pac })); }
}} setValue((v) => ({ ...v, pac_content: pac }));
onClose={() => setEditorOpen(false)} }}
/> onClose={() => setEditorOpen(false)}
/>
)}
</ListItem> </ListItem>
</> </>
)} )}
+17 -15
View File
@@ -14,7 +14,7 @@ import { useVerge } from "@/hooks/use-verge";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme"; import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer"; 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) => { export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -115,7 +115,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
<Item> <Item>
<ListItemText primary={t("CSS Injection")} /> <ListItemText primary={t("CSS Injection")} />
<Button <Button
startIcon={<Edit />} startIcon={<EditRounded />}
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
setEditorOpen(true); setEditorOpen(true);
@@ -123,19 +123,21 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
> >
{t("Edit")} CSS {t("Edit")} CSS
</Button> </Button>
<EditorViewer {editorOpen && (
open={editorOpen} <EditorViewer
title={`${t("Edit")} CSS`} open={true}
initialData={Promise.resolve(theme.css_injection ?? "")} title={`${t("Edit")} CSS`}
language="css" initialData={Promise.resolve(theme.css_injection ?? "")}
onSave={(_prev, curr) => { language="css"
theme.css_injection = curr; onSave={(_prev, curr) => {
handleChange("css_injection"); theme.css_injection = curr;
}} handleChange("css_injection");
onClose={() => { }}
setEditorOpen(false); onClose={() => {
}} setEditorOpen(false);
/> }}
/>
)}
</Item> </Item>
</List> </List>
</BaseDialog> </BaseDialog>
+3 -3
View File
@@ -144,7 +144,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} /> <ListItemText primary={t("Device")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
@@ -190,7 +190,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} /> <ListItemText primary={t("DNS Hijack")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
@@ -207,7 +207,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} /> <ListItemText primary={t("MTU")} />
<TextField <TextField
autoComplete="off" autoComplete="new-password"
size="small" size="small"
type="number" type="number"
autoCorrect="off" 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"> <Stack spacing={0.75} direction="row" mt={1} mb={1} alignItems="center">
<TextField <TextField
autoComplete="off" autoComplete="new-password"
fullWidth fullWidth
size="small" size="small"
value={editValue} value={editValue}
@@ -24,7 +24,7 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
const webUIList = verge?.web_ui_list || [ const webUIList = verge?.web_ui_list || [
"https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret", "https://metacubex.github.io/metacubexd/#/setup?http=true&hostname=%host&port=%port&secret=%secret",
"https://yacd.metacubex.one/?host=%host&port=%port&secret=%secret", "https://yacd.metacubex.one/?hostname=%host&port=%port&secret=%secret",
]; ];
const handleAdd = useLockFn(async (value: string) => { const handleAdd = useLockFn(async (value: string) => {
+24 -5
View File
@@ -2,7 +2,11 @@ import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TextField, Select, MenuItem, Typography } from "@mui/material"; 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 { DialogRef, Notice, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state"; import { GuardState } from "./mods/guard-state";
@@ -16,6 +20,7 @@ import getSystem from "@/utils/get-system";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api"; import { updateGeoData } from "@/services/api";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
const isWIN = getSystem() === "windows"; const isWIN = getSystem() === "windows";
@@ -37,6 +42,7 @@ const SettingClash = ({ onError }: Props) => {
const portRef = useRef<DialogRef>(null); const portRef = useRef<DialogRef>(null);
const ctrlRef = useRef<DialogRef>(null); const ctrlRef = useRef<DialogRef>(null);
const coreRef = useRef<DialogRef>(null); const coreRef = useRef<DialogRef>(null);
const networkRef = useRef<DialogRef>(null);
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IConfigData>) => { const onChangeData = (patch: Partial<IConfigData>) => {
@@ -60,8 +66,21 @@ const SettingClash = ({ onError }: Props) => {
<ClashPortViewer ref={portRef} /> <ClashPortViewer ref={portRef} />
<ControllerViewer ref={ctrlRef} /> <ControllerViewer ref={ctrlRef} />
<ClashCoreViewer ref={coreRef} /> <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 <GuardState
value={allowLan ?? false} value={allowLan ?? false}
valueProps="checked" valueProps="checked"
@@ -112,7 +131,7 @@ const SettingClash = ({ onError }: Props) => {
<TooltipIcon <TooltipIcon
title={t("Random Port")} title={t("Random Port")}
color={enable_random_port ? "primary" : "inherit"} color={enable_random_port ? "primary" : "inherit"}
icon={Shuffle} icon={ShuffleRounded}
onClick={() => { onClick={() => {
Notice.success( Notice.success(
t("Restart Application to Apply Modifications"), t("Restart Application to Apply Modifications"),
@@ -125,7 +144,7 @@ const SettingClash = ({ onError }: Props) => {
} }
> >
<TextField <TextField
autoComplete="off" autoComplete="new-password"
disabled={enable_random_port} disabled={enable_random_port}
size="small" size="small"
value={verge_mixed_port ?? 7897} value={verge_mixed_port ?? 7897}
@@ -148,7 +167,7 @@ const SettingClash = ({ onError }: Props) => {
label={t("Clash Core")} label={t("Clash Core")}
extra={ extra={
<TooltipIcon <TooltipIcon
icon={Settings} icon={SettingsRounded}
onClick={() => coreRef.current?.open()} onClick={() => coreRef.current?.open()}
/> />
} }
+36 -41
View File
@@ -1,13 +1,13 @@
import useSWR from "swr"; import useSWR from "swr";
import { useRef } from "react"; import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PrivacyTipRounded, Settings } from "@mui/icons-material"; import { SettingsRounded } from "@mui/icons-material";
import { checkService } from "@/services/cmds"; import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; 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 { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state"; 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 { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer"; import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@@ -20,22 +20,23 @@ const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge(); const { verge, mutateVerge, patchVerge } = useVerge();
// service mode // service mode
const { data: serviceStatus } = useSWR("checkService", checkService, { const { data: serviceStatus, mutate: mutateServiceStatus } = useSWR(
revalidateIfStale: false, "checkService",
shouldRetryOnError: false, checkService,
focusThrottleInterval: 36e5, // 1 hour {
}); revalidateIfStale: false,
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null); const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null); const tunRef = useRef<DialogRef>(null);
const { const {
enable_tun_mode, enable_tun_mode,
enable_auto_launch, enable_auto_launch,
enable_service_mode,
enable_silent_start, enable_silent_start,
enable_system_proxy, enable_system_proxy,
} = verge ?? {}; } = verge ?? {};
@@ -49,14 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}> <SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} /> <SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} /> <TunViewer ref={tunRef} />
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
<SettingItem <SettingItem
label={t("Tun Mode")} label={t("Tun Mode")}
extra={ extra={
<TooltipIcon <TooltipIcon
title={t("Tun Mode Info")} title={t("Tun Mode Info")}
icon={Settings} icon={SettingsRounded}
onClick={() => tunRef.current?.open()} onClick={() => tunRef.current?.open()}
/> />
} }
@@ -66,38 +66,33 @@ const SettingSystem = ({ onError }: Props) => {
valueProps="checked" valueProps="checked"
onCatch={onError} onCatch={onError}
onFormat={onSwitchFormat} onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_tun_mode: e })} onChange={(e) => {
onGuard={(e) => patchVerge({ enable_tun_mode: 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" /> <Switch edge="end" />
</GuardState> </GuardState>
</SettingItem> </SettingItem>
<SettingItem <SettingItem label={t("Service Mode")}>
label={t("Service Mode")} <ServiceSwitcher
extra={ status={serviceStatus ?? "unknown"}
<TooltipIcon mutate={mutateServiceStatus}
title={t("Service Mode Info")} patchVerge={patchVerge}
icon={PrivacyTipRounded} onChangeData={onChangeData}
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> </SettingItem>
<SettingItem <SettingItem
@@ -106,7 +101,7 @@ const SettingSystem = ({ onError }: Props) => {
<> <>
<TooltipIcon <TooltipIcon
title={t("System Proxy Info")} title={t("System Proxy Info")}
icon={Settings} icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()} 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 { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/api/dialog"; 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 { import {
exitApp, exitApp,
openAppDir, openAppDir,
openCoreDir, openCoreDir,
openLogsDir, openLogsDir,
openDevTools, openDevTools,
copyClashEnv,
} from "@/services/cmds"; } from "@/services/cmds";
import { checkUpdate } from "@tauri-apps/api/updater"; import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -24,6 +32,8 @@ import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer"; import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers"; import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { ContentCopyRounded } from "@mui/icons-material";
interface Props { interface Props {
onError?: (err: Error) => void; 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 ( return (
<SettingList title={t("Verge Setting")}> <SettingList title={t("Verge Setting")}>
<ThemeViewer ref={themeRef} /> <ThemeViewer ref={themeRef} />
@@ -123,7 +138,12 @@ const SettingVerge = ({ onError }: Props) => {
</SettingItem> </SettingItem>
)} )}
<SettingItem label={t("Copy Env Type")}> <SettingItem
label={t("Copy Env Type")}
extra={
<TooltipIcon icon={ContentCopyRounded} onClick={onCopyClashEnv} />
}
>
<GuardState <GuardState
value={env_type ?? (OS === "windows" ? "powershell" : "bash")} value={env_type ?? (OS === "windows" ? "powershell" : "bash")}
onCatch={onError} onCatch={onError}
+15 -9
View File
@@ -13,7 +13,7 @@ import {
alpha, alpha,
} from "@mui/material"; } from "@mui/material";
import { BaseLoading } from "@/components/base"; import { BaseLoading } from "@/components/base";
import { LanguageTwoTone } from "@mui/icons-material"; import { LanguageRounded } from "@mui/icons-material";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { TestBox } from "./test-box"; import { TestBox } from "./test-box";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
@@ -28,12 +28,18 @@ interface Props {
onDelete: (uid: string) => void; onDelete: (uid: string) => void;
} }
let eventListener: UnlistenFn | null = null; let eventListener: UnlistenFn = () => {};
export const TestItem = (props: Props) => { export const TestItem = (props: Props) => {
const { itemData, onEdit, onDelete: onDeleteItem } = props; const { itemData, onEdit, onDelete: onDeleteItem } = props;
const { attributes, listeners, setNodeRef, transform, transition } = const {
useSortable({ id: props.id }); attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: props.id });
const { t } = useTranslation(); const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
@@ -84,9 +90,7 @@ export const TestItem = (props: Props) => {
]; ];
const listenTsetEvent = async () => { const listenTsetEvent = async () => {
if (eventListener !== null) { eventListener();
eventListener();
}
eventListener = await listen("verge://test-all", () => { eventListener = await listen("verge://test-all", () => {
onDelay(); onDelay();
}); });
@@ -94,13 +98,15 @@ export const TestItem = (props: Props) => {
useEffect(() => { useEffect(() => {
listenTsetEvent(); listenTsetEvent();
}, []); }, [url]);
return ( return (
<Box <Box
sx={{ sx={{
position: "relative",
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
zIndex: isDragging ? "calc(infinity)" : undefined,
}} }}
> >
<TestBox <TestBox
@@ -138,7 +144,7 @@ export const TestItem = (props: Props) => {
</Box> </Box>
) : ( ) : (
<Box sx={{ display: "flex", justifyContent: "center" }}> <Box sx={{ display: "flex", justifyContent: "center" }}>
<LanguageTwoTone sx={{ height: "40px" }} fontSize="large" /> <LanguageRounded sx={{ height: "40px" }} fontSize="large" />
</Box> </Box>
)} )}
+39 -3
View File
@@ -1,5 +1,6 @@
{ {
"millis": "millis", "millis": "millis",
"seconds": "seconds",
"mins": "mins", "mins": "mins",
"Back": "Back", "Back": "Back",
"Close": "Close", "Close": "Close",
@@ -54,6 +55,7 @@
"Create Profile": "Create Profile", "Create Profile": "Create Profile",
"Edit Profile": "Edit Profile", "Edit Profile": "Edit Profile",
"Edit Proxies": "Edit Proxies", "Edit Proxies": "Edit Proxies",
"Use newlines for multiple uri": "Use newlines for multiple uri(Base64 encoding supported)",
"Edit Rules": "Edit Rules", "Edit Rules": "Edit Rules",
"Rule Type": "Rule Type", "Rule Type": "Rule Type",
"Rule Content": "Rule Content", "Rule Content": "Rule Content",
@@ -61,11 +63,14 @@
"No Resolve": "No Resolve", "No Resolve": "No Resolve",
"Prepend Rule": "Prepend Rule", "Prepend Rule": "Prepend Rule",
"Append Rule": "Append 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", "Rule Condition Required": "Rule Condition Required",
"Invalid Rule": "Invalid Rule", "Invalid Rule": "Invalid Rule",
"Advanced": "Advanced", "Advanced": "Advanced",
"Visible": "Visible", "Visualization": "Visualization",
"DOMAIN": "Matches the full domain name", "DOMAIN": "Matches the full domain name",
"DOMAIN-SUFFIX": "Matches the domain suffix", "DOMAIN-SUFFIX": "Matches the domain suffix",
"DOMAIN-KEYWORD": "Matches the domain keyword", "DOMAIN-KEYWORD": "Matches the domain keyword",
@@ -104,6 +109,32 @@
"REJECT-DROP": "Discards requests", "REJECT-DROP": "Discards requests",
"PASS": "Skips this rule when matched", "PASS": "Skips this rule when matched",
"Edit Groups": "Edit Proxy Groups", "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 Config": "Extend Config",
"Extend Script": "Extend Script", "Extend Script": "Extend Script",
"Global Merge": "Global Extend Config", "Global Merge": "Global Extend Config",
@@ -211,6 +242,9 @@
"Github Repo": "Github Repo", "Github Repo": "Github Repo",
"Clash Setting": "Clash Setting", "Clash Setting": "Clash Setting",
"Allow Lan": "Allow LAN", "Allow Lan": "Allow LAN",
"Network Interface": "Network Interface",
"Ip Address": "IP Address",
"Mac Address": "MAC Address",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Log Level", "Log Level": "Log Level",
"Port Config": "Port Config", "Port Config": "Port Config",
@@ -232,7 +266,8 @@
"Restart": "Restart", "Restart": "Restart",
"Release Version": "Release Version", "Release Version": "Release Version",
"Alpha Version": "Alpha Version", "Alpha Version": "Alpha Version",
"Tun mode requires": "Tun mode requires", "Please Enable Service Mode": "Please Install and Enable Service Mode First",
"Please enter your root password": "Please enter your root password",
"Grant": "Grant", "Grant": "Grant",
"Open UWP tool": "Open UWP tool", "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", "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 +281,7 @@
"Tray Click Event": "Tray Click Event", "Tray Click Event": "Tray Click Event",
"Show Main Window": "Show Main Window", "Show Main Window": "Show Main Window",
"Copy Env Type": "Copy Env Type", "Copy Env Type": "Copy Env Type",
"Copy Success": "Copy Success",
"Start Page": "Start Page", "Start Page": "Start Page",
"Startup Script": "Startup Script", "Startup Script": "Startup Script",
"Browse": "Browse", "Browse": "Browse",
+38 -2
View File
@@ -1,5 +1,6 @@
{ {
"millis": "میلی‌ثانیه", "millis": "میلی‌ثانیه",
"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": "استفاده از خطوط جدید برای چندین آدرس (پشتیبانی از رمزگذاری Base64)",
"Edit Rules": "ویرایش قوانین", "Edit Rules": "ویرایش قوانین",
"Rule Type": "نوع قانون", "Rule Type": "نوع قانون",
"Rule Content": "محتوای قانون", "Rule Content": "محتوای قانون",
@@ -61,9 +63,12 @@
"No Resolve": "بدون حل", "No Resolve": "بدون حل",
"Prepend Rule": "اضافه کردن قانون به ابتدا", "Prepend Rule": "اضافه کردن قانون به ابتدا",
"Append Rule": "اضافه کردن قانون به انتها", "Append Rule": "اضافه کردن قانون به انتها",
"Delete Rule": "حذف قانون", "Prepend Group": "اضافه کردن گروه به ابتدا",
"Append Group": "اضافه کردن گروه به انتها",
"Rule Condition Required": "شرط قانون الزامی است", "Rule Condition Required": "شرط قانون الزامی است",
"Invalid Rule": "قانون نامعتبر", "Invalid Rule": "قانون نامعتبر",
"Advanced": "پیشرفته",
"Visualization": "تجسم",
"DOMAIN": "مطابقت با نام کامل دامنه", "DOMAIN": "مطابقت با نام کامل دامنه",
"DOMAIN-SUFFIX": "مطابقت با پسوند دامنه", "DOMAIN-SUFFIX": "مطابقت با پسوند دامنه",
"DOMAIN-KEYWORD": "مطابقت با کلمه کلیدی دامنه", "DOMAIN-KEYWORD": "مطابقت با کلمه کلیدی دامنه",
@@ -102,6 +107,32 @@
"REJECT-DROP": "درخواست‌ها را نادیده می‌گیرد", "REJECT-DROP": "درخواست‌ها را نادیده می‌گیرد",
"PASS": "این قانون را در صورت تطابق نادیده می‌گیرد", "PASS": "این قانون را در صورت تطابق نادیده می‌گیرد",
"Edit Groups": "ویرایش گروه‌های پروکسی", "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 Config": "توسعه پیکربندی",
"Extend Script": "ادغام اسکریپت", "Extend Script": "ادغام اسکریپت",
"Global Merge": "تنظیمات گسترده‌ی سراسری", "Global Merge": "تنظیمات گسترده‌ی سراسری",
@@ -206,6 +237,9 @@
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید", "Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"Clash Setting": "تنظیمات Clash", "Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN", "Allow Lan": "اجازه LAN",
"Network Interface": "رابط شبکه",
"Ip Address": "آدرس IP",
"Mac Address": "آدرس MAC",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "سطح لاگ", "Log Level": "سطح لاگ",
"Port Config": "پیکربندی پورت", "Port Config": "پیکربندی پورت",
@@ -227,7 +261,8 @@
"Restart": "راه‌اندازی مجدد", "Restart": "راه‌اندازی مجدد",
"Release Version": "نسخه نهایی", "Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا", "Alpha Version": "نسخه آلفا",
"Tun mode requires": "Tun mode نیاز دارد", "Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید",
"Grant": "اعطا", "Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP", "Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود", "Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
@@ -244,6 +279,7 @@
"Tray Click Event": "رویداد کلیک در سینی سیستم", "Tray Click Event": "رویداد کلیک در سینی سیستم",
"Show Main Window": "نمایش پنجره اصلی", "Show Main Window": "نمایش پنجره اصلی",
"Copy Env Type": "کپی نوع محیط", "Copy Env Type": "کپی نوع محیط",
"Copy Success": "کپی با موفقیت انجام شد",
"Start Page": "صفحه شروع", "Start Page": "صفحه شروع",
"Startup Script": "اسکریپت راه‌اندازی", "Startup Script": "اسکریپت راه‌اندازی",
"Browse": "مرور کردن", "Browse": "مرور کردن",
+38 -2
View File
@@ -1,5 +1,6 @@
{ {
"millis": "миллисекунды", "millis": "миллисекунды",
"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": "Используйте символы новой строки для нескольких URI (поддерживается кодировка Base64)",
"Edit Rules": "Редактировать правила", "Edit Rules": "Редактировать правила",
"Rule Type": "Тип правила", "Rule Type": "Тип правила",
"Rule Content": "Содержимое правила", "Rule Content": "Содержимое правила",
@@ -61,9 +63,12 @@
"No Resolve": "Без разрешения", "No Resolve": "Без разрешения",
"Prepend Rule": "Добавить правило в начало", "Prepend Rule": "Добавить правило в начало",
"Append Rule": "Добавить правило в конец", "Append Rule": "Добавить правило в конец",
"Delete Rule": "Удалить правило", "Prepend Group": "Добавить группу в начало",
"Append Group": "Добавить группу в конец",
"Rule Condition Required": "Требуется условие правила", "Rule Condition Required": "Требуется условие правила",
"Invalid Rule": "Недействительное правило", "Invalid Rule": "Недействительное правило",
"Advanced": "Дополнительно",
"Visualization": "Визуализация",
"DOMAIN": "Соответствует полному доменному имени", "DOMAIN": "Соответствует полному доменному имени",
"DOMAIN-SUFFIX": "Соответствует суффиксу домена", "DOMAIN-SUFFIX": "Соответствует суффиксу домена",
"DOMAIN-KEYWORD": "Соответствует ключевому слову домена", "DOMAIN-KEYWORD": "Соответствует ключевому слову домена",
@@ -102,6 +107,32 @@
"REJECT-DROP": "Отклоняет запросы", "REJECT-DROP": "Отклоняет запросы",
"PASS": "Пропускает это правило при совпадении", "PASS": "Пропускает это правило при совпадении",
"Edit Groups": "Редактировать группы прокси", "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 Config": "Изменить Merge.",
"Extend Script": "Изменить Script", "Extend Script": "Изменить Script",
"Global Merge": "Глобальный расширенный Настройки", "Global Merge": "Глобальный расширенный Настройки",
@@ -209,6 +240,9 @@
"Github Repo": "GitHub репозиторий", "Github Repo": "GitHub репозиторий",
"Clash Setting": "Настройки Clash", "Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть", "Allow Lan": "Разрешить локальную сеть",
"Network Interface": "Сетевой интерфейс",
"Ip Address": "IP адрес",
"Mac Address": "MAC адрес",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "Уровень логов", "Log Level": "Уровень логов",
"Port Config": "Настройка порта", "Port Config": "Настройка порта",
@@ -230,7 +264,8 @@
"Restart": "Перезапуск", "Restart": "Перезапуск",
"Release Version": "Официальная версия", "Release Version": "Официальная версия",
"Alpha Version": "Альфа-версия", "Alpha Version": "Альфа-версия",
"Tun mode requires": "Требуется Режим туннеля", "Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания",
"Please enter your root password": "Пожалуйста, введите ваш пароль root",
"Grant": "Предоставить", "Grant": "Предоставить",
"Open UWP tool": "Открыть UWP инструмент", "Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение", "Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
@@ -244,6 +279,7 @@
"Tray Click Event": "Событие щелчка в лотке", "Tray Click Event": "Событие щелчка в лотке",
"Show Main Window": "Показать главное окно", "Show Main Window": "Показать главное окно",
"Copy Env Type": "Скопировать тип Env", "Copy Env Type": "Скопировать тип Env",
"Copy Success": "Скопировано",
"Start Page": "Главная страница", "Start Page": "Главная страница",
"Startup Script": "Скрипт запуска", "Startup Script": "Скрипт запуска",
"Browse": "Просмотреть", "Browse": "Просмотреть",
+39 -3
View File
@@ -1,6 +1,7 @@
{ {
"millis": "毫秒", "millis": "毫秒",
"mins": "分钟", "mins": "分钟",
"seconds": "秒",
"Back": "返回", "Back": "返回",
"Close": "关闭", "Close": "关闭",
"Cancel": "取消", "Cancel": "取消",
@@ -54,6 +55,7 @@
"Create Profile": "新建配置", "Create Profile": "新建配置",
"Edit Profile": "编辑配置", "Edit Profile": "编辑配置",
"Edit Proxies": "编辑节点", "Edit Proxies": "编辑节点",
"Use newlines for multiple uri": "多条URI请使用换行分隔(支持Base64编码)",
"Edit Rules": "编辑规则", "Edit Rules": "编辑规则",
"Rule Type": "规则类型", "Rule Type": "规则类型",
"Rule Content": "规则内容", "Rule Content": "规则内容",
@@ -61,11 +63,14 @@
"No Resolve": "跳过DNS解析", "No Resolve": "跳过DNS解析",
"Prepend Rule": "添加前置规则", "Prepend Rule": "添加前置规则",
"Append Rule": "添加后置规则", "Append Rule": "添加后置规则",
"Delete Rule": "删除规则", "Prepend Group": "添加前置代理组",
"Append Group": "添加后置代理组",
"Prepend Proxy": "添加前置代理节点",
"Append Proxy": "添加后置代理节点",
"Rule Condition Required": "规则条件缺失", "Rule Condition Required": "规则条件缺失",
"Invalid Rule": "无效规则", "Invalid Rule": "无效规则",
"Advanced": "高级", "Advanced": "高级",
"Visible": "可视化", "Visualization": "可视化",
"DOMAIN": "匹配完整域名", "DOMAIN": "匹配完整域名",
"DOMAIN-SUFFIX": "匹配域名后缀", "DOMAIN-SUFFIX": "匹配域名后缀",
"DOMAIN-KEYWORD": "匹配域名关键字", "DOMAIN-KEYWORD": "匹配域名关键字",
@@ -104,6 +109,32 @@
"REJECT-DROP": "抛弃请求", "REJECT-DROP": "抛弃请求",
"PASS": "跳过此规则", "PASS": "跳过此规则",
"Edit Groups": "编辑代理组", "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 Config": "扩展配置",
"Extend Script": "扩展脚本", "Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置", "Global Merge": "全局扩展配置",
@@ -211,6 +242,9 @@
"Github Repo": "GitHub 项目地址", "Github Repo": "GitHub 项目地址",
"Clash Setting": "Clash 设置", "Clash Setting": "Clash 设置",
"Allow Lan": "局域网连接", "Allow Lan": "局域网连接",
"Network Interface": "网络接口",
"Ip Address": "IP 地址",
"Mac Address": "MAC 地址",
"IPv6": "IPv6", "IPv6": "IPv6",
"Log Level": "日志等级", "Log Level": "日志等级",
"Port Config": "端口设置", "Port Config": "端口设置",
@@ -232,7 +266,8 @@
"Restart": "重启内核", "Restart": "重启内核",
"Release Version": "正式版", "Release Version": "正式版",
"Alpha Version": "预览版", "Alpha Version": "预览版",
"Tun mode requires": "如需启用 Tun 模式需要授权", "Please Enable Service Mode": "请先安装并启用服务模式",
"Please enter your root password": "请输入您的 root 密码",
"Grant": "授权", "Grant": "授权",
"Open UWP tool": "UWP 工具", "Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制", "Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
@@ -246,6 +281,7 @@
"Tray Click Event": "托盘点击事件", "Tray Click Event": "托盘点击事件",
"Show Main Window": "显示主窗口", "Show Main Window": "显示主窗口",
"Copy Env Type": "复制环境变量类型", "Copy Env Type": "复制环境变量类型",
"Copy Success": "复制成功",
"Start Page": "启动页面", "Start Page": "启动页面",
"Startup Script": "启动脚本", "Startup Script": "启动脚本",
"Browse": "浏览", "Browse": "浏览",
+5 -4
View File
@@ -137,7 +137,7 @@ const ProfilePage = () => {
mutate("getProfiles", newProfiles); mutate("getProfiles", newProfiles);
const remoteItem = newProfiles.items?.find((e) => e.type === "remote"); const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
if (!newProfiles.current && remoteItem) { if (newProfiles.current && remoteItem) {
const current = remoteItem.uid; const current = remoteItem.uid;
await patchProfiles({ current }); await patchProfiles({ current });
mutateLogs(); mutateLogs();
@@ -171,10 +171,11 @@ const ProfilePage = () => {
}, 100); }, 100);
try { try {
await patchProfiles({ current }); await patchProfiles({ current });
mutateLogs(); await mutateLogs();
closeAllConnections(); closeAllConnections();
setTimeout(() => activateSelected(), 2000); activateSelected().then(() => {
Notice.success(t("Profile Switched"), 1000); Notice.success(t("Profile Switched"), 1000);
});
} catch (err: any) { } catch (err: any) {
Notice.error(err?.message || err.toString(), 4000); Notice.error(err?.message || err.toString(), 4000);
} finally { } finally {
+2 -2
View File
@@ -2,7 +2,7 @@ import { Box, ButtonGroup, Grid, IconButton } from "@mui/material";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base"; import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineSharp, Telegram } from "@mui/icons-material"; import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds"; import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge"; import SettingVerge from "@/components/setting/setting-verge";
import SettingClash from "@/components/setting/setting-clash"; import SettingClash from "@/components/setting/setting-clash";
@@ -42,7 +42,7 @@ const SettingPage = () => {
title={t("Manual")} title={t("Manual")}
onClick={toGithubDoc} onClick={toGithubDoc}
> >
<HelpOutlineSharp fontSize="inherit" /> <HelpOutlineRounded fontSize="inherit" />
</IconButton> </IconButton>
<IconButton <IconButton
size="medium" size="medium"
+16 -4
View File
@@ -2,6 +2,10 @@ import dayjs from "dayjs";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
export async function copyClashEnv() {
return invoke<void>("copy_clash_env");
}
export async function getClashLogs() { export async function getClashLogs() {
const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/; const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/;
const newRegex = /(.+?)\s+(.+?)\s+(.+)/; const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
@@ -197,12 +201,12 @@ export async function checkService() {
} }
} }
export async function installService() { export async function installService(passwd: string) {
return invoke<void>("install_service"); return invoke<void>("install_service", { passwd });
} }
export async function uninstallService() { export async function uninstallService(passwd: string) {
return invoke<void>("uninstall_service"); return invoke<void>("uninstall_service", { passwd });
} }
export async function invoke_uwp_tool() { export async function invoke_uwp_tool() {
@@ -233,3 +237,11 @@ export async function copyIconFile(
export async function downloadIconCache(url: string, name: string) { export async function downloadIconCache(url: string, name: string) {
return invoke<string>("download_icon_cache", { url, name }); 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");
}
+482 -3
View File
@@ -59,6 +59,7 @@ interface IProxyItem {
time: string; time: string;
delay: number; delay: number;
}[]; }[];
testUrl?: string;
all?: string[]; all?: string[];
now?: string; now?: string;
hidden?: boolean; hidden?: boolean;
@@ -197,11 +198,489 @@ interface IVergeTestItem {
icon?: string; icon?: string;
url: 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 { interface ISeqProfileConfig {
prepend: string[]; prepend: [];
append: string[]; append: [];
delete: string[]; 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 { 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