Compare commits

..
116 Commits
Author SHA1 Message Date
MystiPanda eaed7b2899 Release 1.7.1 2024-07-03 09:52:19 +08:00
MystiPanda 123ecc3548 fix: rules drag error 2024-07-03 09:45:14 +08:00
MystiPanda c857fcf035 fix: compatibility 2024-07-03 08:15:13 +08:00
dongchengjie 923f710e53 fix: search-box takes no effect in rule-editor #1288 2024-07-03 03:08:01 +08:00
dongchengjie 8d8c2ed262 fix: sub usage percent fails to display when number is too small #1290 2024-07-03 02:59:32 +08:00
MystiPanda 8558673a5a Release 1.7.0 2024-07-03 00:32:14 +08:00
MystiPanda 7372f330a4 perf: optimize performance of the rule editor 2024-07-02 23:55:29 +08:00
dongchengjie 07c145c661 feat: editor add tool-tar buttons 2024-07-02 23:24:44 +08:00
dongchengjie f69e1d2a0c refactor: editor-viewer using react-monaco-editor 2024-07-02 21:02:29 +08:00
dongchengjie 0021fc24bb chore: bump schema to 1.18.5-alpha7 2024-07-02 19:38:44 +08:00
MystiPanda 82bc1d5da5 chore: disable autocomplete 2024-07-02 19:02:05 +08:00
MystiPanda ffc343b471 fix: default value of global extend config 2024-07-02 18:46:23 +08:00
dongchengjie 86e43123b0 chore: add rule list search-box 2024-07-02 17:32:49 +08:00
dongchengjie b558202441 chore: rule types locale 2024-07-02 17:04:22 +08:00
MystiPanda 83947b6725 chore: check the validity of the rule content 2024-07-02 13:11:54 +08:00
MystiPanda 7e8b65e61f chore: Adjust the chain processing execution order and default value 2024-07-02 12:40:28 +08:00
MystiPanda cff0ea425c build: update depends 2024-07-02 12:21:17 +08:00
MystiPanda f6ba6d0310 chore: update locale 2024-07-01 23:30:14 +08:00
MystiPanda 7b9bf9e552 feat: allow set bypass without using default value 2024-07-01 22:53:32 +08:00
Sukka c91ad1e016 ci: speed up cargo install by enabling cache (webview2) (#1283) 2024-07-01 14:23:21 +08:00
MystiPanda b1444b8635 feat: global merge and script 2024-07-01 08:25:03 +08:00
MystiPanda 51a49b94d8 feat: drag to reorder rules 2024-06-30 23:16:21 +08:00
MystiPanda 720b46d790 chore: select rule-set name 2024-06-30 22:46:11 +08:00
MystiPanda 9089c30d57 feat: display rules list 2024-06-30 22:34:26 +08:00
MystiPanda e2d522803c fix: editor init error 2024-06-30 18:05:38 +08:00
Sukka babcb00621 ci: speed up cargo install by enabling cache (#1279) 2024-06-30 17:21:55 +08:00
MystiPanda 901a983150 feat: rules editor 2024-06-30 17:17:04 +08:00
MystiPanda 955182b05b chore: locale 2024-06-30 13:03:36 +08:00
MystiPanda f4dfe8eeb4 feat(unfinished): rules editor 2024-06-30 12:46:31 +08:00
MystiPanda 93904b8278 refactor: pure merge 2024-06-30 07:58:44 +08:00
MystiPanda 495580ae2b fix: delete logic 2024-06-30 07:37:52 +08:00
MystiPanda 3051004217 chore: update locale 2024-06-30 07:18:10 +08:00
MystiPanda b854b5e1ac feat: support seq editor 2024-06-30 00:22:05 +08:00
MystiPanda cf61a96ef6 refactor: Associate Profile with Merge/Script. 2024-06-29 23:07:44 +08:00
MystiPanda 1293d25e1b refactor: use async instead of block_on 2024-06-29 19:02:37 +08:00
dongchengjie c1a201f358 fix: do not reactive when changed profile is not current 2024-06-29 11:22:11 +08:00
dongchengjie 9ee5390ec7 feat: reactive after save when profile content changes 2024-06-29 09:21:50 +08:00
dongchengjie 3f1caa702b fix: #1261 2024-06-29 05:02:06 +08:00
wonfen 24f9573c05 chore: add TG link & descriptions 2024-06-29 04:55:43 +08:00
dongchengjie 83b25920ea chore: add descriptions for Miscellaneous 2024-06-28 15:54:27 +08:00
MystiPanda 834edcd03e fix: unset dns when exit 2024-06-28 12:18:18 +08:00
MystiPanda 8f3801e3c2 feat: set dns by service 2024-06-28 11:45:44 +08:00
dongchengjie b5f7c58276 chore: remove duplicate locales 2024-06-26 19:11:39 +08:00
dongchengjie 66ae293ddd fix: await compatibility in #8e78b9e 2024-06-26 18:29:50 +08:00
dongchengjie 8e78b9e405 feat: support manual memory cleanup when running in debug mode 2024-06-26 17:44:42 +08:00
dongchengjie 9da1759247 chore: cleanup 2024-06-26 08:24:43 +08:00
dongchengjie 704c41c0f2 refactor: extract tooltip icon as component 2024-06-26 08:10:18 +08:00
dongchengjie 1f422afe3d chore: tooltips and locales 2024-06-26 05:33:06 +08:00
dongchengjie 709a23cf09 chore: fix connection table bg 2024-06-23 20:40:09 +08:00
dongchengjie bfa3fa293f fix: update_interval won't save when creating local profile while updating does & number input locales 2024-06-23 06:47:51 +08:00
MystiPanda d8b878b1bb feat: handle break change update 2024-06-22 21:05:46 +08:00
dongchengjie b2589dbc04 chore: hotkeys display delimiter 2024-06-21 18:24:58 +08:00
MystiPanda 2a4b605794 chore: hide delay for preset outbound 2024-06-21 00:16:41 +08:00
MystiPanda a3080a3373 revert: just kill 2024-06-20 14:27:13 +08:00
MystiPanda b4b9ae5d7f feat: custom dmg background 2024-06-20 13:50:28 +08:00
dongchengjie bb9a93e462 chore: fix table header bg in connections 2024-06-20 02:01:51 +08:00
dongchengjie e55fbf675e chore: disable shortcuts #1239 2024-06-20 01:39:51 +08:00
MystiPanda 0342477733 fix: runtime error 2024-06-19 11:10:40 +08:00
MystiPanda e6e2b1f142 refactor: change core binary name 2024-06-19 10:04:28 +08:00
MystiPanda 95c23a93cd refactor: kill core by process name 2024-06-18 12:36:58 +08:00
MystiPanda ae784cb985 fix: reg error 2024-06-17 16:38:30 +08:00
Sukka 17765bdba9 fix(#1226): missing conn unsub (#1228) 2024-06-17 13:36:35 +08:00
Sukka 45f791b3c8 refactor(connections): use swr subscription (#1226) 2024-06-17 13:14:36 +08:00
dongchengjie b0ad39ed0b fix: resizing reset when data changes 2024-06-17 13:03:47 +08:00
Sukka 1988aeb945 refactor(log): use swr subscription (#1220) 2024-06-17 10:48:37 +08:00
MystiPanda f48a5710aa chore: update depends 2024-06-17 10:46:18 +08:00
Sukka c8d2410c27 feat: add a wrapper around sockette w/ error retry (#1219)
* feat: add a wrapper around sockette w/ error retry

* chore: use import path alias

* perf: reduce retry
2024-06-16 18:25:33 +08:00
MystiPanda a9ef32c32f Revert "feat: disable running with admin permission and check service mode"
This reverts commit a0f9fb90ee.
2024-06-16 12:06:23 +08:00
Sukka 19b6f78c8a ci(alpha): avoid race by cancel non-latest concurrent runs (#1213) 2024-06-16 01:10:30 +08:00
Sukka 4ab507fd43 fix: hide save button in readonly editor view (#1208) 2024-06-15 19:24:26 +08:00
Sukka 2662df2547 fix(#1203): correct types (#1207) 2024-06-15 19:23:58 +08:00
Sukka 0332415ac9 perf: replace Array#map Array#filter chain w/ Array#reduce (#1203) 2024-06-15 12:22:33 +08:00
MystiPanda a0f9fb90ee feat: disable running with admin permission and check service mode 2024-06-14 23:15:49 +08:00
MystiPanda 1b8c4cb832 build: downgrade auto-launch 2024-06-14 21:26:30 +08:00
MystiPanda cfd50d281b fix: fix bypass check regex 2024-06-14 21:23:58 +08:00
Sukka 1667856894 chore: use swr subscription for layout traffic / memory (#1202)
* chore: update swr to 2

* refactor: use swr subscription for memory & traffic

* refactor: introduce `sockette`
2024-06-14 18:23:29 +08:00
dongchengjie c5c76ab539 chore: update metacubexd #1200 2024-06-14 17:09:12 +08:00
dongchengjie eb060d2e43 feat: local profile name autofill #1191 2024-06-13 16:29:25 +08:00
MystiPanda b5556613cf refactor: remove grant logic 2024-06-13 16:07:56 +08:00
MystiPanda ad1a057edb fix: check service 2024-06-13 12:58:47 +08:00
MystiPanda 8342f985e5 ci: fix release 2024-06-12 12:25:12 +08:00
MystiPanda 9d32e58915 chore: update publisher 2024-06-12 11:51:44 +08:00
MystiPanda 84027cbae1 Release 1.6.6 2024-06-12 10:33:20 +08:00
MystiPanda 5343040ef3 fix: start param error 2024-06-12 10:19:23 +08:00
MystiPanda 75cd7dd4b9 chore: update & fmt & clippy 2024-06-12 10:00:22 +08:00
dongchengjie fb6ffd8e09 chore: missing locale 2024-06-11 16:19:25 +08:00
dongchengjie aca9e61f39 chore: locale "Invalid Bypass Format" 2024-06-09 20:02:20 +08:00
MystiPanda 56078098a4 fix: start param error 2024-06-09 20:00:17 +08:00
MystiPanda 1f197965e0 feat: check bypass format 2024-06-09 13:37:47 +08:00
MystiPanda 183f0f6c42 feat: keep default bypass 2024-06-09 12:45:57 +08:00
dongchengjie 76624c7d83 chore: missing locale 2024-06-09 11:16:13 +08:00
dongchengjie ab47c56ec5 chore: locale reorganization 2024-06-09 09:12:14 +08:00
Eric Huang ad35ed96c8 feat(settings page): add loading state (#1157)
* feat(settings page): add loading state

* fix: type
2024-06-09 06:26:07 +08:00
wonfen 97bd51cf5f chore: update ci with apple notarization 2024-06-09 02:01:33 +08:00
MystiPanda f4a110cfd7 ci: fix error 2024-06-08 20:58:57 +08:00
MystiPanda e9ed06adfd ci: update 2024-06-08 20:45:28 +08:00
MystiPanda bc5eaf34fe feat: add download button on updater dialog
#1129
2024-06-08 20:35:23 +08:00
MystiPanda 3e8b891dd0 fix: run app as normal user 2024-06-08 20:20:47 +08:00
MystiPanda 3f2ea4bc64 build: update depends 2024-06-08 20:02:25 +08:00
MystiPanda ae81b37c1d build: remove appimage 2024-06-08 19:36:58 +08:00
MystiPanda b4349afb1b ci: codesign 2024-06-08 18:52:34 +08:00
dongchengjie 0cffe8e3f7 chore: disbale Meta+Q on macOS 2024-06-08 13:12:24 +08:00
Sukka 7a1f4f9abe refactor: simplify useConnectionSetting (#1141) 2024-06-07 17:23:53 +08:00
wonfen 5768b01786 fix: apple sign error again 2024-06-07 17:19:44 +08:00
MystiPanda 7e9079467f fix: sign error 2024-06-07 16:39:06 +08:00
MystiPanda 615e96922e fix: sign error 2024-06-07 16:37:58 +08:00
Eric Huang 8b94c452fb feat: make SettingItem clickable (#1138)
* feat: make `SettingItem` clickable

* clean up
2024-06-07 15:51:51 +08:00
Sukka 66dd510acc refactor: replace recoil (#1137) 2024-06-07 12:27:37 +08:00
wonfen 96e044566c Style: modify proxy pages 2024-06-07 10:32:27 +08:00
MystiPanda 726ad84d7f fix: sign error 2024-06-06 16:06:58 +08:00
wonfen 4fc422f880 chore: apple developer sign test 2024-06-06 15:21:53 +08:00
dongchengjie 1938024ab3 chore: test_delay trace 2024-06-05 19:17:43 +08:00
dongchengjie 05b1de6fd5 chore: theme colors to uppercase 2024-06-05 14:20:24 +08:00
dongchengjie 0bf8807b50 build: no legacy chunks 2024-06-05 09:04:08 +08:00
Sukka 23668ee74e fix(#1126): add Object.hasOwn polyfill (#1127) 2024-06-05 00:12:06 +08:00
dongchengjie 6bc5c5a808 fix: csp missing asset: 2024-06-04 15:26:39 +08:00
119 changed files with 5815 additions and 3944 deletions
+2 -5
View File
@@ -1,8 +1,5 @@
pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
cargo tauri build --target $INPUT_TARGET
else
cargo tauri build --target $INPUT_TARGET -b deb,rpm
fi
pnpm build --target $INPUT_TARGET
-2
View File
@@ -5,8 +5,6 @@ tar -Jxvf ./node-v20.10.0-linux-x64.tar.xz
export PATH=$(pwd)/node-v20.10.0-linux-x64/bin:$PATH
npm install pnpm -g
cargo install --git https://github.com/tauri-apps/tauri --branch 1.x tauri-cli
rustup target add "$INPUT_TARGET"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
+17 -16
View File
@@ -9,6 +9,10 @@ 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:
alpha:
@@ -42,16 +46,17 @@ jobs:
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@v3
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
@@ -66,6 +71,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: alpha
releaseName: "Clash Verge Rev Alpha"
@@ -115,17 +126,6 @@ jobs:
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v2
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body: "More new features are now supported."
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
@@ -164,16 +164,17 @@ jobs:
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@v3
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
@@ -250,7 +251,7 @@ jobs:
- MacOS apple M芯片: aarch64.dmg
### Linux
- Linux 64位: amd64.AppImage/amd64.deb/amd64.rpm
- Linux 64位: amd64.deb/amd64.rpm
- Linux 32位: i386.deb/i386.rpm
- Linux arm64架构: arm64.deb/aarch64.rpm
- Linux armv7架构: armhf.deb/armhfp.rpm
+11 -18
View File
@@ -39,16 +39,16 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v3
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
@@ -63,6 +63,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev v__VERSION__"
@@ -109,16 +115,6 @@ jobs:
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v2
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v2
with:
@@ -162,10 +158,9 @@ jobs:
with:
node-version: "20"
- uses: pnpm/action-setup@v3
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
@@ -224,10 +219,9 @@ jobs:
with:
node-version: "20"
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install
@@ -250,10 +244,9 @@ jobs:
with:
node-version: "20"
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install
+3 -5
View File
@@ -7,17 +7,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install
@@ -39,10 +38,9 @@ jobs:
with:
node-version: "20"
- uses: pnpm/action-setup@v2
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install
+92
View File
@@ -1,3 +1,95 @@
## v1.7.1
### Break Changes
- 更新后请务必重新导入所有订阅,包括 Remote 和 Local
- 此版本重构了 Merge/Script,更新前请先备份好自定义 Merge 和 Script(更新并不会删除配置文件,但是旧版 Merge 和 Script 在更新后无法从前端访问,备份以防万一)
- Merge 改名为 `扩展配置`,分为 `全局扩展配置` 和 `订阅扩展配置`,全局扩展配置对所有订阅生效,订阅扩展配置只对关联的订阅生效
- Script 改名为 `扩展脚本`,同样分为 `全局扩展脚本` 和 `订阅扩展脚本`
- 订阅扩展配置在订阅右键菜单里进入
- 执行优先级为: 全局扩展配置 -> 全局扩展脚本 -> 订阅扩展配置 ->订阅扩展脚本
- 扩展配置删除了 `prepend/append` 能力,请使用 右键订阅 -> `编辑规则`/`编辑节点`/`编辑代理组` 来代替
- MacOS 用户更新后请重新安装服务模式
### Features
- 升级内核到 1.18.6
- 移除内核授权,改为服务模式实现
- 自动填充本地订阅名称
- 添加重大更新处理逻辑
- 订阅单独指定扩展配置/脚本(需要重新导入订阅)
- 添加可视化规则编辑器(需要重新导入订阅)
- 编辑器新增工具栏按钮(格式化、最大化/最小化)
- WEBUI 使用最新版 metacubex,并解决无法自动登陆问问题
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
- 各种细节优化和界面性能优化
### Bugs Fixes
- 修复代理绕过格式检查
- 通过进程名称关闭进程
- 退出软件时恢复 DNS 设置
- 修复创建本地订阅时更新间隔无法保存
- 连接页面列宽无法调整
---
## v1.7.0
### Break Changes
- 此版本重构了 Merge/Script,更新前请先备份好自定义 Merge 和 Script(更新并不会删除配置文件,但是旧版 Merge 和 Script 在更新后无法从前端访问,备份以防万一)
- Merge 改名为 `扩展配置`,分为 `全局扩展配置` 和 `订阅扩展配置`,全局扩展配置对所有订阅生效,订阅扩展配置只对关联的订阅生效
- Script 改名为 `扩展脚本`,同样分为 `全局扩展脚本` 和 `订阅扩展脚本`
- 执行优先级为: 全局扩展配置 -> 全局扩展脚本 -> 订阅扩展配置 ->订阅扩展脚本
- MacOS 用户更新后请重新安装服务模式
### Features
- 移除内核授权,改为服务模式实现
- 自动填充本地订阅名称
- 添加重大更新处理逻辑
- 订阅单独指定扩展配置/脚本(需要重新导入订阅)
- 添加可视化规则编辑器(需要重新导入订阅)
- 编辑器新增工具栏按钮(格式化、最大化/最小化)
- WEBUI 使用最新版 metacubex,并解决无法自动登陆问问题
- 禁用部分 Webview2 快捷键
- 热键配置新增连接符 + 号
- 新增部分悬浮提示按钮,用于解释说明
- 当日志等级为`Debug`时(更改需重启软件生效),支持点击内存主动内存回收(绿色文字)
- 设置页面右上角新增 TG 频道链接
### Bugs Fixes
- 修复代理绕过格式检查
- 通过进程名称关闭进程
- 退出软件时恢复 DNS 设置
- 修复创建本地订阅时更新间隔无法保存
- 连接页面列宽无法调整
---
## v1.6.6
### Features
- MacOS 应用签名
- 删除 AppImage
- 应用更新对话框添加下载按钮
- 设置系统代理绕过时保留默认值
- 系统代理绕过设置输入格式检查
### Bugs Fixes
- MacOS 代理组图标无法显示
- RPM 包依赖缺失
---
## v1.6.5
### Features
+24 -19
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.6.5",
"version": "1.7.1",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",
@@ -24,47 +24,51 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.15.19",
"@mui/icons-material": "^5.15.21",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.15.19",
"@mui/x-data-grid": "^6.20.0",
"@tauri-apps/api": "^1.5.6",
"@mui/material": "^5.15.21",
"@mui/x-data-grid": "^7.8.0",
"@tauri-apps/api": "^1.6.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.0",
"axios": "^1.7.2",
"dayjs": "1.11.5",
"foxact": "^0.2.35",
"i18next": "^23.11.5",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"meta-json-schema": "1.18.5-alpha4",
"meta-json-schema": "1.18.6",
"monaco-editor": "^0.49.0",
"monaco-yaml": "^5.1.1",
"monaco-yaml": "^5.2.0",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.51.5",
"react-hook-form": "^7.52.1",
"react-i18next": "^13.5.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.23.1",
"react-monaco-editor": "^0.55.0",
"react-router-dom": "^6.24.0",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.7.11",
"recoil": "^0.7.7",
"swr": "^1.3.0",
"sockette": "^2.0.6",
"swr": "^2.2.5",
"tar": "^6.2.1",
"types-pac": "^1.0.2"
},
"devDependencies": {
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.5.14",
"@tauri-apps/cli": "^1.6.0",
"@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-legacy": "^5.4.1",
"@vitejs/plugin-react": "^4.3.0",
"adm-zip": "^0.5.13",
"@vitejs/plugin-react": "^4.3.1",
"adm-zip": "^0.5.14",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1",
@@ -72,10 +76,10 @@
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"sass": "^1.77.4",
"terser": "^5.31.0",
"typescript": "^5.4.5",
"vite": "^5.2.12",
"sass": "^1.77.6",
"terser": "^5.31.1",
"typescript": "^5.5.3",
"vite": "^5.3.2",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},
@@ -84,5 +88,6 @@
"semi": true,
"singleQuote": false,
"endOfLine": "lf"
}
},
"packageManager": "pnpm@9.1.4"
}
+1270 -1137
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -172,8 +172,8 @@ function clashMetaAlpha() {
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
return {
name: "clash-meta-alpha",
targetFile: `clash-meta-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
name: "verge-mihomo-alpha",
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
@@ -189,8 +189,8 @@ function clashMeta() {
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
return {
name: "clash-meta",
targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
name: "verge-mihomo",
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
@@ -434,13 +434,13 @@ const resolveEnableLoopback = () =>
const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
name: "clash-meta-alpha",
name: "verge-mihomo-alpha",
func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: "clash-meta",
name: "verge-mihomo",
func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,
+2 -2
View File
@@ -40,8 +40,8 @@ async function resolvePortable() {
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta-alpha.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(
path.join(
+2 -2
View File
@@ -39,8 +39,8 @@ async function resolvePortable() {
const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta-alpha.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo.exe"));
zip.addLocalFile(path.join(releaseDir, "verge-mihomo-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(configDir, ".config");
+3
View File
@@ -41,6 +41,7 @@ async function resolveUpdater() {
"windows-x86_64": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
"windows-x86": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" },
},
};
@@ -60,11 +61,13 @@ async function resolveUpdater() {
// win32 url
if (name.endsWith("x86_fixed_webview2-setup.nsis.zip")) {
updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x86_fixed_webview2-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig;
}
// win arm url
+12 -15
View File
@@ -46,11 +46,13 @@ async function resolveUpdater() {
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" },
"linux-x86": { signature: "", url: "" },
"linux-i686": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"linux-armv7": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
"windows-x86": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" },
},
};
@@ -72,11 +74,13 @@ async function resolveUpdater() {
// win32 url
if (name.endsWith("x64-setup.nsis.zip")) {
updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig;
}
// win arm url
@@ -106,29 +110,22 @@ async function resolveUpdater() {
// darwin url (aarch)
if (name.endsWith("aarch64.app.tar.gz")) {
updateData.platforms["darwin-aarch64"].url = browser_download_url;
// 使linux可以检查更新
updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url;
updateData.platforms["linux-x86"].url = browser_download_url;
updateData.platforms["linux-i686"].url = browser_download_url;
updateData.platforms["linux-aarch64"].url = browser_download_url;
updateData.platforms["linux-armv7"].url = browser_download_url;
}
// darwin signature (aarch)
if (name.endsWith("aarch64.app.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["darwin-aarch64"].signature = sig;
}
// linux x64 url
if (name.endsWith("amd64.AppImage.tar.gz")) {
updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url;
updateData.platforms["linux-x86"].url = browser_download_url;
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].url = browser_download_url;
updateData.platforms["linux-armv7"].url = browser_download_url;
}
// linux x64 signature
if (name.endsWith("amd64.AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms.linux.signature = sig;
updateData.platforms["linux-x86_64"].signature = sig;
updateData.platforms["linux-x86"].url = browser_download_url;
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
updateData.platforms["linux-i686"].url = browser_download_url;
updateData.platforms["linux-aarch64"].signature = sig;
updateData.platforms["linux-armv7"].signature = sig;
}
+204 -135
View File
@@ -123,9 +123,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-broadcast"
version = "0.7.0"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb"
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
dependencies = [
"event-listener 5.3.0",
"event-listener-strategy",
@@ -214,9 +214,9 @@ dependencies = [
[[package]]
name = "async-io"
version = "2.3.2"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964"
dependencies = [
"async-lock 3.4.0",
"cfg-if",
@@ -224,7 +224,7 @@ dependencies = [
"futures-io",
"futures-lite 2.3.0",
"parking",
"polling 3.7.0",
"polling 3.7.2",
"rustix 0.38.34",
"slab",
"tracing",
@@ -281,12 +281,12 @@ dependencies = [
[[package]]
name = "async-process"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d"
checksum = "f7eda79bbd84e29c2b308d1dc099d7de8dcc7035e48f4bf5dc4a531a44ff5e2a"
dependencies = [
"async-channel 2.3.1",
"async-io 2.3.2",
"async-io 2.3.3",
"async-lock 3.4.0",
"async-signal",
"async-task",
@@ -312,11 +312,11 @@ dependencies = [
[[package]]
name = "async-signal"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "329972aa325176e89114919f2a80fdae4f4c040f66a370b1a1159c6c0f94e7aa"
checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d"
dependencies = [
"async-io 2.3.2",
"async-io 2.3.3",
"async-lock 3.4.0",
"atomic-waker",
"cfg-if",
@@ -378,7 +378,8 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "auto-launch"
version = "0.5.0"
source = "git+https://github.com/zzzgydi/auto-launch?branch=main#2d94a103ca20652a3baf581ca2c296791c35c09b"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
dependencies = [
"dirs 4.0.0",
"thiserror",
@@ -393,9 +394,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "backtrace"
version = "0.3.72"
version = "0.3.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11"
checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
dependencies = [
"addr2line",
"cc",
@@ -645,9 +646,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytemuck"
version = "1.16.0"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
dependencies = [
"bytemuck_derive",
]
@@ -714,9 +715,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.98"
version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695"
[[package]]
name = "cesu8"
@@ -783,7 +784,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "1.6.5"
version = "1.7.1"
dependencies = [
"anyhow",
"auto-launch",
@@ -797,11 +798,11 @@ dependencies = [
"log4rs",
"nanoid",
"once_cell",
"open 5.1.3",
"open 5.1.4",
"parking_lot",
"percent-encoding",
"port_scanner",
"reqwest 0.12.4",
"reqwest 0.12.5",
"runas",
"serde",
"serde_json",
@@ -1203,15 +1204,15 @@ dependencies = [
[[package]]
name = "derive_more"
version = "0.99.17"
version = "0.99.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version 0.4.0",
"syn 1.0.109",
"syn 2.0.66",
]
[[package]]
@@ -1341,9 +1342,9 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
[[package]]
name = "dtoa-short"
version = "0.3.4"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74"
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
dependencies = [
"dtoa",
]
@@ -1369,7 +1370,7 @@ dependencies = [
"cc",
"memchr",
"rustc_version 0.4.0",
"toml 0.8.13",
"toml 0.8.14",
"vswhom",
"winreg 0.52.0",
]
@@ -1397,9 +1398,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
[[package]]
name = "enumflags2"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3278c9d5fb675e0a51dabcf4c0d355f692b064171535ba72361be1528a9d8e8d"
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
dependencies = [
"enumflags2_derive",
"serde",
@@ -1407,9 +1408,9 @@ dependencies = [
[[package]]
name = "enumflags2_derive"
version = "0.7.9"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4"
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
dependencies = [
"proc-macro2",
"quote",
@@ -1985,8 +1986,8 @@ dependencies = [
"aho-corasick 1.1.3",
"bstr",
"log 0.4.21",
"regex-automata 0.4.6",
"regex-syntax 0.8.3",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
]
[[package]]
@@ -2175,6 +2176,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "hex"
version = "0.4.3"
@@ -2249,12 +2256,12 @@ dependencies = [
[[package]]
name = "http-body-util"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"pin-project-lite",
@@ -2268,9 +2275,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.8.0"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
name = "httpdate"
@@ -2286,9 +2293,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.28"
version = "0.14.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33"
dependencies = [
"bytes",
"futures-channel",
@@ -2301,7 +2308,7 @@ dependencies = [
"httpdate",
"itoa 1.0.11",
"pin-project-lite",
"socket2 0.4.10",
"socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
@@ -2330,9 +2337,9 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.26.0"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155"
dependencies = [
"futures-util",
"http 1.1.0",
@@ -2343,6 +2350,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots",
]
[[package]]
@@ -2352,7 +2360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper 0.14.28",
"hyper 0.14.29",
"native-tls",
"tokio",
"tokio-native-tls",
@@ -2581,7 +2589,7 @@ dependencies = [
"globset",
"log 0.4.21",
"memchr",
"regex-automata 0.4.6",
"regex-automata 0.4.7",
"same-file",
"walkdir",
"winapi-util",
@@ -2692,7 +2700,7 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.9",
"libc",
"windows-sys 0.48.0",
]
@@ -2711,7 +2719,7 @@ checksum = "c416c05ba2a10240e022887617af3128fccdbf69713214da0fc81a5690d00df7"
dependencies = [
"ahash",
"once_cell",
"regex 1.10.4",
"regex 1.10.5",
]
[[package]]
@@ -3077,9 +3085,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.7.2"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmem"
@@ -3144,9 +3152,9 @@ checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
[[package]]
name = "miniz_oxide"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
dependencies = [
"adler",
"simd-adler32",
@@ -3389,7 +3397,7 @@ version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.9",
"libc",
]
@@ -3584,9 +3592,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.35.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e"
checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434"
dependencies = [
"memchr",
]
@@ -3615,9 +3623,9 @@ dependencies = [
[[package]]
name = "open"
version = "5.1.3"
version = "5.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb49fbd5616580e9974662cb96a3463da4476e649a7e4b258df0de065db0657"
checksum = "b5ca541f22b1c46d4bb9801014f234758ab4297e7870b904b6a8415b980a7388"
dependencies = [
"is-wsl",
"libc",
@@ -3695,9 +3703,9 @@ dependencies = [
[[package]]
name = "os_pipe"
version = "1.1.5"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9"
checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -3758,7 +3766,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.1",
"redox_syscall 0.5.2",
"smallvec",
"windows-targets 0.52.5",
]
@@ -4056,13 +4064,13 @@ dependencies = [
[[package]]
name = "polling"
version = "3.7.0"
version = "3.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3"
checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix 0.38.34",
"tracing",
@@ -4156,9 +4164,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.84"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
dependencies = [
"unicode-ident",
]
@@ -4178,6 +4186,53 @@ dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad"
dependencies = [
"bytes",
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustls",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "quinn-proto"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe"
dependencies = [
"bytes",
"rand 0.8.5",
"ring",
"rustc-hash",
"rustls",
"slab",
"thiserror",
"tinyvec",
"tracing",
]
[[package]]
name = "quinn-udp"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46"
dependencies = [
"libc",
"once_cell",
"socket2 0.5.7",
"tracing",
"windows-sys 0.52.0",
]
[[package]]
name = "quote"
version = "1.0.36"
@@ -4305,9 +4360,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
dependencies = [
"bitflags 2.5.0",
]
@@ -4338,14 +4393,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.4"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick 1.1.3",
"memchr",
"regex-automata 0.4.6",
"regex-syntax 0.8.3",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
]
[[package]]
@@ -4359,13 +4414,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick 1.1.3",
"memchr",
"regex-syntax 0.8.3",
"regex-syntax 0.8.4",
]
[[package]]
@@ -4385,9 +4440,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "regress"
@@ -4413,7 +4468,7 @@ dependencies = [
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper 0.14.29",
"hyper-tls 0.5.0",
"ipnet",
"js-sys",
@@ -4427,7 +4482,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
@@ -4443,9 +4498,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.4"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -4468,13 +4523,14 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile 2.1.2",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 1.0.1",
"system-configuration",
"tokio",
"tokio-native-tls",
@@ -4604,11 +4660,11 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.22.4"
version = "0.23.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402"
dependencies = [
"log 0.4.21",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
@@ -5176,9 +5232,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "subtle"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5"
[[package]]
name = "syn"
@@ -5208,6 +5264,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
[[package]]
name = "synstructure"
version = "0.13.1"
@@ -5291,7 +5353,7 @@ dependencies = [
"cfg-expr 0.15.8",
"heck 0.5.0",
"pkg-config",
"toml 0.8.13",
"toml 0.8.14",
"version-compare 0.2.0",
]
@@ -5363,9 +5425,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.40"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb"
checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909"
dependencies = [
"filetime",
"libc",
@@ -5380,8 +5442,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tauri"
version = "1.6.7"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e3b2c32de5bf5bed15a376064286643862b84e4e091d093f7bcdf1fda4b9758"
dependencies = [
"anyhow",
"base64 0.21.7",
@@ -5414,7 +5477,7 @@ dependencies = [
"png",
"rand 0.8.5",
"raw-window-handle",
"regex 1.10.4",
"regex 1.10.5",
"reqwest 0.11.27",
"rfd",
"semver 1.0.23",
@@ -5443,8 +5506,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "1.5.2"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5461,8 +5525,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.4.3"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1aed706708ff1200ec12de9cfbf2582b5d8ec05f6a7293911091effbd22036b"
dependencies = [
"base64 0.21.7",
"brotli",
@@ -5472,7 +5537,7 @@ dependencies = [
"png",
"proc-macro2",
"quote",
"regex 1.10.4",
"regex 1.10.5",
"semver 1.0.23",
"serde",
"serde_json",
@@ -5486,8 +5551,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.4.4"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88f831d2973ae4f81a706a0004e67dac87f2e4439973bbe98efbd73825d8ede"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5499,8 +5565,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.14.3"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3068ed62b63dedc705558f4248c7ecbd5561f0f8050949859ea0db2326f26012"
dependencies = [
"gtk",
"http 0.2.12",
@@ -5519,8 +5586,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.14.8"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c3db170233096aa30330feadcd895bf9317be97e624458560a20e814db7955"
dependencies = [
"arboard",
"cocoa 0.24.1",
@@ -5539,8 +5607,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.5.4"
source = "git+https://github.com/Pylogmon/tauri?branch=1.x#9c0b64965560b28c3d06963494a3e0d2e1bae9f4"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2826db448309d382dac14d520f0c0a40839b87b57b977e59cf5f296b3ace6a93"
dependencies = [
"brotli",
"ctor",
@@ -5651,7 +5720,7 @@ dependencies = [
"num-derive",
"num-traits",
"ordered-float",
"regex 1.10.4",
"regex 1.10.5",
"semver 0.11.0",
"sha2 0.9.9",
"signal-hook",
@@ -5837,9 +5906,9 @@ dependencies = [
[[package]]
name = "tokio-rustls"
version = "0.25.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls",
"rustls-pki-types",
@@ -5894,14 +5963,14 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.13"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.13",
"toml_edit 0.22.14",
]
[[package]]
@@ -5939,15 +6008,15 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.22.13"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.9",
"winnow 0.6.13",
]
[[package]]
@@ -6030,7 +6099,7 @@ dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex 1.10.4",
"regex 1.10.5",
"sharded-slab",
"smallvec",
"thread_local 1.1.8",
@@ -6175,9 +6244,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.0"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
@@ -6221,9 +6290,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
@@ -6329,7 +6398,7 @@ dependencies = [
"futures-util",
"headers",
"http 0.2.12",
"hyper 0.14.28",
"hyper 0.14.29",
"log 0.4.21",
"mime",
"mime_guess",
@@ -6570,9 +6639,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.26.1"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
dependencies = [
"rustls-pki-types",
]
@@ -6606,7 +6675,7 @@ version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7"
dependencies = [
"regex 1.10.4",
"regex 1.10.5",
"serde",
"serde_json",
"thiserror",
@@ -6803,9 +6872,9 @@ checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278"
[[package]]
name = "windows-result"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.5",
]
@@ -7092,9 +7161,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.9"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1"
dependencies = [
"memchr",
]
@@ -7255,12 +7324,12 @@ checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
[[package]]
name = "xdg-home"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e5a325c3cb8398ad6cf859c1135b25dd29e186679cf2da7581d9679f63b38e"
checksum = "ca91dcf8f93db085f3a0a29358cd0b9d670915468f4290e8b85d118a34211ab8"
dependencies = [
"libc",
"winapi",
"windows-sys 0.52.0",
]
[[package]]
@@ -7289,16 +7358,16 @@ dependencies = [
[[package]]
name = "zbus"
version = "4.2.2"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989c3977a7aafa97b12b9a35d21cdcff9b0d2289762b14683f45d66b1ba6c48f"
checksum = "23915fcb26e7a9a9dc05fd93a9870d336d6d032cd7e8cebf1c5c37666489fdd5"
dependencies = [
"async-broadcast",
"async-executor",
"async-fs 2.1.2",
"async-io 2.3.2",
"async-io 2.3.3",
"async-lock 3.4.0",
"async-process 2.2.2",
"async-process 2.2.3",
"async-recursion",
"async-task",
"async-trait",
@@ -7327,9 +7396,9 @@ dependencies = [
[[package]]
name = "zbus_macros"
version = "4.2.2"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe9de53245dcf426b7be226a4217dd5e339080e5d46e64a02d6e5dcbf90fca1"
checksum = "02bcca0b586d2f8589da32347b4784ba424c4891ed86aa5b50d5e88f6b2c4f5d"
dependencies = [
"proc-macro-crate 3.1.0",
"proc-macro2",
+4 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "1.6.5"
version = "1.7.1"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -10,7 +10,7 @@ edition = "2021"
build = "build.rs"
[build-dependencies]
tauri-build = { git="https://github.com/Pylogmon/tauri",branch = "1.x", features = [] }
tauri-build = { version="1", features = [] }
[dependencies]
warp = "0.3"
@@ -30,14 +30,14 @@ once_cell = "1.19"
port_scanner = "0.1.5"
delay_timer = "0.11"
parking_lot = "0.12"
auto-launch = "0.5.0"
percent-encoding = "2.3.1"
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { git="https://github.com/Pylogmon/tauri",branch = "1.x", 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"] }
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
deelevate = "0.2.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+1 -10
View File
@@ -181,15 +181,6 @@ pub async fn restart_sidecar() -> CmdResult {
wrap_err!(CoreManager::global().run_core().await)
}
#[tauri::command]
pub fn grant_permission(_core: String) -> CmdResult {
#[cfg(any(target_os = "macos", target_os = "linux"))]
return wrap_err!(manager::grant_permission(_core));
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
return Err("Unsupported target".into());
}
/// get the system proxy
#[tauri::command]
pub fn get_sys_proxy() -> CmdResult<Mapping> {
@@ -327,7 +318,7 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
Err(err) => Err(err.to_string()),
}
} else {
return Err("file not found".to_string());
Err("file not found".to_string())
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ pub struct IClashTemp(pub Mapping);
impl IClashTemp {
pub fn new() -> Self {
let template = Self::template();
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
match dirs::clash_path().and_then(|path| help::read_mapping(&path)) {
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
@@ -33,7 +33,7 @@ impl IClashTemp {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("stack".into(), "gvisor".into());
tun.insert("device".into(), "Meta".into());
tun.insert("device".into(), "Mihomo".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
+21 -4
View File
@@ -1,5 +1,6 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
config::PrfItem,
enhance,
utils::{dirs, help},
};
@@ -46,8 +47,24 @@ impl Config {
}
/// 初始化订阅
pub fn init_config() -> Result<()> {
crate::log_err!(Self::generate());
pub async fn init_config() -> Result<()> {
if Self::profiles()
.data()
.get_item(&"Merge".to_string())
.is_err()
{
let merge_item = PrfItem::from_merge(Some("Merge".to_string()))?;
Self::profiles().data().append_item(merge_item.clone())?;
}
if Self::profiles()
.data()
.get_item(&"Script".to_string())
.is_err()
{
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?;
}
crate::log_err!(Self::generate().await);
if let Err(err) = Self::generate_file(ConfigType::Run) {
log::error!(target: "app", "{err}");
@@ -83,8 +100,8 @@ impl Config {
}
/// 生成订阅存好
pub fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance();
pub async fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance().await;
*Config::runtime().draft() = IRuntime {
config: Some(config),
+1
View File
@@ -1,4 +1,5 @@
mod clash;
#[allow(clippy::module_inception)]
mod config;
mod draft;
mod prfitem;
+202 -42
View File
@@ -94,6 +94,16 @@ pub struct PrfOption {
/// default is `false`
#[serde(skip_serializing_if = "Option::is_none")]
pub danger_accept_invalid_certs: Option<bool>,
pub merge: Option<String>,
pub script: Option<String>,
pub rules: Option<String>,
pub proxies: Option<String>,
pub groups: Option<String>,
}
impl PrfOption {
@@ -103,8 +113,15 @@ impl PrfOption {
a.user_agent = b.user_agent.or(a.user_agent);
a.with_proxy = b.with_proxy.or(a.with_proxy);
a.self_proxy = b.self_proxy.or(a.self_proxy);
a.danger_accept_invalid_certs = b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs);
a.danger_accept_invalid_certs = b
.danger_accept_invalid_certs
.or(a.danger_accept_invalid_certs);
a.update_interval = b.update_interval.or(a.update_interval);
a.merge = b.merge.or(a.merge);
a.script = b.script.or(a.script);
a.rules = b.rules.or(a.rules);
a.proxies = b.proxies.or(a.proxies);
a.groups = b.groups.or(a.groups);
Some(a)
}
t => t.0.or(t.1),
@@ -133,17 +150,7 @@ impl PrfItem {
"local" => {
let name = item.name.unwrap_or("Local File".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc, file_data)
}
"merge" => {
let name = item.name.unwrap_or("Merge".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_merge(name, desc)
}
"script" => {
let name = item.name.unwrap_or("Script".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
PrfItem::from_local(name, desc, file_data, item.option)
}
typ => bail!("invalid profile item type \"{typ}\""),
}
@@ -151,10 +158,49 @@ impl PrfItem {
/// ## Local type
/// create a new item from name/desc
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
let uid = help::get_uid("l");
pub fn from_local(
name: String,
desc: String,
file_data: Option<String>,
option: Option<PrfOption>,
) -> Result<PrfItem> {
let uid = help::get_uid("L");
let file = format!("{uid}.yaml");
let opt_ref = option.as_ref();
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone());
let mut rules = opt_ref.and_then(|o| o.rules.clone());
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
let mut groups = opt_ref.and_then(|o| o.groups.clone());
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
Config::profiles().data().append_item(merge_item.clone())?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
Config::profiles().data().append_item(script_item.clone())?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
Config::profiles().data().append_item(rules_item.clone())?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
Config::profiles()
.data()
.append_item(proxies_item.clone())?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
Config::profiles().data().append_item(groups_item.clone())?;
groups = groups_item.uid;
}
Ok(PrfItem {
uid: Some(uid),
itype: Some("local".into()),
@@ -164,7 +210,15 @@ impl PrfItem {
url: None,
selected: None,
extra: None,
option: None,
option: Some(PrfOption {
update_interval,
merge,
script,
rules,
proxies,
groups,
..PrfOption::default()
}),
home: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
@@ -182,10 +236,15 @@ impl PrfItem {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs = opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
let accept_invalid_certs =
opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut merge = opt_ref.and_then(|o| o.merge.clone());
let mut script = opt_ref.and_then(|o| o.script.clone());
let mut rules = opt_ref.and_then(|o| o.rules.clone());
let mut proxies = opt_ref.and_then(|o| o.proxies.clone());
let mut groups = opt_ref.and_then(|o| o.groups.clone());
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
// 使用软件自己的代理
@@ -279,17 +338,11 @@ impl PrfItem {
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
),
};
let option = match update_interval {
Some(val) => Some(PrfOption {
update_interval: Some(val),
..PrfOption::default()
}),
let update_interval = match update_interval {
Some(val) => Some(val),
None => match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Ok(val) => Some(val * 60), // hour -> min
Err(_) => None,
},
None => None,
@@ -300,11 +353,11 @@ impl PrfItem {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
Some(str_value.to_string())
},
}
None => None,
};
let uid = help::get_uid("r");
let uid = help::get_uid("R");
let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?;
@@ -320,6 +373,34 @@ impl PrfItem {
bail!("profile does not contain `proxies` or `proxy-providers`");
}
if merge.is_none() {
let merge_item = PrfItem::from_merge(None)?;
Config::profiles().data().append_item(merge_item.clone())?;
merge = merge_item.uid;
}
if script.is_none() {
let script_item = PrfItem::from_script(None)?;
Config::profiles().data().append_item(script_item.clone())?;
script = script_item.uid;
}
if rules.is_none() {
let rules_item = PrfItem::from_rules()?;
Config::profiles().data().append_item(rules_item.clone())?;
rules = rules_item.uid;
}
if proxies.is_none() {
let proxies_item = PrfItem::from_proxies()?;
Config::profiles()
.data()
.append_item(proxies_item.clone())?;
proxies = proxies_item.uid;
}
if groups.is_none() {
let groups_item = PrfItem::from_groups()?;
Config::profiles().data().append_item(groups_item.clone())?;
groups = groups_item.uid;
}
Ok(PrfItem {
uid: Some(uid),
itype: Some("remote".into()),
@@ -329,7 +410,15 @@ impl PrfItem {
url: Some(url.into()),
selected: None,
extra,
option,
option: Some(PrfOption {
update_interval,
merge,
script,
rules,
proxies,
groups,
..PrfOption::default()
}),
home,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()),
@@ -338,15 +427,20 @@ impl PrfItem {
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
pub fn from_merge(uid: Option<String>) -> Result<PrfItem> {
let mut id = help::get_uid("m");
let mut template = tmpl::ITEM_MERGE_EMPTY.into();
if let Some(uid) = uid {
id = uid;
template = tmpl::ITEM_MERGE.into();
}
let file = format!("{id}.yaml");
Ok(PrfItem {
uid: Some(uid),
uid: Some(id),
itype: Some("merge".into()),
name: Some(name),
desc: Some(desc),
name: None,
desc: None,
file: Some(file),
url: None,
selected: None,
@@ -354,21 +448,24 @@ impl PrfItem {
option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_MERGE.into()),
file_data: Some(template),
})
}
/// ## Script type (enhance)
/// create the enhanced item by using javascript quick.js
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("s");
let file = format!("{uid}.js"); // js ext
pub fn from_script(uid: Option<String>) -> Result<PrfItem> {
let mut id = help::get_uid("s");
if let Some(uid) = uid {
id = uid;
}
let file = format!("{id}.js"); // js ext
Ok(PrfItem {
uid: Some(uid),
uid: Some(id),
itype: Some("script".into()),
name: Some(name),
desc: Some(desc),
name: None,
desc: None,
file: Some(file),
url: None,
home: None,
@@ -380,6 +477,69 @@ impl PrfItem {
})
}
/// ## Rules type (enhance)
pub fn from_rules() -> Result<PrfItem> {
let uid = help::get_uid("r");
let file = format!("{uid}.yaml"); // yaml ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("rules".into()),
name: None,
desc: None,
file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_RULES.into()),
})
}
/// ## Proxies type (enhance)
pub fn from_proxies() -> Result<PrfItem> {
let uid = help::get_uid("p");
let file = format!("{uid}.yaml"); // yaml ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("proxies".into()),
name: None,
desc: None,
file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_PROXIES.into()),
})
}
/// ## Groups type (enhance)
pub fn from_groups() -> Result<PrfItem> {
let uid = help::get_uid("g");
let file = format!("{uid}.yaml"); // yaml ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("groups".into()),
name: None,
desc: None,
file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_GROUPS.into()),
})
}
/// get the file data
pub fn read_file(&self) -> Result<String> {
if self.file.is_none() {
+169 -12
View File
@@ -1,4 +1,4 @@
use super::prfitem::PrfItem;
use super::{prfitem::PrfItem, PrfOption};
use crate::utils::{dirs, help};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
@@ -11,9 +11,6 @@ pub struct IProfiles {
/// same as PrfConfig.current
pub current: Option<String>,
/// same as PrfConfig.chain
pub chain: Option<Vec<String>>,
/// profile list
pub items: Option<Vec<PrfItem>>,
}
@@ -80,10 +77,6 @@ impl IProfiles {
}
}
if let Some(chain) = patch.chain {
self.chain = Some(chain);
}
Ok(())
}
@@ -212,6 +205,7 @@ impl IProfiles {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home;
each.option = PrfOption::merge(each.option.clone(), item.option);
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
@@ -243,9 +237,19 @@ impl IProfiles {
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
let item = self.get_item(&uid)?;
let merge_uid = item.option.as_ref().and_then(|e| e.merge.clone());
let script_uid = item.option.as_ref().and_then(|e| e.script.clone());
let rules_uid = item.option.as_ref().and_then(|e| e.rules.clone());
let proxies_uid = item.option.as_ref().and_then(|e| e.proxies.clone());
let groups_uid = item.option.as_ref().and_then(|e| e.groups.clone());
let mut items = self.items.take().unwrap_or_default();
let mut index = None;
let mut merge_index = None;
let mut script_index = None;
let mut rules_index = None;
let mut proxies_index = None;
let mut groups_index = None;
// get the index
for (i, _) in items.iter().enumerate() {
@@ -254,7 +258,6 @@ impl IProfiles {
break;
}
}
if let Some(index) = index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
@@ -265,7 +268,91 @@ impl IProfiles {
});
}
}
// get the merge index
for (i, _) in items.iter().enumerate() {
if items[i].uid == merge_uid {
merge_index = Some(i);
break;
}
}
if let Some(index) = merge_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// get the script index
for (i, _) in items.iter().enumerate() {
if items[i].uid == script_uid {
script_index = Some(i);
break;
}
}
if let Some(index) = script_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// get the rules index
for (i, _) in items.iter().enumerate() {
if items[i].uid == rules_uid {
rules_index = Some(i);
break;
}
}
if let Some(index) = rules_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// get the proxies index
for (i, _) in items.iter().enumerate() {
if items[i].uid == proxies_uid {
proxies_index = Some(i);
break;
}
}
if let Some(index) = proxies_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// get the groups index
for (i, _) in items.iter().enumerate() {
if items[i].uid == groups_uid {
groups_index = Some(i);
break;
}
}
if let Some(index) = groups_index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// delete the original uid
if current == uid {
self.current = match !items.is_empty() {
@@ -288,11 +375,81 @@ impl IProfiles {
Some(file) => dirs::app_profiles_dir()?.join(file),
None => bail!("failed to get the file field"),
};
return help::read_merge_mapping(&file_path);
return help::read_mapping(&file_path);
}
bail!("failed to find the current profile \"uid:{current}\"");
}
_ => Ok(Mapping::new()),
}
}
/// 获取current指向的订阅的merge
pub fn current_merge(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let merge = item.option.as_ref().and_then(|e| e.merge.clone());
return merge;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的script
pub fn current_script(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let script = item.option.as_ref().and_then(|e| e.script.clone());
return script;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的rules
pub fn current_rules(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let rules = item.option.as_ref().and_then(|e| e.rules.clone());
return rules;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的proxies
pub fn current_proxies(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let proxies = item.option.as_ref().and_then(|e| e.proxies.clone());
return proxies;
}
None
}
_ => None,
}
}
/// 获取current指向的订阅的groups
pub fn current_groups(&self) -> Option<String> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let groups = item.option.as_ref().and_then(|e| e.groups.clone());
return groups;
}
None
}
_ => None,
}
}
}
+6 -1
View File
@@ -75,6 +75,9 @@ pub struct IVerge {
/// enable proxy guard
pub enable_proxy_guard: Option<bool>,
/// always use default bypass
pub use_default_bypass: Option<bool>,
/// set system proxy bypass
pub system_proxy_bypass: Option<String>,
@@ -198,7 +201,7 @@ impl IVerge {
pub fn template() -> Self {
Self {
clash_core: Some("clash-meta".into()),
clash_core: Some("verge-mihomo".into()),
language: Some("zh".into()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
@@ -235,6 +238,7 @@ impl IVerge {
verge_port: Some(7899),
verge_http_enabled: Some(true),
enable_proxy_guard: Some(false),
use_default_bypass: Some(true),
proxy_guard_duration: Some(30),
auto_close_connection: Some(true),
auto_check_update: Some(true),
@@ -297,6 +301,7 @@ impl IVerge {
patch!(verge_http_enabled);
patch!(enable_system_proxy);
patch!(enable_proxy_guard);
patch!(use_default_bypass);
patch!(system_proxy_bypass);
patch!(proxy_guard_duration);
patch!(proxy_auto_config);
+1
View File
@@ -85,6 +85,7 @@ fn clash_client_info() -> Result<(String, HeaderMap)> {
}
/// 缩短clash的日志
#[allow(dead_code)]
pub fn parse_log(log: String) -> String {
if log.starts_with("time=") && log.len() > 33 {
return (log[33..]).to_owned();
+67 -79
View File
@@ -1,13 +1,13 @@
use super::service;
use super::{clash_api, logger::Logger};
use crate::config::*;
use crate::core::{clash_api, handle, logger::Logger, service};
use crate::log_err;
use crate::{config::*, utils::dirs};
use anyhow::{bail, Context, Result};
use crate::utils::dirs;
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use serde_yaml::Mapping;
use std::{fs, io::Write, sync::Arc, time::Duration};
use sysinfo::{Pid, System};
use std::{sync::Arc, time::Duration};
use sysinfo::System;
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep;
@@ -30,21 +30,6 @@ impl CoreManager {
}
pub fn init(&self) -> Result<()> {
// kill old clash process
let _ = dirs::clash_pid_path()
.and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
.and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
.map(|pid| {
let mut system = System::new();
system.refresh_all();
if let Some(proc) = system.process(Pid::from_u32(pid)) {
if proc.name().contains("clash") {
log::debug!(target: "app", "kill old clash process");
proc.kill();
}
}
});
tauri::async_runtime::spawn(async {
// 启动clash
log_err!(Self::global().run_core().await);
@@ -87,14 +72,26 @@ impl CoreManager {
#[allow(unused_mut)]
let mut should_kill = match self.sidecar.lock().take() {
Some(child) => {
log::debug!(target: "app", "stop the core by sidecar");
let _ = child.kill();
true
}
Some(_) => true,
None => false,
};
// 关闭tun模式
let mut disable = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
disable.insert("tun".into(), tun.into());
log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await;
let mut system = System::new();
system.refresh_all();
let procs = system.processes_by_name("verge-mihomo");
for proc in procs {
log::debug!(target: "app", "kill all clash process");
proc.kill();
}
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await);
@@ -116,12 +113,12 @@ impl CoreManager {
// 服务模式启动失败就直接运行sidecar
log::debug!(target: "app", "try to run core in service mode");
match (|| async {
let res = async {
service::check_service().await?;
service::run_core_by_service(&config_path).await
})()
.await
{
}
.await;
match res {
Ok(_) => return Ok(()),
Err(err) => {
// 修改这个值,免得stop出错
@@ -135,31 +132,29 @@ impl CoreManager {
let app_dir = dirs::path_to_str(&app_dir)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
let is_clash = clash_core == "clash";
let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let config_path = dirs::path_to_str(&config_path)?;
let args = match clash_core.as_str() {
"clash-meta" => vec!["-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-d", app_dir, "-f", config_path],
_ => vec!["-d", app_dir, "-f", config_path],
};
let args = vec!["-d", app_dir, "-f", config_path];
let cmd = Command::new_sidecar(clash_core)?;
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
// 将pid写入文件中
crate::log_err!((|| {
let pid = cmd_child.pid();
let path = dirs::clash_pid_path()?;
fs::File::create(path)
.context("failed to create the pid file")?
.write(format!("{pid}").as_bytes())
.context("failed to write pid to the file")?;
<Result<()>>::Ok(())
})());
let mut sidecar = self.sidecar.lock();
*sidecar = Some(cmd_child);
drop(sidecar);
@@ -168,25 +163,19 @@ impl CoreManager {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
if is_clash {
let stdout = clash_api::parse_log(line.clone());
log::info!(target: "app", "[clash]: {stdout}");
} else {
log::info!(target: "app", "[clash]: {line}");
};
log::info!(target: "app", "[mihomo]: {line}");
Logger::global().set_log(line);
}
CommandEvent::Stderr(err) => {
// let stdout = clash_api::parse_log(err.clone());
log::error!(target: "app", "[clash]: {err}");
log::error!(target: "app", "[mihomo]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Error(err) => {
log::error!(target: "app", "[clash]: {err}");
log::error!(target: "app", "[mihomo]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Terminated(_) => {
log::info!(target: "app", "clash core terminated");
log::info!(target: "app", "mihomo core terminated");
let _ = CoreManager::global().recover_core();
break;
}
@@ -206,9 +195,7 @@ impl CoreManager {
}
// 清空原来的sidecar值
if let Some(sidecar) = self.sidecar.lock().take() {
let _ = sidecar.kill();
}
let _ = self.sidecar.lock().take();
tauri::async_runtime::spawn(async move {
// 6秒之后再查看服务是否正常 (时间随便搞的)
@@ -232,29 +219,30 @@ impl CoreManager {
}
/// 停止核心运行
pub fn stop_core(&self) -> Result<()> {
pub async fn stop_core(&self) -> Result<()> {
// 关闭tun模式
tauri::async_runtime::block_on(async move {
let mut disable = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
disable.insert("tun".into(), tun.into());
log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await;
});
let mut disable = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
disable.insert("tun".into(), tun.into());
log::debug!(target: "app", "disable tun mode");
let _ = clash_api::patch_configs(&disable).await;
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move {
log_err!(service::stop_core_by_service().await);
});
log_err!(service::stop_core_by_service().await);
return Ok(());
}
let mut sidecar = self.sidecar.lock();
if let Some(child) = sidecar.take() {
log::debug!(target: "app", "stop the core by sidecar");
let _ = child.kill();
let _ = sidecar.take();
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();
}
Ok(())
}
@@ -262,7 +250,7 @@ impl CoreManager {
/// 切换核心
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
const CLASH_CORES: [&str; 2] = ["clash-meta", "clash-meta-alpha"];
const CLASH_CORES: [&str; 2] = ["verge-mihomo", "verge-mihomo-alpha"];
if !CLASH_CORES.contains(&clash_core.as_str()) {
bail!("invalid clash core name \"{clash_core}\"");
@@ -273,7 +261,7 @@ impl CoreManager {
Config::verge().draft().clash_core = Some(clash_core);
// 更新订阅
Config::generate()?;
Config::generate().await?;
self.check_config()?;
@@ -301,7 +289,7 @@ impl CoreManager {
log::debug!(target: "app", "try to update clash config");
// 更新订阅
Config::generate()?;
Config::generate().await?;
// 检查订阅是否正常
self.check_config()?;
+2 -2
View File
@@ -43,7 +43,7 @@ impl Hotkey {
}
}
}
*self.current.lock() = hotkeys.clone();
self.current.lock().clone_from(hotkeys);
}
Ok(())
@@ -142,7 +142,7 @@ impl Hotkey {
});
new_map.iter().for_each(|(&key, &func)| {
if old_map.get(key).is_none() {
if !old_map.contains_key(key) {
add_list.push((key, func));
}
});
-47
View File
@@ -1,47 +0,0 @@
/// 给clash内核的tun模式授权
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn grant_permission(core: String) -> anyhow::Result<()> {
use std::process::Command;
use tauri::utils::platform::current_exe;
let path = current_exe()?.with_file_name(core).canonicalize()?;
let path = path.display().to_string();
log::debug!("grant_permission path: {path}");
#[cfg(target_os = "macos")]
let output = {
let path = path.replace(' ', "\\\\ ");
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
Command::new("osascript")
.args(vec!["-e", &command])
.output()?
};
#[cfg(target_os = "linux")]
let output = {
let path = path.replace(' ', "\\ "); // 避免路径中有空格
let shell = format!("setcap cap_net_bind_service,cap_net_admin,cap_dac_override=+ep {path}");
let sudo = match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
};
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
};
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
anyhow::bail!("{stderr}");
}
}
+2 -2
View File
@@ -1,13 +1,13 @@
pub mod clash_api;
#[allow(clippy::module_inception)]
mod core;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod manager;
pub mod service;
pub mod sysopt;
pub mod timer;
pub mod tray;
pub mod service;
pub mod win_uwp;
pub use self::core::*;
+57 -2
View File
@@ -1,4 +1,5 @@
use crate::config::Config;
use crate::config::{Config, IVerge};
use crate::core::handle;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
@@ -237,7 +238,21 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
}
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());
// compatibility
if clash_core.contains("clash") {
clash_core = "verge-mihomo".to_string();
Config::verge().draft().patch_config(IVerge {
clash_core: Some("verge-mihomo".to_string()),
..IVerge::default()
});
Config::verge().apply();
match Config::verge().data().save_file() {
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
}
let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
@@ -297,3 +312,43 @@ pub(super) async fn stop_core_by_service() -> Result<()> {
Ok(())
}
/// set dns by service
pub async fn set_dns_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/set_dns");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// unset dns by service
pub async fn unset_dns_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/unset_dns");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
+53 -48
View File
@@ -41,6 +41,42 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172
static DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
fn get_bypass() -> String {
// let bypass = DEFAULT_BYPASS.to_string();
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
let res = {
let verge = Config::verge();
let verge = verge.latest();
verge.system_proxy_bypass.clone()
};
let custom_bypass = match res {
Some(bypass) => bypass,
None => "".to_string(),
};
#[cfg(target_os = "windows")]
let bypass = if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else {
if use_default {
format!("{};{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
#[cfg(not(target_os = "windows"))]
let bypass = if custom_bypass.is_empty() {
DEFAULT_BYPASS.to_string()
} else {
if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
bypass
}
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
@@ -63,12 +99,11 @@ impl Sysopt {
.unwrap_or(Config::clash().data().get_mixed_port());
let pac_port = IVerge::get_singleton_port();
let (enable, bypass, pac) = {
let (enable, pac) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.system_proxy_bypass.clone(),
verge.proxy_auto_config.unwrap_or(false),
)
};
@@ -76,16 +111,7 @@ impl Sysopt {
enable,
host: String::from("127.0.0.1"),
port,
bypass: match bypass {
Some(bypass) => {
if bypass.is_empty() {
DEFAULT_BYPASS.into()
} else {
bypass
}
}
None => DEFAULT_BYPASS.into(),
},
bypass: get_bypass(),
};
let mut auto = Autoproxy {
enable,
@@ -127,28 +153,26 @@ impl Sysopt {
let mut cur_autoproxy = self.cur_autoproxy.lock();
let old_autoproxy = self.old_autoproxy.lock();
let (enable, bypass, pac) = {
let (enable, pac) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.system_proxy_bypass.clone(),
verge.proxy_auto_config.unwrap_or(false),
)
};
if pac {
if cur_autoproxy.is_none() || old_autoproxy.is_none() {
drop(cur_autoproxy);
drop(old_autoproxy);
return self.init_sysproxy();
}
} else {
if cur_sysproxy.is_none() || old_sysproxy.is_none() {
drop(cur_sysproxy);
drop(old_sysproxy);
return self.init_sysproxy();
}
if pac && (cur_autoproxy.is_none() || old_autoproxy.is_none()) {
drop(cur_autoproxy);
drop(old_autoproxy);
return self.init_sysproxy();
}
if !pac && (cur_sysproxy.is_none() || old_sysproxy.is_none()) {
drop(cur_sysproxy);
drop(old_sysproxy);
return self.init_sysproxy();
}
let port = Config::verge()
.latest()
.verge_mixed_port
@@ -156,16 +180,7 @@ impl Sysopt {
let pac_port = IVerge::get_singleton_port();
let mut sysproxy = cur_sysproxy.take().unwrap();
sysproxy.bypass = match bypass {
Some(bypass) => {
if bypass.is_empty() {
DEFAULT_BYPASS.into()
} else {
bypass
}
}
None => DEFAULT_BYPASS.into(),
};
sysproxy.bypass = get_bypass();
sysproxy.port = port;
let mut autoproxy = cur_autoproxy.take().unwrap();
@@ -349,14 +364,13 @@ impl Sysopt {
loop {
sleep(Duration::from_secs(wait_secs)).await;
let (enable, guard, guard_duration, bypass, pac) = {
let (enable, guard, guard_duration, pac) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
verge.proxy_guard_duration.unwrap_or(10),
verge.system_proxy_bypass.clone(),
verge.proxy_auto_config.unwrap_or(false),
)
};
@@ -389,16 +403,7 @@ impl Sysopt {
enable: true,
host: "127.0.0.1".into(),
port,
bypass: match bypass {
Some(bypass) => {
if bypass.is_empty() {
DEFAULT_BYPASS.into()
} else {
bypass
}
}
None => DEFAULT_BYPASS.into(),
},
bypass: get_bypass(),
};
log_err!(sysproxy.set_system_proxy());
+1 -1
View File
@@ -88,7 +88,7 @@ impl Tray {
),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")))
}
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
+24 -8
View File
@@ -1,3 +1,4 @@
use super::SeqMap;
use crate::{
config::PrfItem,
utils::{dirs, help},
@@ -15,6 +16,9 @@ pub struct ChainItem {
pub enum ChainType {
Merge(Mapping),
Script(String),
Rules(SeqMap),
Proxies(SeqMap),
Groups(SeqMap),
}
#[derive(Debug, Clone)]
@@ -43,7 +47,19 @@ impl From<&PrfItem> for Option<ChainItem> {
}),
"merge" => Some(ChainItem {
uid,
data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
data: ChainType::Merge(help::read_mapping(&path).ok()?),
}),
"rules" => Some(ChainItem {
uid,
data: ChainType::Rules(help::read_seq_map(&path).ok()?),
}),
"proxies" => Some(ChainItem {
uid,
data: ChainType::Proxies(help::read_seq_map(&path).ok()?),
}),
"groups" => Some(ChainItem {
uid,
data: ChainType::Groups(help::read_seq_map(&path).ok()?),
}),
_ => None,
}
@@ -88,13 +104,13 @@ impl ChainItem {
impl ChainSupport {
pub fn is_support(&self, core: Option<&String>) -> bool {
match core {
Some(core) => match (self, core.as_str()) {
(ChainSupport::All, _) => true,
(ChainSupport::Clash, "clash") => true,
(ChainSupport::ClashMeta, "clash-meta") => true,
(ChainSupport::ClashMetaAlpha, "clash-meta-alpha") => true,
_ => false,
},
Some(core) => matches!(
(self, core.as_str()),
(ChainSupport::All, _)
| (ChainSupport::Clash, "clash")
| (ChainSupport::ClashMeta, "verge-mihomo")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
),
None => true,
}
}
+2 -19
View File
@@ -11,8 +11,8 @@ pub const HANDLE_FIELDS: [&str; 11] = [
"allow-lan",
"log-level",
"ipv6",
"secret",
"external-controller",
"secret",
];
pub const DEFAULT_FIELDS: [&str; 5] = [
@@ -23,19 +23,6 @@ pub const DEFAULT_FIELDS: [&str; 5] = [
"rules",
];
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
let mut ret = Mapping::new();
for (key, value) in config.into_iter() {
if let Some(key) = key.as_str() {
if filter.contains(&key.to_string()) {
ret.insert(Value::from(key), value);
}
}
}
ret
}
pub fn use_lowercase(config: Mapping) -> Mapping {
let mut ret = Mapping::new();
@@ -60,11 +47,7 @@ pub fn use_sort(config: Mapping) -> Mapping {
let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect();
let config_keys: HashSet<&str> = config
.keys()
.filter_map(|e| e.as_str())
.into_iter()
.collect();
let config_keys: HashSet<&str> = config.keys().filter_map(|e| e.as_str()).collect();
config_keys.difference(&supported_keys).for_each(|&key| {
let key = Value::from(key);
+6 -50
View File
@@ -1,18 +1,9 @@
use super::{use_filter, use_lowercase};
use serde_yaml::{self, Mapping, Sequence, Value};
const MERGE_FIELDS: [&str; 6] = [
"prepend-rules",
"append-rules",
"prepend-proxies",
"append-proxies",
"prepend-proxy-groups",
"append-proxy-groups",
];
use super::use_lowercase;
use serde_yaml::{self, Mapping, Value};
fn deep_merge(a: &mut Value, b: &Value) {
match (a, b) {
(&mut Value::Mapping(ref mut a), &Value::Mapping(ref b)) => {
(&mut Value::Mapping(ref mut a), Value::Mapping(b)) => {
for (k, v) in b {
deep_merge(a.entry(k.clone()).or_insert(Value::Null), v);
}
@@ -23,47 +14,12 @@ fn deep_merge(a: &mut Value, b: &Value) {
pub fn use_merge(merge: Mapping, config: Mapping) -> Mapping {
let mut config = Value::from(config);
let mut merge_without_append = use_lowercase(merge.clone());
for key in MERGE_FIELDS {
merge_without_append.remove(key).unwrap_or_default();
}
deep_merge(&mut config, &Value::from(merge_without_append));
let merge = use_lowercase(merge.clone());
let mut config = config.as_mapping().unwrap().clone();
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
let merge = use_filter(merge, &merge_list.collect());
deep_merge(&mut config, &Value::from(merge));
["rules", "proxies", "proxy-groups"]
.iter()
.for_each(|key_str| {
let key_val = Value::from(key_str.to_string());
let config = config.as_mapping().unwrap().clone();
let mut list = Sequence::default();
list = config.get(&key_val).map_or(list.clone(), |val| {
val.as_sequence().map_or(list, |v| v.clone())
});
let pre_key = Value::from(format!("prepend-{key_str}"));
let post_key = Value::from(format!("append-{key_str}"));
if let Some(pre_val) = merge.get(&pre_key) {
if pre_val.is_sequence() {
let mut pre_val = pre_val.as_sequence().unwrap().clone();
pre_val.extend(list);
list = pre_val;
}
}
if let Some(post_val) = merge.get(&post_key) {
if post_val.is_sequence() {
list.extend(post_val.as_sequence().unwrap().clone());
}
}
if !list.is_empty() {
config.insert(key_val, Value::from(list));
}
});
config
}
+136 -36
View File
@@ -2,14 +2,17 @@ mod chain;
pub mod field;
mod merge;
mod script;
pub mod seq;
mod tun;
use self::chain::*;
use self::field::*;
use self::merge::*;
use self::script::*;
use self::seq::*;
use self::tun::*;
use crate::config::Config;
use crate::utils::tmpl;
use serde_yaml::Mapping;
use std::collections::HashMap;
use std::collections::HashSet;
@@ -18,7 +21,7 @@ type ResultLog = Vec<(String, String)>;
/// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest().0.clone() };
@@ -47,48 +50,147 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
};
// 从profiles里拿东西
let (mut config, chain) = {
let (
mut config,
merge_item,
script_item,
rules_item,
proxies_item,
groups_item,
global_merge,
global_script,
) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let current = profiles.current_mapping().unwrap_or_default();
let merge = profiles
.get_item(&profiles.current_merge().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Merge(Mapping::new()),
});
let script = profiles
.get_item(&profiles.current_script().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
let rules = profiles
.get_item(&profiles.current_rules().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Rules(SeqMap::default()),
});
let proxies = profiles
.get_item(&profiles.current_proxies().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Proxies(SeqMap::default()),
});
let groups = profiles
.get_item(&profiles.current_groups().unwrap_or_default())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "".into(),
data: ChainType::Groups(SeqMap::default()),
});
let chain = match profiles.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| profiles.get_item(uid).ok())
.filter_map(<Option<ChainItem>>::from)
.collect::<Vec<ChainItem>>(),
None => vec![],
};
let global_merge = profiles
.get_item(&"Merge".to_string())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "Merge".into(),
data: ChainType::Merge(Mapping::new()),
});
(current, chain)
let global_script = profiles
.get_item(&"Script".to_string())
.ok()
.and_then(<Option<ChainItem>>::from)
.unwrap_or_else(|| ChainItem {
uid: "Script".into(),
data: ChainType::Script(tmpl::ITEM_SCRIPT.into()),
});
(
current,
merge,
script,
rules,
proxies,
groups,
global_merge,
global_script,
)
};
let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys
// 处理用户的profile
chain.into_iter().for_each(|item| match item.data {
ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
}
ChainType::Script(script) => {
let mut logs = vec![];
// 全局Merge和Script
if let ChainType::Merge(merge) = global_merge.data {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
}
match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
logs.extend(res_logs);
}
Err(err) => logs.push(("exception".into(), err.to_string())),
if let ChainType::Script(script) = global_script.data {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
logs.extend(res_logs);
}
result_map.insert(item.uid, logs);
Err(err) => logs.push(("exception".into(), err.to_string())),
}
});
result_map.insert(global_script.uid, logs);
}
// 订阅关联的Merge、Script、Rules、Proxies、Groups
if let ChainType::Rules(rules) = rules_item.data {
config = use_seq(rules, config.to_owned(), "rules");
}
if let ChainType::Proxies(proxies) = proxies_item.data {
config = use_seq(proxies, config.to_owned(), "proxies");
}
if let ChainType::Groups(groups) = groups_item.data {
config = use_seq(groups, config.to_owned(), "proxy-groups");
}
if let ChainType::Merge(merge) = merge_item.data {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
}
if let ChainType::Script(script) = script_item.data {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
logs.extend(res_logs);
}
Err(err) => logs.push(("exception".into(), err.to_string())),
}
result_map.insert(script_item.uid, logs);
}
// 合并默认的config
for (key, value) in clash_config.into_iter() {
@@ -136,26 +238,24 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.map(|(_, c)| c)
.for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid);
match item.data {
ChainType::Script(script) => match use_script(script, config.to_owned()) {
if let ChainType::Script(script) = item.data {
match use_script(script, config.to_owned()) {
Ok((res_config, _)) => {
config = res_config;
}
Err(err) => {
log::error!(target: "app", "builtin script error `{err}`");
}
},
_ => {}
}
}
});
}
config = use_tun(config, enable_tun);
config = use_tun(config, enable_tun).await;
config = use_sort(config);
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys.into_iter());
exists_set.extend(exists_keys);
exists_keys = exists_set.into_iter().collect();
(config, exists_keys, result_map)
+1 -1
View File
@@ -16,7 +16,7 @@ pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(Stri
2,
NativeFunction::from_closure(
move |_: &JsValue, args: &[JsValue], context: &mut Context| {
let level = args.get(0).unwrap().to_string(context)?;
let level = args.first().unwrap().to_string(context)?;
let level = level.to_std_string().unwrap();
let data = args.get(1).unwrap().to_string(context)?;
let data = data.to_std_string().unwrap();
+53
View File
@@ -0,0 +1,53 @@
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
prepend: Sequence,
append: Sequence,
delete: Sequence,
}
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
let prepend = seq_map.prepend;
let append = seq_map.append;
let delete = seq_map.delete;
let origin_seq = config.get(&name).map_or(Sequence::default(), |val| {
val.as_sequence().unwrap().clone()
});
let mut seq = origin_seq.clone();
for item in prepend {
seq.insert(0, item);
}
for item in append {
seq.push(item);
}
let mut delete_names = Vec::new();
for item in delete {
let item = item.clone();
if let Some(name) = if item.is_string() {
Some(item)
} else {
item.get("name").map(|y| y.clone())
} {
delete_names.push(name.clone());
}
}
seq.retain(|x| {
if let Some(x_name) = if x.is_string() {
Some(x)
} else {
x.get("name")
} {
!delete_names.contains(&x_name)
} else {
true
}
});
let mut config = config.clone();
config.insert(Value::from(name), Value::from(seq));
return config;
}
+4 -53
View File
@@ -1,3 +1,4 @@
use crate::{core::service, log_err};
use serde_yaml::{Mapping, Value};
macro_rules! revise {
@@ -17,7 +18,7 @@ macro_rules! append {
};
}
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
@@ -34,60 +35,10 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
revise!(config, "tun", tun_val);
if enable {
#[cfg(target_os = "macos")]
{
use crate::utils::dirs;
use tauri::api::process::Command;
log::info!(target: "app", "try to set system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("set_dns.sh");
let script = script.to_string_lossy();
match Command::new("bash")
.args([script])
.current_dir(resource_dir)
.status()
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
log_err!(service::set_dns_by_service().await);
use_dns_for_tun(config)
} else {
#[cfg(target_os = "macos")]
{
use crate::utils::dirs;
use tauri::api::process::Command;
log::info!(target: "app", "try to unset system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("unset_dns.sh");
let script = script.to_string_lossy();
match Command::new("bash")
.args([script])
.current_dir(resource_dir)
.status()
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}
log_err!(service::unset_dns_by_service().await);
config
}
}
+25 -16
View File
@@ -106,7 +106,7 @@ pub fn toggle_tun_mode() {
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
match {
let res = {
let redir_port = patch.get("redir-port");
let tproxy_port = patch.get("tproxy-port");
let mixed_port = patch.get("mixed-port");
@@ -139,7 +139,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
|| patch.get("secret").is_some()
|| patch.get("external-controller").is_some()
{
Config::generate()?;
Config::generate().await?;
CoreManager::global().run_core().await?;
handle::Handle::refresh_clash();
}
@@ -156,7 +156,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::runtime().latest().patch_config(patch);
<Result<()>>::Ok(())
} {
};
match res {
Ok(()) => {
Config::clash().apply();
Config::clash().data().save_config()?;
@@ -193,29 +194,29 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let tproxy_enabled = patch.verge_tproxy_enabled;
let socks_enabled = patch.verge_socks_enabled;
let http_enabled = patch.verge_http_enabled;
match {
let res = {
let service_mode = patch.enable_service_mode;
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
Config::generate()?;
Config::generate().await?;
CoreManager::global().run_core().await?;
} else if tun_mode.is_some() {
update_core_config().await?;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() {
Config::generate()?;
Config::generate().await?;
CoreManager::global().run_core().await?;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() {
Config::generate()?;
Config::generate().await?;
CoreManager::global().run_core().await?;
}
if socks_enabled.is_some() || http_enabled.is_some() {
Config::generate()?;
Config::generate().await?;
CoreManager::global().run_core().await?;
}
if auto_launch.is_some() {
@@ -255,7 +256,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
}
<Result<()>>::Ok(())
} {
};
match res {
Ok(()) => {
Config::verge().apply();
Config::verge().data().save_file()?;
@@ -290,7 +292,6 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
Some((url, opt)) => {
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
@@ -385,11 +386,19 @@ pub async fn test_delay(url: String) -> Result<u32> {
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
let start = Instant::now();
let response = request.send().await?;
if response.status().is_success() {
let delay = start.elapsed().as_millis() as u32;
Ok(delay)
} else {
Ok(10000u32)
let response = request.send().await;
match response {
Ok(response) => {
log::trace!(target: "app", "test_delay response: {:#?}", response);
if response.status().is_success() {
Ok(start.elapsed().as_millis() as u32)
} else {
Ok(10000u32)
}
}
Err(err) => {
log::trace!(target: "app", "test_delay error: {:#?}", err);
Err(err.into())
}
}
}
+12 -4
View File
@@ -15,8 +15,15 @@ use tauri::{api, SystemTray};
fn main() -> std::io::Result<()> {
// 单例检测
if server::check_singleton().is_err() {
println!("app exists");
let app_exists: bool = tauri::async_runtime::block_on(async move {
if server::check_singleton().await.is_err() {
println!("app exists");
true
} else {
false
}
});
if app_exists {
return Ok(());
}
@@ -29,7 +36,9 @@ fn main() -> std::io::Result<()> {
let mut builder = tauri::Builder::default()
.system_tray(SystemTray::new())
.setup(|app| {
resolve::resolve_setup(app);
tauri::async_runtime::block_on(async move {
resolve::resolve_setup(app).await;
});
Ok(())
})
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
@@ -44,7 +53,6 @@ fn main() -> std::io::Result<()> {
cmds::get_portable_flag,
// cmds::kill_sidecar,
cmds::restart_sidecar,
cmds::grant_permission,
// clash
cmds::get_clash_info,
cmds::get_clash_logs,
-4
View File
@@ -88,10 +88,6 @@ pub fn profiles_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join(PROFILE_YAML))
}
pub fn clash_pid_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join("clash.pid"))
}
#[cfg(not(target_os = "windows"))]
pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service"))
+9 -1
View File
@@ -1,3 +1,4 @@
use crate::enhance::seq::SeqMap;
use anyhow::{anyhow, bail, Context, Result};
use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize};
@@ -26,7 +27,7 @@ pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
}
/// read mapping from yaml fix #165
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
pub fn read_mapping(path: &PathBuf) -> Result<Mapping> {
let mut val: Value = read_yaml(path)?;
val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@@ -40,6 +41,13 @@ pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
.to_owned())
}
/// read mapping from yaml fix #165
pub fn read_seq_map(path: &PathBuf) -> Result<SeqMap> {
let val: SeqMap = read_yaml(path)?;
Ok(val)
}
/// save the data to the file
/// can set `prefix` string to add some comments
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
+3 -3
View File
@@ -137,7 +137,7 @@ pub fn delete_log() -> Result<()> {
}
let service_log_dir = log_dir.join("service");
for file in fs::read_dir(&service_log_dir)?.flatten() {
for file in fs::read_dir(service_log_dir)?.flatten() {
let _ = process_file(file);
}
@@ -318,11 +318,11 @@ pub fn startup_script() -> Result<()> {
Some(dir) => {
let _ = Command::new(shell)
.current_dir(dir.to_path_buf())
.args(&[path])
.args([path])
.output()?;
}
None => {
let _ = Command::new(shell).args(&[path]).output()?;
let _ = Command::new(shell).args([path]).output()?;
}
}
}
+27 -32
View File
@@ -1,10 +1,6 @@
use crate::config::{IVerge, PrfOption};
use crate::{
config::{Config, PrfItem},
core::*,
utils::init,
utils::server,
};
use crate::cmds::import_profile;
use crate::config::IVerge;
use crate::{config::Config, core::*, utils::init, utils::server};
use crate::{log_err, trace_err};
use anyhow::Result;
use once_cell::sync::OnceCell;
@@ -34,7 +30,7 @@ pub fn find_unused_port() -> Result<u16> {
}
/// handle something when start app
pub fn resolve_setup(app: &mut App) {
pub async fn resolve_setup(app: &mut App) {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let version = app.package_info().version.to_string();
@@ -73,7 +69,8 @@ pub fn resolve_setup(app: &mut App) {
// 启动核心
log::trace!("init config");
log_err!(Config::init_config());
log_err!(Config::init_config().await);
log::trace!("launch core");
log_err!(CoreManager::global().init());
@@ -99,16 +96,20 @@ pub fn resolve_setup(app: &mut App) {
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
tauri::async_runtime::block_on(async {
resolve_scheme(argvs[1].to_owned()).await;
});
let param = argvs[1].as_str();
if param.starts_with("clash:") {
log_err!(resolve_scheme(argvs[1].to_owned()).await);
}
}
}
/// reset system proxy
pub fn resolve_reset() {
log_err!(sysopt::Sysopt::global().reset_sysproxy());
log_err!(CoreManager::global().stop_core());
tauri::async_runtime::block_on(async move {
log_err!(CoreManager::global().stop_core().await);
log_err!(service::unset_dns_by_service().await);
});
}
/// create main window
@@ -205,7 +206,6 @@ pub fn create_window(app_handle: &AppHandle) {
}
Err(_) => {
log::error!("failed to create window");
return;
}
}
}
@@ -236,31 +236,26 @@ pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) ->
Ok(())
}
pub async fn resolve_scheme(param: String) {
pub async fn resolve_scheme(param: String) -> Result<()> {
let url = param
.trim_start_matches("clash://install-config/?url=")
.trim_start_matches("clash://install-config?url=");
let option = PrfOption {
user_agent: None,
with_proxy: Some(true),
self_proxy: None,
danger_accept_invalid_certs: None,
update_interval: None,
};
if let Ok(item) = PrfItem::from_url(url, None, None, Some(option)).await {
if Config::profiles().data().append_item(item).is_ok() {
match import_profile(url.to_string(), None).await {
Ok(_) => {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile success")
.show()
.unwrap();
};
} else {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile failed")
.show()
.unwrap();
log::error!("failed to parse url: {}", url);
}
Err(e) => {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body(format!("Import profile failed: {e}"))
.show()
.unwrap();
log::error!("Import profile failed: {e}");
}
}
Ok(())
}
+24 -21
View File
@@ -1,7 +1,10 @@
extern crate warp;
use super::resolve;
use crate::config::{Config, IVerge, DEFAULT_PAC};
use crate::{
config::{Config, IVerge, DEFAULT_PAC},
log_err,
};
use anyhow::{bail, Result};
use port_scanner::local_port_available;
use std::convert::Infallible;
@@ -14,38 +17,38 @@ struct QueryParam {
}
/// check whether there is already exists
pub fn check_singleton() -> Result<()> {
pub async fn check_singleton() -> Result<()> {
let port = IVerge::get_singleton_port();
if !local_port_available(port) {
tauri::async_runtime::block_on(async {
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
.await?
.text()
.await?;
let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
.await?
.text()
.await?;
if &resp == "ok" {
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
let param = argvs[1].as_str();
if &resp == "ok" {
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
let param = argvs[1].as_str();
if param.starts_with("clash:") {
reqwest::get(format!(
"http://127.0.0.1:{port}/commands/scheme?param={param}"
))
.await?
.text()
.await?;
} else {
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
.await?
.text()
.await?;
}
bail!("app exists");
} else {
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
.await?
.text()
.await?;
}
bail!("app exists");
}
log::error!("failed to setup singleton listen server");
Ok(())
})
log::error!("failed to setup singleton listen server");
Ok(())
} else {
Ok(())
}
@@ -85,7 +88,7 @@ pub fn embed_server(app_handle: AppHandle) {
.and_then(scheme_handler);
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
resolve::resolve_scheme(query.param).await;
log_err!(resolve::resolve_scheme(query.param).await);
Ok("ok")
}
let commands = ping.or(visible).or(pac).or(scheme);
+36 -8
View File
@@ -13,17 +13,15 @@ rules: []
/// enhanced profile
pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
prepend-rules: []
profile:
store-selected: true
prepend-proxies: []
dns:
use-system-hosts: false
";
prepend-proxy-groups: []
pub const ITEM_MERGE_EMPTY: &str = "# Profile Enhancement Merge Template for Clash Verge
append-rules: []
append-proxies: []
append-proxy-groups: []
";
/// enhanced profile
@@ -33,3 +31,33 @@ function main(config) {
return config;
}
";
/// enhanced profile
pub const ITEM_RULES: &str = "# Profile Enhancement Rules Template for Clash Verge
prepend: []
append: []
delete: []
";
/// enhanced profile
pub const ITEM_PROXIES: &str = "# Profile Enhancement Proxies Template for Clash Verge
prepend: []
append: []
delete: []
";
/// enhanced profile
pub const ITEM_GROUPS: &str = "# Profile Enhancement Groups Template for Clash Verge
prepend: []
append: []
delete: []
";
+5 -4
View File
@@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Clash Verge",
"version": "1.6.5"
"version": "1.7.1"
},
"build": {
"distDir": "../dist",
@@ -22,8 +22,9 @@
"icons/icon.ico"
],
"resources": ["resources"],
"externalBin": ["sidecar/clash-meta", "sidecar/clash-meta-alpha"],
"copyright": "© 2022 zzzgydi All Rights Reserved",
"publisher": "Clash Verge Rev",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "A Clash Meta GUI based on tauri.",
"longDescription": "A Clash Meta GUI based on tauri."
@@ -75,7 +76,7 @@
},
"windows": [],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self';"
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: asset: 'unsafe-eval' 'unsafe-inline' 'self';"
}
}
}
+7 -7
View File
@@ -6,20 +6,20 @@
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["deb", "rpm", "appimage", "updater"],
"targets": ["deb", "rpm"],
"deb": {
"depends": ["openssl"],
"desktopTemplate": "./template/clash-verge.desktop",
"provides": ["clash-verge", "clash-meta"],
"conflicts": ["clash-verge", "clash-meta"],
"replaces": ["clash-verge", "clash-meta"]
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"replaces": ["clash-verge"]
},
"rpm": {
"depends": ["openssl"],
"desktopTemplate": "./template/clash-verge.desktop",
"provides": ["clash-verge", "clash-meta"],
"conflicts": ["clash-verge", "clash-meta"],
"obsoletes": ["clash-verge", "clash-meta"]
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"obsoletes": ["clash-verge"]
}
}
}
+20 -1
View File
@@ -12,8 +12,27 @@
"frameworks": [],
"minimumSystemVersion": "10.15",
"exceptionDomain": "",
"signingIdentity": "-",
"signingIdentity": null,
"entitlements": null
},
"dmg": {
"background": "images/background.png",
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"height": 400,
"width": 660
},
"windowPosition": {
"x": 200,
"y": 180
}
}
}
}
+39 -2
View File
@@ -323,10 +323,15 @@ Var AppStartMenuFolder
!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)"
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut
; Show run app after installation.
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe"
!define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_FINISH
Function RunMainBinary
nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" ""
FunctionEnd
; Uninstaller Pages
; 1. Confirm uninstall page
Var DeleteAppDataCheckbox
@@ -436,6 +441,38 @@ FunctionEnd
!endif
${EndIf}
; Check if verge-mihomo-alpha.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo-alpha.exe"
!else
nsis_tauri_utils::FindProcess "verge-mihomo-alpha.exe"
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "Kill verge-mihomo-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo-alpha.exe"
!else
nsis_tauri_utils::KillProcess "verge-mihomo-alpha.exe"
!endif
${EndIf}
; Check if verge-mihomo.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "verge-mihomo.exe"
!else
nsis_tauri_utils::FindProcess "verge-mihomo.exe"
!endif
Pop $R0
${If} $R0 = 0
DetailPrint "Kill verge-mihomo.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "verge-mihomo.exe"
!else
nsis_tauri_utils::KillProcess "verge-mihomo.exe"
!endif
${EndIf}
; Check if clash-meta-alpha.exe is running
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "clash-meta-alpha.exe"
@@ -729,7 +766,7 @@ Function .onInstSuccess
${GetOptions} $CMDLINE "/R" $R0
IfErrors run_done 0
${GetOptions} $CMDLINE "/ARGS" $R0
Exec '"$INSTDIR\${MAINBINARYNAME}.exe" $R0'
nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0"
run_done:
FunctionEnd
+3 -1
View File
@@ -1,5 +1,6 @@
import { alpha, Box, Typography } from "@mui/material";
import { InboxRounded } from "@mui/icons-material";
import { useTranslation } from "react-i18next";
interface Props {
text?: React.ReactNode;
@@ -8,6 +9,7 @@ interface Props {
export const BaseEmpty = (props: Props) => {
const { text = "Empty", extra } = props;
const { t } = useTranslation();
return (
<Box
@@ -22,7 +24,7 @@ export const BaseEmpty = (props: Props) => {
})}
>
<InboxRounded sx={{ fontSize: "4em" }} />
<Typography sx={{ fontSize: "1.25em" }}>{text}</Typography>
<Typography sx={{ fontSize: "1.25em" }}>{t(`${text}`)}</Typography>
{extra}
</Box>
);
+38
View File
@@ -0,0 +1,38 @@
import React from "react";
import { Box, styled } from "@mui/material";
type Props = {
label: string;
fontSize?: string;
width?: string;
padding?: string;
children?: React.ReactNode;
};
export const BaseFieldset: React.FC<Props> = (props: Props) => {
const Fieldset = styled(Box)(() => ({
position: "relative",
border: "1px solid #bbb",
borderRadius: "5px",
width: props.width ?? "auto",
padding: props.padding ?? "15px",
}));
const Label = styled("legend")(({ theme }) => ({
position: "absolute",
top: "-10px",
left: props.padding ?? "15px",
backgroundColor: theme.palette.background.paper,
backgroundImage:
"linear-gradient(rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.16))",
color: theme.palette.text.primary,
fontSize: props.fontSize ?? "1em",
}));
return (
<Fieldset component="fieldset">
<Label>{props.label}</Label>
{props.children}
</Fieldset>
);
};
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { ReactNode } from "react";
import { Typography, alpha } from "@mui/material";
import { Typography } from "@mui/material";
import { BaseErrorBoundary } from "./base-error-boundary";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
+11 -4
View File
@@ -9,6 +9,9 @@ import useRegularExpressionIcon from "@/assets/image/component/use_regular_expre
type SearchProps = {
placeholder?: string;
matchCase?: boolean;
matchWholeWord?: boolean;
useRegularExpression?: boolean;
onSearch: (
match: (content: string) => boolean,
state: {
@@ -23,9 +26,13 @@ type SearchProps = {
export const BaseSearchBox = styled((props: SearchProps) => {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(true);
const [matchWholeWord, setMatchWholeWord] = useState(false);
const [useRegularExpression, setUseRegularExpression] = useState(false);
const [matchCase, setMatchCase] = useState(props.matchCase ?? true);
const [matchWholeWord, setMatchWholeWord] = useState(
props.matchWholeWord ?? false
);
const [useRegularExpression, setUseRegularExpression] = useState(
props.useRegularExpression ?? false
);
const [errorMessage, setErrorMessage] = useState("");
const iconStyle = {
@@ -89,11 +96,11 @@ export const BaseSearchBox = styled((props: SearchProps) => {
return (
<Tooltip title={errorMessage} placement="bottom-start">
<TextField
autoComplete="off"
inputRef={inputRef}
hiddenLabel
fullWidth
size="small"
autoComplete="off"
variant="outlined"
spellCheck="false"
placeholder={props.placeholder ?? t("Filter conditions")}
@@ -6,10 +6,10 @@ export const BaseStyledTextField = styled((props: TextFieldProps) => {
return (
<TextField
autoComplete="off"
hiddenLabel
fullWidth
size="small"
autoComplete="off"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
+24
View File
@@ -0,0 +1,24 @@
import {
Tooltip,
IconButton,
IconButtonProps,
SvgIconProps,
} from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
interface Props extends IconButtonProps {
title?: string;
icon?: React.ElementType<SvgIconProps>;
}
export const TooltipIcon: React.FC<Props> = (props: Props) => {
const { title = "", icon: Icon = InfoRounded, ...restProps } = props;
return (
<Tooltip title={title} placement="top">
<IconButton color="inherit" size="small" {...restProps}>
<Icon fontSize="inherit" style={{ cursor: "pointer", opacity: 0.75 }} />
</IconButton>
</Tooltip>
);
};
+21 -22
View File
@@ -1,10 +1,7 @@
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import {
DataGrid,
GridColDef,
GridValueFormatterParams,
} from "@mui/x-data-grid";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { useThemeMode } from "@/services/states";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
@@ -16,12 +13,15 @@ interface Props {
export const ConnectionTable = (props: Props) => {
const { connections, onShowDetail } = props;
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const backgroundColor = isDark ? "#282A36" : "#ffffff";
const [columnVisible, setColumnVisible] = useState<
Partial<Record<keyof IConnectionsItem, boolean>>
>({});
const columns: GridColDef[] = [
const [columns] = useState<GridColDef[]>([
{ field: "host", headerName: t("Host"), flex: 220, minWidth: 220 },
{
field: "download",
@@ -29,8 +29,7 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" "),
valueFormatter: (value: number) => parseTraffic(value).join(" "),
},
{
field: "upload",
@@ -38,8 +37,7 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" "),
valueFormatter: (value: number) => parseTraffic(value).join(" "),
},
{
field: "dlSpeed",
@@ -47,8 +45,7 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" ") + "/s",
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
},
{
field: "ulSpeed",
@@ -56,8 +53,7 @@ export const ConnectionTable = (props: Props) => {
width: 88,
align: "right",
headerAlign: "right",
valueFormatter: (params: GridValueFormatterParams<number>) =>
parseTraffic(params.value).join(" ") + "/s",
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
},
{ field: "chains", headerName: t("Chains"), flex: 360, minWidth: 360 },
{ field: "rule", headerName: t("Rule"), flex: 300, minWidth: 250 },
@@ -69,11 +65,9 @@ export const ConnectionTable = (props: Props) => {
minWidth: 100,
align: "right",
headerAlign: "right",
sortComparator: (v1, v2) => {
return new Date(v2).getTime() - new Date(v1).getTime();
},
valueFormatter: (params: GridValueFormatterParams<string>) =>
dayjs(params.value).fromNow(),
sortComparator: (v1: string, v2: string) =>
new Date(v2).getTime() - new Date(v1).getTime(),
valueFormatter: (value: number) => dayjs(value).fromNow(),
},
{ field: "source", headerName: t("Source"), flex: 200, minWidth: 130 },
{
@@ -83,7 +77,7 @@ export const ConnectionTable = (props: Props) => {
minWidth: 130,
},
{ field: "type", headerName: t("Type"), flex: 160, minWidth: 100 },
];
]);
const connRows = useMemo(() => {
return connections.map((each) => {
@@ -106,7 +100,6 @@ export const ConnectionTable = (props: Props) => {
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
destinationIP: metadata.destinationIP,
type: `${metadata.type}(${metadata.network})`,
connectionData: each,
};
});
@@ -119,7 +112,13 @@ export const ConnectionTable = (props: Props) => {
columns={columns}
onRowClick={(e) => onShowDetail(e.row.connectionData)}
density="compact"
sx={{ border: "none", "div:focus": { outline: "none !important" } }}
sx={{
border: "none",
"div:focus": { outline: "none !important" },
"& div[aria-rowindex]": {
backgroundColor: `${backgroundColor} !important`,
},
}}
columnVisibilityModel={columnVisible}
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
/>
+105 -48
View File
@@ -8,13 +8,22 @@ import {
import { useClashInfo } from "@/hooks/use-clash";
import { useVerge } from "@/hooks/use-verge";
import { TrafficGraph, type TrafficRef } from "./traffic-graph";
import { useLogSetup } from "./use-log-setup";
import { useLogData } from "@/hooks/use-log-data";
import { useVisibility } from "@/hooks/use-visibility";
import { useWebsocket } from "@/hooks/use-websocket";
import parseTraffic from "@/utils/parse-traffic";
import useSWRSubscription from "swr/subscription";
import { createSockette } from "@/utils/websocket";
import { useTranslation } from "react-i18next";
import { isDebugEnabled, gc } from "@/services/api";
interface MemoryUsage {
inuse: number;
oslimit?: number;
}
// setup the traffic
export const LayoutTraffic = () => {
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const { verge } = useVerge();
@@ -22,62 +31,104 @@ export const LayoutTraffic = () => {
const trafficGraph = verge?.traffic_graph ?? true;
const trafficRef = useRef<TrafficRef>(null);
const [traffic, setTraffic] = useState({ up: 0, down: 0 });
const [memory, setMemory] = useState({ inuse: 0 });
const pageVisible = useVisibility();
// setup log ws during layout
useLogSetup();
const trafficWs = useWebsocket(
(event) => {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
setTraffic(data);
},
{ onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 }
);
const [isDebug, setIsDebug] = useState(false);
useEffect(() => {
if (!clashInfo || !pageVisible) return;
isDebugEnabled().then((flag) => setIsDebug(flag));
return () => {};
}, [isDebug]);
const { server = "", secret = "" } = clashInfo;
trafficWs.connect(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`
);
return () => trafficWs.disconnect();
}, [clashInfo, pageVisible]);
// https://swr.vercel.app/docs/subscription#deduplication
// useSWRSubscription auto deduplicates to one subscription per key per entire app
// So we can simply invoke it here acting as preconnect
useLogData();
const { data: traffic = { up: 0, down: 0 } } = useSWRSubscription<
ITrafficItem,
any,
"getRealtimeTraffic" | null
>(
clashInfo && pageVisible ? "getRealtimeTraffic" : null,
(_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
const s = createSockette(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
const data = JSON.parse(event.data) as ITrafficItem;
trafficRef.current?.appendData(data);
next(null, data);
},
onerror(event) {
this.close();
next(event, { up: 0, down: 0 });
},
}
);
return () => {
s.close();
};
},
{
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
}
);
/* --------- meta memory information --------- */
const isMetaCore = verge?.clash_core?.includes("clash-meta");
const displayMemory = isMetaCore && (verge?.enable_memory_usage ?? true);
const memoryWs = useWebsocket(
(event) => {
setMemory(JSON.parse(event.data));
const displayMemory = verge?.enable_memory_usage ?? true;
const { data: memory = { inuse: 0 } } = useSWRSubscription<
MemoryUsage,
any,
"getRealtimeMemory" | null
>(
clashInfo && pageVisible && displayMemory ? "getRealtimeMemory" : null,
(_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
const s = createSockette(
`ws://${server}/memory?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
const data = JSON.parse(event.data) as MemoryUsage;
next(null, data);
},
onerror(event) {
this.close();
next(event, { inuse: 0 });
},
}
);
return () => {
s.close();
};
},
{ onError: () => setMemory({ inuse: 0 }), errorCount: 10 }
{
fallbackData: { inuse: 0 },
keepPreviousData: true,
}
);
useEffect(() => {
if (!clashInfo || !pageVisible || !displayMemory) return;
const { server = "", secret = "" } = clashInfo;
memoryWs.connect(
`ws://${server}/memory?token=${encodeURIComponent(secret)}`
);
return () => memoryWs.disconnect();
}, [clashInfo, pageVisible, displayMemory]);
const [up, upUnit] = parseTraffic(traffic.up);
const [down, downUnit] = parseTraffic(traffic.down);
const [inuse, inuseUnit] = parseTraffic(memory.inuse);
const boxStyle: any = {
display: "flex",
alignItems: "center",
whiteSpace: "nowrap",
};
const iconStyle: any = {
sx: { mr: "8px", fontSize: 16 },
};
const valStyle: any = {
component: "span",
// color: "primary",
textAlign: "center",
sx: { flex: "1 1 56px", userSelect: "none" },
};
@@ -90,15 +141,18 @@ export const LayoutTraffic = () => {
};
return (
<Box position="relative" onClick={trafficRef.current?.toggleStyle}>
<Box position="relative">
{trafficGraph && pageVisible && (
<div style={{ width: "100%", height: 60, marginBottom: 6 }}>
<div
style={{ width: "100%", height: 60, marginBottom: 6 }}
onClick={trafficRef.current?.toggleStyle}
>
<TrafficGraph ref={trafficRef} />
</div>
)}
<Box display="flex" flexDirection="column" gap={0.75}>
<Box display="flex" alignItems="center" whiteSpace="nowrap">
<Box title={t("Upload Speed")} {...boxStyle}>
<ArrowUpward
{...iconStyle}
color={+up > 0 ? "secondary" : "disabled"}
@@ -109,7 +163,7 @@ export const LayoutTraffic = () => {
<Typography {...unitStyle}>{upUnit}/s</Typography>
</Box>
<Box display="flex" alignItems="center" whiteSpace="nowrap">
<Box title={t("Download Speed")} {...boxStyle}>
<ArrowDownward
{...iconStyle}
color={+down > 0 ? "primary" : "disabled"}
@@ -122,12 +176,15 @@ export const LayoutTraffic = () => {
{displayMemory && (
<Box
display="flex"
alignItems="center"
whiteSpace="nowrap"
title="Memory Usage"
title={t(isDebug ? "Memory Cleanup" : "Memory Usage")}
{...boxStyle}
sx={{ cursor: isDebug ? "pointer" : "auto" }}
color={isDebug ? "success.main" : "disabled"}
onClick={async () => {
isDebug && (await gc());
}}
>
<MemoryOutlined {...iconStyle} color="disabled" />
<MemoryOutlined {...iconStyle} />
<Typography {...valStyle}>{inuse}</Typography>
<Typography {...unitStyle}>{inuseUnit}</Typography>
</Box>
+3 -3
View File
@@ -1,8 +1,7 @@
import { useEffect, useMemo } from "react";
import { useRecoilState } from "recoil";
import { alpha, createTheme, Shadows, Theme } from "@mui/material";
import { appWindow } from "@tauri-apps/api/window";
import { atomThemeMode } from "@/services/states";
import { useSetThemeMode, useThemeMode } from "@/services/states";
import { defaultTheme, defaultDarkTheme } from "@/pages/_theme";
import { useVerge } from "@/hooks/use-verge";
@@ -12,7 +11,8 @@ import { useVerge } from "@/hooks/use-verge";
export const useCustomTheme = () => {
const { verge } = useVerge();
const { theme_mode, theme_setting } = verge ?? {};
const [mode, setMode] = useRecoilState(atomThemeMode);
const mode = useThemeMode();
const setMode = useSetThemeMode();
useEffect(() => {
const themeMode = ["light", "dark", "system"].includes(theme_mode!)
-39
View File
@@ -1,39 +0,0 @@
import dayjs from "dayjs";
import { useEffect } from "react";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { getClashLogs } from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash";
import { atomEnableLog, atomLogData } from "@/services/states";
import { useWebsocket } from "@/hooks/use-websocket";
const MAX_LOG_NUM = 1000;
// setup the log websocket
export const useLogSetup = () => {
const { clashInfo } = useClashInfo();
const enableLog = useRecoilValue(atomEnableLog);
const setLogData = useSetRecoilState(atomLogData);
const { connect, disconnect } = useWebsocket((event) => {
const data = JSON.parse(event.data) as ILogItem;
const time = dayjs().format("MM-DD HH:mm:ss");
setLogData((l) => {
if (l.length >= MAX_LOG_NUM) l.shift();
return [...l, { ...data, time }];
});
});
useEffect(() => {
if (!enableLog || !clashInfo) return;
getClashLogs().then(setLogData);
const { server = "", secret = "" } = clashInfo;
connect(`ws://${server}/logs?token=${encodeURIComponent(secret)}`);
return () => {
disconnect();
};
}, [clashInfo, enableLog]);
};
+3 -3
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
@@ -27,10 +27,10 @@ export const ConfirmViewer = (props: Props) => {
return (
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>{t(title)}</DialogTitle>
<DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ pb: 1, userSelect: "text" }}>
{t(message)}
{message}
</DialogContent>
<DialogActions>
+184 -129
View File
@@ -1,183 +1,238 @@
import { ReactNode, useEffect, useRef } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { useRecoilValue } from "recoil";
import { useTranslation } from "react-i18next";
import {
Button,
ButtonGroup,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
} from "@mui/material";
import { atomThemeMode } from "@/services/states";
import { readProfileFile, saveProfileFile } from "@/services/cmds";
import FormatPaintIcon from "@mui/icons-material/FormatPaint";
import OpenInFullIcon from "@mui/icons-material/OpenInFull";
import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen";
import { useThemeMode } from "@/services/states";
import { Notice } from "@/components/base";
import { nanoid } from "nanoid";
import { appWindow } from "@tauri-apps/api/window";
import getSystem from "@/utils/get-system";
import * as monaco from "monaco-editor";
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import MonacoEditor from "react-monaco-editor";
import { configureMonacoYaml } from "monaco-yaml";
import { type JSONSchema7 } from "json-schema";
import metaSchema from "meta-json-schema/schemas/meta-json-schema.json";
import mergeSchema from "meta-json-schema/schemas/clash-verge-merge-json-schema.json";
import pac from "types-pac/pac.d.ts?raw";
interface Props {
title?: string | ReactNode;
mode: "profile" | "text";
property: string;
open: boolean;
readOnly?: boolean;
language: "yaml" | "javascript" | "css";
schema?: "clash" | "merge";
onClose: () => void;
onChange?: (content?: string) => void;
type Language = "yaml" | "javascript" | "css";
type Schema<T extends Language> = LanguageSchemaMap[T];
interface LanguageSchemaMap {
yaml: "clash" | "merge";
javascript: never;
css: never;
}
// yaml worker
configureMonacoYaml(monaco, {
validate: true,
enableSchemaRequest: true,
schemas: [
{
uri: "http://example.com/meta-json-schema.json",
fileMatch: ["**/*.clash.yaml"],
//@ts-ignore
schema: metaSchema as JSONSchema7,
},
{
uri: "http://example.com/clash-verge-merge-json-schema.json",
fileMatch: ["**/*.merge.yaml"],
//@ts-ignore
schema: mergeSchema as JSONSchema7,
},
],
});
// PAC definition
monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts");
monaco.languages.registerCompletionItemProvider("javascript", {
provideCompletionItems: (model, position) => ({
suggestions: [
interface Props<T extends Language> {
open: boolean;
title?: string | ReactNode;
initialData: Promise<string>;
readOnly?: boolean;
language: T;
schema?: Schema<T>;
onChange?: (prev?: string, curr?: string) => void;
onSave?: (prev?: string, curr?: string) => void;
onClose: () => void;
}
let initialized = false;
const monacoInitialization = () => {
if (initialized) return;
// configure yaml worker
configureMonacoYaml(monaco, {
validate: true,
enableSchemaRequest: true,
schemas: [
{
label: "%mixed-port%",
kind: monaco.languages.CompletionItemKind.Text,
insertText: "%mixed-port%",
range: {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: model.getWordUntilPosition(position).startColumn - 1,
endColumn: model.getWordUntilPosition(position).endColumn - 1,
},
uri: "http://example.com/meta-json-schema.json",
fileMatch: ["**/*.clash.yaml"],
// @ts-ignore
schema: metaSchema as JSONSchema7,
},
{
uri: "http://example.com/clash-verge-merge-json-schema.json",
fileMatch: ["**/*.merge.yaml"],
// @ts-ignore
schema: mergeSchema as JSONSchema7,
},
],
}),
});
});
// configure PAC definition
monaco.languages.typescript.javascriptDefaults.addExtraLib(pac, "pac.d.ts");
export const EditorViewer = (props: Props) => {
const {
title,
mode,
property,
open,
readOnly,
language,
schema,
onClose,
onChange,
} = props;
initialized = true;
};
export const EditorViewer = <T extends Language>(props: Props<T>) => {
const { t } = useTranslation();
const editorRef = useRef<any>();
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const themeMode = useRecoilValue(atomThemeMode);
const themeMode = useThemeMode();
const [isMaximized, setIsMaximized] = useState(false);
useEffect(() => {
if (!open) return;
const {
open = false,
title = t("Edit File"),
initialData = Promise.resolve(""),
readOnly = false,
language = "yaml",
schema,
onChange,
onSave,
onClose,
} = props;
let fetchContent;
switch (mode) {
case "profile": // profile文件
fetchContent = readProfileFile(property);
break;
case "text": // 文本内容
fetchContent = Promise.resolve(property);
break;
}
fetchContent.then((data) => {
const dom = editorRef.current;
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const prevData = useRef<string | undefined>("");
const currData = useRef<string | undefined>("");
if (!dom) return;
const editorWillMount = () => {
monacoInitialization(); // initialize monaco
};
if (instanceRef.current) instanceRef.current.dispose();
const editorDidMount = async (
editor: monaco.editor.IStandaloneCodeEditor
) => {
editorRef.current = editor;
// retrieve initial data
await initialData.then((data) => {
prevData.current = data;
currData.current = data;
// create and set model
const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`);
const model = monaco.editor.createModel(data, language, uri);
instanceRef.current = editor.create(editorRef.current, {
model: model,
language: language,
tabSize: ["yaml", "javascript", "css"].includes(language) ? 2 : 4, // 根据语言类型设置缩进大小
theme: themeMode === "light" ? "vs" : "vs-dark",
minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
readOnly: readOnly, // 只读模式
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
renderValidationDecorations: "on", // 只读模式下显示校验信息
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, // 平滑滚动
});
editorRef.current?.setModel(model);
});
};
return () => {
if (instanceRef.current) {
instanceRef.current.dispose();
instanceRef.current = null;
}
};
}, [open]);
const onSave = useLockFn(async () => {
const value = instanceRef.current?.getValue();
if (value == null) return;
const handleChange = useLockFn(async (value: string | undefined) => {
try {
if (mode === "profile") {
await saveProfileFile(property, value);
}
onChange?.(value);
currData.current = value;
onChange?.(prevData.current, currData.current);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
const handleSave = useLockFn(async () => {
try {
!readOnly && onSave?.(prevData.current, currData.current);
onClose();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
const handleClose = useLockFn(async () => {
try {
onClose();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
useEffect(() => {
const unlistenResized = appWindow.onResized(() => {
appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized);
});
});
return () => {
unlistenResized.then((fn) => fn());
editorRef.current?.dispose();
editorRef.current = undefined;
};
}, []);
return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{title ?? t("Edit File")}</DialogTitle>
<DialogTitle>{title}</DialogTitle>
<DialogContent sx={{ width: "auto", height: "100vh" }}>
<div style={{ width: "100%", height: "100%" }} ref={editorRef} />
<MonacoEditor
language={language}
theme={themeMode === "light" ? "vs" : "vs-dark"}
options={{
tabSize: ["yaml", "javascript", "css"].includes(language) ? 2 : 4, // 根据语言类型设置缩进大小
minimap: {
enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
readOnly: readOnly, // 只读模式
readOnlyMessage: { value: t("ReadOnlyMessage") }, // 只读模式尝试编辑时的提示信息
renderValidationDecorations: "on", // 只读模式下显示校验信息
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, // 平滑滚动
}}
editorWillMount={editorWillMount}
editorDidMount={editorDidMount}
onChange={handleChange}
/>
<ButtonGroup
variant="contained"
sx={{ position: "absolute", left: "14px", bottom: "8px" }}
>
<IconButton
size="medium"
color="inherit"
sx={{ display: readOnly ? "none" : "" }}
title={t("Format document")}
onClick={() =>
editorRef.current
?.getAction("editor.action.formatDocument")
?.run()
}
>
<FormatPaintIcon fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
color="inherit"
title={t(isMaximized ? "Minimize" : "Maximize")}
onClick={() =>
appWindow.toggleMaximize().then(() => editorRef.current?.layout())
}
>
{isMaximized ? <CloseFullscreenIcon /> : <OpenInFullIcon />}
</IconButton>
</ButtonGroup>
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={onSave} variant="contained">
{t("Save")}
<Button onClick={handleClose} variant="outlined">
{t(readOnly ? "Close" : "Cancel")}
</Button>
{!readOnly && (
<Button onClick={handleSave} variant="contained">
{t("Save")}
</Button>
)}
</DialogActions>
</Dialog>
);
+2 -2
View File
@@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next";
import { Box, Button, Typography } from "@mui/material";
interface Props {
onChange: (value: string) => void;
onChange: (file: File, value: string) => void;
}
export const FileInput = (props: Props) => {
@@ -28,7 +28,7 @@ export const FileInput = (props: Props) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(null);
onChange(event.target?.result as string);
onChange(file, event.target?.result as string);
};
reader.onerror = reject;
reader.readAsText(file);
+1 -1
View File
@@ -61,7 +61,7 @@ export const LogViewer = (props: Props) => {
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Back")}
{t("Close")}
</Button>
</DialogActions>
</Dialog>
+179 -40
View File
@@ -2,7 +2,6 @@ import dayjs from "dayjs";
import { mutate } from "swr";
import { useEffect, useState } from "react";
import { useLockFn } from "ahooks";
import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
@@ -17,13 +16,19 @@ import {
CircularProgress,
} from "@mui/material";
import { RefreshRounded, DragIndicator } from "@mui/icons-material";
import { atomLoadingCache } from "@/services/states";
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import {
viewProfile,
readProfileFile,
updateProfile,
saveProfileFile,
} from "@/services/cmds";
import { Notice } from "@/components/base";
import { RulesEditorViewer } from "@/components/profile/rules-editor-viewer";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { ProfileBox } from "./profile-box";
import parseTraffic from "@/utils/parse-traffic";
import { ConfirmViewer } from "./confirm-viewer";
import { ConfirmViewer } from "@/components/profile/confirm-viewer";
import { open } from "@tauri-apps/api/shell";
const round = keyframes`
from { transform: rotate(0deg); }
@@ -37,22 +42,27 @@ interface Props {
itemData: IProfileItem;
onSelect: (force: boolean) => void;
onEdit: () => void;
onSave?: (prev?: string, curr?: string) => void;
onDelete: () => void;
}
export const ProfileItem = (props: Props) => {
const { selected, activating, itemData, onSelect, onEdit } = props;
const { selected, activating, itemData, onSelect, onEdit, onSave, onDelete } =
props;
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props.id });
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [loadingCache, setLoadingCache] = useRecoilState(atomLoadingCache);
const loadingCache = useLoadingCache();
const setLoadingCache = useSetLoadingCache();
const { uid, name = "Profile", extra, updated = 0 } = itemData;
const { uid, name = "Profile", extra, updated = 0, option } = itemData;
// local file mode
// remote file mode
// remote file mode
const hasUrl = !!itemData.url;
const hasExtra = !!extra; // only subscription url has extra info
const hasHome = !!itemData.home; // only subscription url has home page
@@ -61,7 +71,7 @@ export const ProfileItem = (props: Props) => {
const from = parseUrl(itemData.url);
const description = itemData.desc;
const expire = parseExpire(extra?.expire);
const progress = Math.round(((download + upload) * 100) / (total + 0.1));
const progress = Math.round(((download + upload) * 100) / (total + 0.01) + 1);
const loading = loadingCache[itemData.uid] ?? false;
@@ -94,6 +104,11 @@ export const ProfileItem = (props: Props) => {
}, [hasUrl, updated]);
const [fileOpen, setFileOpen] = useState(false);
const [rulesOpen, setRulesOpen] = useState(false);
const [proxiesOpen, setProxiesOpen] = useState(false);
const [groupsOpen, setGroupsOpen] = useState(false);
const [mergeOpen, setMergeOpen] = useState(false);
const [scriptOpen, setScriptOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const onOpenHome = () => {
@@ -111,6 +126,31 @@ export const ProfileItem = (props: Props) => {
setFileOpen(true);
};
const onEditRules = () => {
setAnchorEl(null);
setRulesOpen(true);
};
const onEditProxies = () => {
setAnchorEl(null);
setProxiesOpen(true);
};
const onEditGroups = () => {
setAnchorEl(null);
setGroupsOpen(true);
};
const onEditMerge = () => {
setAnchorEl(null);
setMergeOpen(true);
};
const onEditScript = () => {
setAnchorEl(null);
setScriptOpen(true);
};
const onForceSelect = () => {
setAnchorEl(null);
onSelect(true);
@@ -162,44 +202,86 @@ export const ProfileItem = (props: Props) => {
}
});
const onDelete = useLockFn(async () => {
setAnchorEl(null);
try {
await deleteProfile(itemData.uid);
mutate("getProfiles");
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome }] : []
hasHome ? [{ label: "Home", handler: onOpenHome, disabled: false }] : []
).concat([
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(0) },
{ label: "Update(Proxy)", handler: () => onUpdate(2) },
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
{
label: "Edit Rules",
handler: onEditRules,
disabled: !option?.rules,
},
{
label: "Edit Proxies",
handler: onEditProxies,
disabled: !option?.proxies,
},
{
label: "Edit Groups",
handler: onEditGroups,
disabled: !option?.groups,
},
{
label: "Extend Config",
handler: onEditMerge,
disabled: !option?.merge,
},
{
label: "Extend Script",
handler: onEditScript,
disabled: !option?.script,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{ label: "Update", handler: () => onUpdate(0), disabled: false },
{ label: "Update(Proxy)", handler: () => onUpdate(2), disabled: false },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
disabled: false,
},
]);
const fileModeMenu = [
{ label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "Select", handler: onForceSelect, disabled: false },
{ label: "Edit Info", handler: onEditInfo, disabled: false },
{ label: "Edit File", handler: onEditFile, disabled: false },
{
label: "Edit Rules",
handler: onEditRules,
disabled: !option?.rules,
},
{
label: "Edit Proxies",
handler: onEditProxies,
disabled: !option?.proxies,
},
{
label: "Edit Groups",
handler: onEditGroups,
disabled: !option?.groups,
},
{
label: "Extend Config",
handler: onEditMerge,
disabled: !option?.merge,
},
{
label: "Extend Script",
handler: onEditScript,
disabled: !option?.script,
},
{ label: "Open File", handler: onOpenFile, disabled: false },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
disabled: false,
},
];
@@ -242,7 +324,7 @@ export const ProfileItem = (props: Props) => {
backdropFilter: "blur(2px)",
}}
>
<CircularProgress size={20} />
<CircularProgress color="inherit" size={20} />
</Box>
)}
<Box position="relative">
@@ -278,6 +360,7 @@ export const ProfileItem = (props: Props) => {
{/* only if has url can it be updated */}
{hasUrl && (
<IconButton
title={t("Refresh")}
sx={{
position: "absolute",
p: "3px",
@@ -311,7 +394,7 @@ export const ProfileItem = (props: Props) => {
</Typography>
) : (
hasUrl && (
<Typography noWrap title={`From ${from}`}>
<Typography noWrap title={`${t("From")} ${from}`}>
{from}
</Typography>
)
@@ -322,7 +405,7 @@ export const ProfileItem = (props: Props) => {
flex="1 0 auto"
fontSize={14}
textAlign="right"
title={`Updated Time: ${parseExpire(updated)}`}
title={`${t("Update Time")}: ${parseExpire(updated)}`}
>
{updated > 0 ? dayjs(updated * 1000).fromNow() : ""}
</Typography>
@@ -333,17 +416,21 @@ export const ProfileItem = (props: Props) => {
{/* the third line show extra info or last updated time */}
{hasExtra ? (
<Box sx={{ ...boxStyle, fontSize: 14 }}>
<span title="Used / Total">
<span title={t("Used / Total")}>
{parseTraffic(upload + download)} / {parseTraffic(total)}
</span>
<span title="Expire Time">{expire}</span>
<span title={t("Expire Time")}>{expire}</span>
</Box>
) : (
<Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
<span title="Updated Time">{parseExpire(updated)}</span>
<span title={t("Update Time")}>{parseExpire(updated)}</span>
</Box>
)}
<LinearProgress variant="determinate" value={progress} />
<LinearProgress
variant="determinate"
value={progress}
style={{ opacity: total > 0 ? 1 : 0 }}
/>
</ProfileBox>
<Menu
@@ -363,6 +450,7 @@ export const ProfileItem = (props: Props) => {
<MenuItem
key={item.label}
onClick={item.handler}
disabled={item.disabled}
sx={[
{
minWidth: 120,
@@ -384,16 +472,67 @@ export const ProfileItem = (props: Props) => {
</Menu>
<EditorViewer
mode="profile"
property={uid}
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?.proxies ?? "")}
language="yaml"
onSave={async (prev, curr) => {
await saveProfileFile(option?.proxies ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setGroupsOpen(false)}
/>
<EditorViewer
open={mergeOpen}
initialData={readProfileFile(option?.merge ?? "")}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setMergeOpen(false)}
/>
<EditorViewer
open={scriptOpen}
initialData={readProfileFile(option?.script ?? "")}
language="javascript"
onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setScriptOpen(false)}
/>
<ConfirmViewer
title="Confirm deletion"
message="This operation is not reversible"
title={t("Confirm deletion")}
message={t("This operation is not reversible")}
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
+27 -103
View File
@@ -1,4 +1,3 @@
import dayjs from "dayjs";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
@@ -12,54 +11,28 @@ import {
IconButton,
} from "@mui/material";
import { FeaturedPlayListRounded } from "@mui/icons-material";
import { viewProfile } from "@/services/cmds";
import { viewProfile, readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice } from "@/components/base";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { ProfileBox } from "./profile-box";
import { LogViewer } from "./log-viewer";
import { ConfirmViewer } from "./confirm-viewer";
interface Props {
selected: boolean;
itemData: IProfileItem;
enableNum: number;
logInfo?: [string, string][];
onEnable: () => void;
onDisable: () => void;
onMoveTop: () => void;
onMoveEnd: () => void;
onDelete: () => void;
onEdit: () => void;
id: "Merge" | "Script";
onSave?: (prev?: string, curr?: string) => void;
}
// profile enhanced item
export const ProfileMore = (props: Props) => {
const {
selected,
itemData,
enableNum,
logInfo = [],
onEnable,
onDisable,
onMoveTop,
onMoveEnd,
onDelete,
onEdit,
} = props;
const { id, logInfo = [], onSave } = props;
const { uid, type } = itemData;
const { t, i18n } = useTranslation();
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 });
const [fileOpen, setFileOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [logOpen, setLogOpen] = useState(false);
const onEditInfo = () => {
setAnchorEl(null);
onEdit();
};
const onEditFile = () => {
setAnchorEl(null);
setFileOpen(true);
@@ -68,7 +41,7 @@ export const ProfileMore = (props: Props) => {
const onOpenFile = useLockFn(async () => {
setAnchorEl(null);
try {
await viewProfile(itemData.uid);
await viewProfile(id);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
@@ -80,36 +53,10 @@ export const ProfileMore = (props: Props) => {
};
const hasError = !!logInfo.find((e) => e[0] === "exception");
const showMove = enableNum > 1 && !hasError;
const enableMenu = [
{ label: "Disable", handler: fnWrapper(onDisable) },
{ label: "Edit Info", handler: onEditInfo },
const itemMenu = [
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{ label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) },
{ label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
];
const disableMenu = [
{ label: "Enable", handler: fnWrapper(onEnable) },
{ label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile },
{
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
];
const boxStyle = {
@@ -123,9 +70,7 @@ export const ProfileMore = (props: Props) => {
return (
<>
<ProfileBox
aria-selected={selected}
onDoubleClick={onEditFile}
// onClick={() => onSelect(false)}
onContextMenu={(event) => {
const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX });
@@ -144,13 +89,13 @@ export const ProfileMore = (props: Props) => {
variant="h6"
component="h2"
noWrap
title={itemData.name}
title={t(`Global ${id}`)}
>
{itemData.name}
{t(`Global ${id}`)}
</Typography>
<Chip
label={type}
label={id}
color="primary"
size="small"
variant="outlined"
@@ -159,8 +104,8 @@ export const ProfileMore = (props: Props) => {
</Box>
<Box sx={boxStyle}>
{selected && type === "script" ? (
hasError ? (
{id === "Script" &&
(hasError ? (
<Badge color="error" variant="dot" overlap="circular">
<IconButton
size="small"
@@ -182,16 +127,7 @@ export const ProfileMore = (props: Props) => {
>
<FeaturedPlayListRounded fontSize="inherit" />
</IconButton>
)
) : (
<Typography
noWrap
title={itemData.desc}
sx={i18n.language === "zh" ? { width: "calc(100% - 75px)" } : {}}
>
{itemData.desc}
</Typography>
)}
))}
</Box>
</ProfileBox>
@@ -208,7 +144,7 @@ export const ProfileMore = (props: Props) => {
e.preventDefault();
}}
>
{(selected ? enableMenu : disableMenu)
{itemMenu
.filter((item: any) => item.show !== false)
.map((item) => (
<MenuItem
@@ -233,35 +169,23 @@ export const ProfileMore = (props: Props) => {
</Menu>
<EditorViewer
mode="profile"
property={uid}
open={fileOpen}
language={type === "merge" ? "yaml" : "javascript"}
schema={type === "merge" ? "merge" : undefined}
title={`${t("Global " + id)}`}
initialData={readProfileFile(id)}
language={id === "Merge" ? "yaml" : "javascript"}
schema={id === "Merge" ? "clash" : undefined}
onSave={async (prev, curr) => {
await saveProfileFile(id, curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setFileOpen(false)}
/>
<ConfirmViewer
title="Confirm deletion"
message="This operation is not reversible"
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
onDelete();
setConfirmOpen(false);
}}
<LogViewer
open={logOpen}
logInfo={logInfo}
onClose={() => setLogOpen(false)}
/>
{selected && (
<LogViewer
open={logOpen}
logInfo={logInfo}
onClose={() => setLogOpen(false)}
/>
)}
</>
);
};
function parseExpire(expire?: number) {
if (!expire) return "-";
return dayjs(expire * 1000).format("YYYY-MM-DD");
}
+10 -14
View File
@@ -33,7 +33,7 @@ export interface ProfileViewerRef {
}
// create or edit the profile
// remote / local / merge / script
// remote / local
export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
(props, ref) => {
const { t } = useTranslation();
@@ -92,9 +92,7 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
if (form.type === "remote" && !form.url) {
throw new Error("The URL should not be null");
}
if (form.type !== "remote" && form.type !== "local") {
delete form.option;
}
if (form.option?.update_interval) {
form.option.update_interval = +form.option.update_interval;
} else {
@@ -167,8 +165,6 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
<Select {...field} autoFocus label={t("Type")}>
<MenuItem value="remote">Remote</MenuItem>
<MenuItem value="local">Local</MenuItem>
<MenuItem value="script">Script</MenuItem>
<MenuItem value="merge">Merge</MenuItem>
</Select>
</FormControl>
)}
@@ -228,16 +224,11 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
<TextField
{...text}
{...field}
onChange={(e) => {
e.target.value = e.target.value
?.replace(/\D/, "")
.slice(0, 10);
field.onChange(e);
}}
type="number"
label={t("Update Interval")}
InputProps={{
endAdornment: (
<InputAdornment position="end">mins</InputAdornment>
<InputAdornment position="end">{t("mins")}</InputAdornment>
),
}}
/>
@@ -246,7 +237,12 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
)}
{isLocal && openType === "new" && (
<FileInput onChange={(val) => (fileDataRef.current = val)} />
<FileInput
onChange={(file, val) => {
formIns.setValue("name", formIns.getValues("name") || file.name);
fileDataRef.current = val;
}}
/>
)}
{isRemote && (
+83
View File
@@ -0,0 +1,83 @@
import {
Box,
Divider,
IconButton,
ListItem,
ListItemText,
Typography,
alpha,
} 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";
ruleRaw: string;
onDelete: () => void;
}
export const RuleItem = (props: Props) => {
let { type, ruleRaw, onDelete } = props;
const sortable = type === "prepend" || type === "append";
const rule = ruleRaw.replace(",no-resolve", "").split(",");
const { attributes, listeners, setNodeRef, transform, transition } = sortable
? useSortable({ id: ruleRaw })
: {
attributes: {},
listeners: {},
setNodeRef: null,
transform: null,
transition: null,
};
return (
<ListItem
sx={({ palette }) => ({
p: 0,
borderRadius: "10px",
border: "solid 2px",
borderColor:
type === "original"
? "var(--divider-color)"
: type === "delete"
? alpha(palette.error.main, 0.5)
: alpha(palette.success.main, 0.5),
mb: 1,
transform: CSS.Transform.toString(transform),
transition,
})}
>
<ListItemText
{...attributes}
{...listeners}
ref={setNodeRef}
sx={{ px: 1 }}
primary={
<>
<Typography
sx={{
textDecoration: type === "delete" ? "line-through" : "",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
}}
variant="h6"
title={rule.length === 3 ? rule[1] : "-"}
>
{rule.length === 3 ? rule[1] : "-"}
</Typography>
</>
}
secondary={
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Box>{rule[0]}</Box>
<Box>{rule.length === 3 ? rule[2] : rule[1]}</Box>
</Box>
}
/>
<Divider orientation="vertical" flexItem />
<IconButton size="small" color="inherit" onClick={onDelete}>
{type === "delete" ? <UndoRounded /> : <DeleteForeverRounded />}
</IconButton>
</ListItem>
);
};
@@ -0,0 +1,601 @@
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 {
Autocomplete,
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemText,
TextField,
styled,
} from "@mui/material";
import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice, Switch } from "@/components/base";
import getSystem from "@/utils/get-system";
import { RuleItem } from "@/components/profile/rule-item";
import { BaseSearchBox } from "../base/base-search-box";
import { Virtuoso } from "react-virtuoso";
interface Props {
profileUid: string;
title?: string | ReactNode;
property: string;
open: boolean;
onClose: () => void;
onSave?: (prev?: string, curr?: string) => void;
}
const portValidator = (value: string): boolean => {
return new RegExp(
"^(?:[1-9]\\d{0,3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])$"
).test(value);
};
const ipv4CIDRValidator = (value: string): boolean => {
return new RegExp(
"^(?:(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))\\.){3}(?:[1-9]?[0-9]|1[0-9][0-9]|2(?:[0-4][0-9]|5[0-5]))(?:\\/(?:[12]?[0-9]|3[0-2]))$"
).test(value);
};
const ipv6CIDRValidator = (value: string): boolean => {
return new RegExp(
"^([0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){7}|::|:(?::[0-9a-fA-F]{1,4}){1,6}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,5}|(?:[0-9a-fA-F]{1,4}:){2}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){3}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){4}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){5}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,6}:)\\/(?:12[0-8]|1[01][0-9]|[1-9]?[0-9])$"
).test(value);
};
const rules: {
name: string;
required?: boolean;
example?: string;
noResolve?: boolean;
validator?: (value: string) => boolean;
}[] = [
{
name: "DOMAIN",
example: "example.com",
},
{
name: "DOMAIN-SUFFIX",
example: "example.com",
},
{
name: "DOMAIN-KEYWORD",
example: "example",
},
{
name: "DOMAIN-REGEX",
example: "example.*",
},
{
name: "GEOSITE",
example: "youtube",
},
{
name: "GEOIP",
example: "CN",
noResolve: true,
},
{
name: "SRC-GEOIP",
example: "CN",
},
{
name: "IP-ASN",
example: "13335",
noResolve: true,
validator: (value) => (+value ? true : false),
},
{
name: "SRC-IP-ASN",
example: "9808",
validator: (value) => (+value ? true : false),
},
{
name: "IP-CIDR",
example: "127.0.0.0/8",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-CIDR6",
example: "2620:0:2d0:200::7/32",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-CIDR",
example: "192.168.1.201/32",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "IP-SUFFIX",
example: "8.8.8.8/24",
noResolve: true,
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-IP-SUFFIX",
example: "192.168.1.201/8",
validator: (value) => ipv4CIDRValidator(value) || ipv6CIDRValidator(value),
},
{
name: "SRC-PORT",
example: "7777",
validator: (value) => portValidator(value),
},
{
name: "DST-PORT",
example: "80",
validator: (value) => portValidator(value),
},
{
name: "IN-PORT",
example: "7890",
validator: (value) => portValidator(value),
},
{
name: "DSCP",
example: "4",
},
{
name: "PROCESS-NAME",
example: getSystem() === "windows" ? "chrome.exe" : "curl",
},
{
name: "PROCESS-PATH",
example:
getSystem() === "windows"
? "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
: "/usr/bin/wget",
},
{
name: "PROCESS-NAME-REGEX",
example: ".*telegram.*",
},
{
name: "PROCESS-PATH-REGEX",
example:
getSystem() === "windows" ? "(?i).*Application\\chrome.*" : ".*bin/wget",
},
{
name: "NETWORK",
example: "udp",
validator: (value) => ["tcp", "udp"].includes(value),
},
{
name: "UID",
example: "1001",
validator: (value) => (+value ? true : false),
},
{
name: "IN-TYPE",
example: "SOCKS/HTTP",
},
{
name: "IN-USER",
example: "mihomo",
},
{
name: "IN-NAME",
example: "ss",
},
{
name: "SUB-RULE",
example: "(NETWORK,tcp)",
},
{
name: "RULE-SET",
example: "providername",
noResolve: true,
},
{
name: "AND",
example: "((DOMAIN,baidu.com),(NETWORK,UDP))",
},
{
name: "OR",
example: "((NETWORK,UDP),(DOMAIN,baidu.com))",
},
{
name: "NOT",
example: "((DOMAIN,baidu.com))",
},
{
name: "MATCH",
required: false,
},
];
const builtinProxyPolicies = ["DIRECT", "REJECT", "REJECT-DROP", "PASS"];
export const RulesEditorViewer = (props: Props) => {
const { title, profileUid, property, open, onClose, onSave } = props;
const { t } = useTranslation();
const [prevData, setPrevData] = useState("");
const [match, setMatch] = useState(() => (_: string) => true);
const [ruleType, setRuleType] = useState<(typeof rules)[number]>(rules[0]);
const [ruleContent, setRuleContent] = useState("");
const [noResolve, setNoResolve] = useState(false);
const [proxyPolicy, setProxyPolicy] = useState(builtinProxyPolicies[0]);
const [proxyPolicyList, setProxyPolicyList] = useState<string[]>([]);
const [ruleList, setRuleList] = useState<string[]>([]);
const [ruleSetList, setRuleSetList] = useState<string[]>([]);
const [subRuleList, setSubRuleList] = useState<string[]>([]);
const [prependSeq, setPrependSeq] = useState<string[]>([]);
const [appendSeq, setAppendSeq] = useState<string[]>([]);
const [deleteSeq, setDeleteSeq] = useState<string[]>([]);
const filteredRuleList = useMemo(
() => ruleList.filter((rule) => match(rule)),
[ruleList, match]
);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
const reorder = (list: string[], 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 = prependSeq.indexOf(active.id.toString());
let overIndex = prependSeq.indexOf(over.id.toString());
setPrependSeq(reorder(prependSeq, activeIndex, overIndex));
}
}
};
const onAppendDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
if (over) {
if (active.id !== over.id) {
let activeIndex = appendSeq.indexOf(active.id.toString());
let overIndex = appendSeq.indexOf(over.id.toString());
setAppendSeq(reorder(appendSeq, activeIndex, overIndex));
}
}
};
const fetchContent = async () => {
let data = await readProfileFile(property);
let obj = yaml.load(data) as { prepend: []; append: []; delete: [] };
setPrependSeq(obj.prepend || []);
setAppendSeq(obj.append || []);
setDeleteSeq(obj.delete || []);
setPrevData(data);
};
const fetchProfile = async () => {
let data = await readProfileFile(profileUid);
let groupsObj = yaml.load(data) as { "proxy-groups": [] };
let rulesObj = yaml.load(data) as { rules: [] };
let ruleSetObj = yaml.load(data) as { "rule-providers": [] };
let subRuleObj = yaml.load(data) as { "sub-rules": [] };
setProxyPolicyList(
builtinProxyPolicies.concat(
groupsObj["proxy-groups"]
? groupsObj["proxy-groups"].map((item: any) => item.name)
: []
)
);
setRuleList(rulesObj.rules || []);
setRuleSetList(
ruleSetObj["rule-providers"]
? Object.keys(ruleSetObj["rule-providers"])
: []
);
setSubRuleList(
subRuleObj["sub-rules"] ? Object.keys(subRuleObj["sub-rules"]) : []
);
};
useEffect(() => {
fetchContent();
fetchProfile();
}, [open]);
const validateRule = () => {
if ((ruleType.required ?? true) && !ruleContent) {
throw new Error(t("Rule Condition Required"));
}
if (ruleType.validator && !ruleType.validator(ruleContent)) {
throw new Error(t("Invalid Rule"));
}
return `${ruleType.name}${
ruleContent ? "," + ruleContent : ""
},${proxyPolicy}${ruleType.noResolve && noResolve ? ",no-resolve" : ""}`;
};
const handleSave = useLockFn(async () => {
try {
let currData = yaml.dump({
prepend: prependSeq,
append: appendSeq,
delete: deleteSeq,
});
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>{title ?? t("Edit Rules")}</DialogTitle>
<DialogContent sx={{ display: "flex", width: "auto", height: "100vh" }}>
<List
sx={{
height: "calc(100% - 16px)",
width: "50%",
padding: "0 10px",
}}
>
<Item>
<ListItemText primary={t("Rule Type")} />
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={rules}
value={ruleType}
getOptionLabel={(option) => option.name}
renderOption={(props, option) => (
<li {...props} title={t(option.name)}>
{option.name}
</li>
)}
onChange={(_, value) => value && setRuleType(value)}
/>
</Item>
<Item sx={{ display: !(ruleType.required ?? true) ? "none" : "" }}>
<ListItemText primary={t("Rule Content")} />
{ruleType.name === "RULE-SET" && (
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={ruleSetList}
value={ruleContent}
onChange={(_, value) => value && setRuleContent(value)}
/>
)}
{ruleType.name === "SUB-RULE" && (
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={subRuleList}
value={ruleContent}
onChange={(_, value) => value && setRuleContent(value)}
/>
)}
{ruleType.name !== "RULE-SET" && ruleType.name !== "SUB-RULE" && (
<TextField
autoComplete="off"
size="small"
sx={{ minWidth: "240px" }}
value={ruleContent}
required={ruleType.required ?? true}
error={(ruleType.required ?? true) && !ruleContent}
placeholder={ruleType.example}
onChange={(e) => setRuleContent(e.target.value)}
/>
)}
</Item>
<Item>
<ListItemText primary={t("Proxy Policy")} />
<Autocomplete
size="small"
sx={{ minWidth: "240px" }}
renderInput={(params) => <TextField {...params} />}
options={proxyPolicyList}
value={proxyPolicy}
renderOption={(props, option) => (
<li {...props} title={t(option)}>
{option}
</li>
)}
onChange={(_, value) => value && setProxyPolicy(value)}
/>
</Item>
{ruleType.noResolve && (
<Item>
<ListItemText primary={t("No Resolve")} />
<Switch
checked={noResolve}
onChange={() => setNoResolve(!noResolve)}
/>
</Item>
)}
<Item>
<Button
fullWidth
variant="contained"
onClick={() => {
try {
let raw = validateRule();
if (prependSeq.includes(raw)) return;
setPrependSeq([...prependSeq, raw]);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
}}
>
{t("Prepend Rule")}
</Button>
</Item>
<Item>
<Button
fullWidth
variant="contained"
onClick={() => {
try {
let raw = validateRule();
if (appendSeq.includes(raw)) return;
setAppendSeq([...appendSeq, raw]);
} catch (err: any) {
Notice.error(err.message || err.toString());
}
}}
>
{t("Append Rule")}
</Button>
</Item>
</List>
<List
sx={{ height: "calc(100% - 16px)", width: "50%", padding: "0 10px" }}
>
<BaseSearchBox
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<Virtuoso
style={{ height: "calc(100% - 16px)", marginTop: "8px" }}
totalCount={
filteredRuleList.length +
(prependSeq.length > 0 ? 1 : 0) +
(appendSeq.length > 0 ? 1 : 0)
}
increaseViewportBy={256}
itemContent={(index) => {
let shift = prependSeq.length > 0 ? 1 : 0;
if (prependSeq.length > 0 && index === 0) {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onPrependDragEnd}
>
<SortableContext
items={prependSeq.map((x) => {
return x;
})}
>
{prependSeq.map((item, index) => {
return (
<RuleItem
key={`${item}-${index}`}
type="prepend"
ruleRaw={item}
onDelete={() => {
setPrependSeq(
prependSeq.filter((v) => v !== item)
);
}}
/>
);
})}
</SortableContext>
</DndContext>
);
} else if (index < filteredRuleList.length + shift) {
let newIndex = index - shift;
return (
<RuleItem
key={`${filteredRuleList[newIndex]}-${index}`}
type={
deleteSeq.includes(filteredRuleList[newIndex])
? "delete"
: "original"
}
ruleRaw={filteredRuleList[newIndex]}
onDelete={() => {
if (deleteSeq.includes(filteredRuleList[newIndex])) {
setDeleteSeq(
deleteSeq.filter(
(v) => v !== filteredRuleList[newIndex]
)
);
} else {
setDeleteSeq((prev) => [
...prev,
filteredRuleList[newIndex],
]);
}
}}
/>
);
} else {
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onAppendDragEnd}
>
<SortableContext
items={appendSeq.map((x) => {
return x;
})}
>
{appendSeq.map((item, index) => {
return (
<RuleItem
key={`${item}-${index}`}
type="append"
ruleRaw={item}
onDelete={() => {
setAppendSeq(appendSeq.filter((v) => v !== item));
}}
/>
);
})}
</SortableContext>
</DndContext>
);
}
}}
/>
</List>
</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",
}));
+6 -8
View File
@@ -69,7 +69,7 @@ export const ProviderButton = () => {
sx={{ textTransform: "capitalize" }}
onClick={() => setOpen(true)}
>
{t("Provider")}
{t("Proxy Provider")}
</Button>
<BaseDialog
@@ -81,11 +81,9 @@ export const ProviderButton = () => {
variant="contained"
size="small"
onClick={async () => {
Object.entries(data || {}).forEach(
async ([key, item], index) => {
await handleUpdate(key, index);
}
);
Object.entries(data || {}).forEach(async ([key], index) => {
await handleUpdate(key, index);
});
}}
>
{t("Update All")}
@@ -94,7 +92,7 @@ export const ProviderButton = () => {
}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -171,7 +169,7 @@ export const ProviderButton = () => {
<IconButton
size="small"
color="inherit"
title="Update Provider"
title={`${t("Update")}${t("Proxy Provider")}`}
onClick={() => handleUpdate(key, index)}
sx={{
animation: updating[index]
+3 -1
View File
@@ -14,12 +14,14 @@ import { BaseEmpty } from "../base";
import { useRenderList } from "./use-render-list";
import { ProxyRender } from "./proxy-render";
import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next";
interface Props {
mode: string;
}
export const ProxyGroups = (props: Props) => {
const { t } = useTranslation();
const { mode } = props;
const { renderList, onProxies, onHeadState } = useRenderList(mode);
@@ -116,7 +118,7 @@ export const ProxyGroups = (props: Props) => {
};
if (mode === "direct") {
return <BaseEmpty text="Direct Mode" />;
return <BaseEmpty text={t("clash_mode_direct")} />;
}
return (
+2 -1
View File
@@ -132,6 +132,7 @@ export const ProxyHead = (props: Props) => {
{textState === "filter" && (
<TextField
autoComplete="off"
autoFocus={autoFocus}
hiddenLabel
value={filterText}
@@ -145,10 +146,10 @@ export const ProxyHead = (props: Props) => {
{textState === "url" && (
<TextField
autoComplete="off"
autoFocus={autoFocus}
hiddenLabel
autoSave="off"
autoComplete="off"
value={testUrl}
size="small"
variant="outlined"
+6 -1
View File
@@ -21,6 +21,8 @@ export const ProxyItemMini = (props: Props) => {
const { t } = useTranslation();
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
const isPreset = presetList.includes(proxy.name);
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
@@ -28,6 +30,7 @@ export const ProxyItemMini = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
if (isPreset) return;
delayManager.setListener(proxy.name, group.name, setDelay);
return () => {
@@ -160,7 +163,9 @@ export const ProxyItemMini = (props: Props) => {
</Box>
)}
</Box>
<Box sx={{ ml: 0.5, color: "primary.main" }}>
<Box
sx={{ ml: 0.5, color: "primary.main", display: isPreset ? "none" : "" }}
>
{delay === -2 && (
<Widget>
<BaseLoading />
+8 -1
View File
@@ -46,12 +46,15 @@ const TypeBox = styled(Box)(({ theme }) => ({
export const ProxyItem = (props: Props) => {
const { group, proxy, selected, showType = true, sx, onClick } = props;
const presetList = ["DIRECT", "REJECT", "REJECT-DROP", "PASS", "COMPATIBLE"];
const isPreset = presetList.includes(proxy.name);
// -1/<=0 为 不显示
// -2 为 loading
const [delay, setDelay] = useState(-1);
const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => {
if (isPreset) return;
delayManager.setListener(proxy.name, group.name, setDelay);
return () => {
@@ -131,7 +134,11 @@ export const ProxyItem = (props: Props) => {
/>
<ListItemIcon
sx={{ justifyContent: "flex-end", color: "primary.main" }}
sx={{
justifyContent: "flex-end",
color: "primary.main",
display: isPreset ? "none" : "",
}}
>
{delay === -2 && (
<Widget>
+2 -3
View File
@@ -17,8 +17,7 @@ import { ProxyItem } from "./proxy-item";
import { ProxyItemMini } from "./proxy-item-mini";
import type { IRenderItem } from "./use-render-list";
import { useVerge } from "@/hooks/use-verge";
import { useRecoilState } from "recoil";
import { atomThemeMode } from "@/services/states";
import { useThemeMode } from "@/services/states";
import { useEffect, useMemo, useState } from "react";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { downloadIconCache } from "@/services/cmds";
@@ -38,7 +37,7 @@ export const ProxyRender = (props: RenderProps) => {
const { type, group, headState, proxy, proxyCol } = item;
const { verge } = useVerge();
const enable_group_icon = verge?.enable_group_icon ?? true;
const [mode] = useRecoilState(atomThemeMode);
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const itembackgroundcolor = isDark ? "#282A36" : "#ffffff";
const [iconCachePath, setIconCachePath] = useState("");
+6 -8
View File
@@ -67,7 +67,7 @@ export const ProviderButton = () => {
sx={{ textTransform: "capitalize" }}
onClick={() => setOpen(true)}
>
{t("Provider")}
{t("Rule Provider")}
</Button>
<BaseDialog
@@ -79,11 +79,9 @@ export const ProviderButton = () => {
variant="contained"
size="small"
onClick={async () => {
Object.entries(data || {}).forEach(
async ([key, item], index) => {
await handleUpdate(key, index);
}
);
Object.entries(data || {}).forEach(async ([key], index) => {
await handleUpdate(key, index);
});
}}
>
{t("Update All")}
@@ -92,7 +90,7 @@ export const ProviderButton = () => {
}
contentSx={{ width: 400 }}
disableOk
cancelBtn={t("Cancel")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -145,7 +143,7 @@ export const ProviderButton = () => {
<IconButton
size="small"
color="inherit"
title="Update Provider"
title={`${t("Update")}${t("Rule Provider")}`}
onClick={() => handleUpdate(key, index)}
sx={{
animation: updating[index]
@@ -9,23 +9,19 @@ import { SwitchAccessShortcut, RestartAlt } from "@mui/icons-material";
import {
Box,
Button,
Tooltip,
Chip,
List,
ListItemButton,
ListItemText,
} from "@mui/material";
import { changeClashCore, restartSidecar } from "@/services/cmds";
import { closeAllConnections, upgradeCore } from "@/services/api";
import { grantPermission } from "@/services/cmds";
import getSystem from "@/utils/get-system";
const VALID_CORE = [
{ name: "Clash Meta", core: "clash-meta" },
{ name: "Clash Meta Alpha", core: "clash-meta-alpha" },
{ name: "Mihomo", core: "verge-mihomo", chip: "Release Version" },
{ name: "Mihomo Alpha", core: "verge-mihomo-alpha", chip: "Alpha Version" },
];
const OS = getSystem();
export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -39,7 +35,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
close: () => setOpen(false),
}));
const { clash_core = "clash-meta" } = verge ?? {};
const { clash_core = "verge-mihomo" } = verge ?? {};
const onCoreChange = useLockFn(async (core: string) => {
if (core === clash_core) return;
@@ -58,17 +54,6 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
}
});
const onGrant = useLockFn(async (core: string) => {
try {
await grantPermission(core);
// 自动重启
if (core === clash_core) await restartSidecar();
Notice.success(t("Permissions Granted Successfully for _clash Core", { core: `${core}` }), 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
});
const onRestart = useLockFn(async () => {
try {
await restartSidecar();
@@ -128,7 +113,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
marginTop: "-8px",
}}
disableOk
cancelBtn={t("Back")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -140,22 +125,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
onClick={() => onCoreChange(each.core)}
>
<ListItemText primary={each.name} secondary={`/${each.core}`} />
{(OS === "macos" || OS === "linux") && (
<Tooltip title={t("Tun mode requires")}>
<Button
variant="outlined"
size="small"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onGrant(each.core);
}}
>
{t("Grant")}
</Button>
</Tooltip>
)}
<Chip label={t(`${each.chip}`)} size="small" />
</ListItemButton>
))}
</List>
@@ -122,7 +122,7 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
return (
<BaseDialog
open={open}
title={t("Clash Port")}
title={t("Port Config")}
contentSx={{ width: 300 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
@@ -132,10 +132,10 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
>
<List>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Mixed Port" />
<ListItemText primary={t("Mixed Port")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 135 }}
value={mixedPort}
onChange={(e) =>
@@ -144,10 +144,10 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Socks Port" />
<ListItemText primary={t("Socks Port")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 135 }}
value={socksPort}
onChange={(e) =>
@@ -167,10 +167,10 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Http Port" />
<ListItemText primary={t("Http Port")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 135 }}
value={port}
onChange={(e) =>
@@ -191,10 +191,10 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
{OS !== "windows" && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Redir Port" />
<ListItemText primary={t("Redir Port")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 135 }}
value={redirPort}
onChange={(e) =>
@@ -216,10 +216,10 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
)}
{OS === "linux" && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Tproxy Port" />
<ListItemText primary={t("Tproxy Port")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 135 }}
value={tproxyPort}
onChange={(e) =>
@@ -22,15 +22,14 @@ export const ConfigViewer = forwardRef<DialogRef>((_, ref) => {
return (
<EditorViewer
open={open}
title={
<Box>
{t("Runtime Config")}
<Chip label={t("ReadOnly")} size="small" />
</Box>
}
mode="text"
property={runtimeConfig}
open={open}
initialData={Promise.resolve(runtimeConfig)}
readOnly
language="yaml"
schema="clash"
@@ -48,8 +48,8 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("External Controller")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 175 }}
value={controller}
placeholder="Required"
@@ -60,8 +60,8 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Core Secret")} />
<TextField
size="small"
autoComplete="off"
size="small"
sx={{ width: 175 }}
value={secret}
placeholder={t("Recommended")}
+17 -7
View File
@@ -2,6 +2,7 @@ import { useRef, useState } from "react";
import { alpha, Box, IconButton, styled } from "@mui/material";
import { DeleteRounded } from "@mui/icons-material";
import { parseHotkey } from "@/utils/parse-hotkey";
import { useTranslation } from "react-i18next";
const KeyWrapper = styled("div")(({ theme }) => ({
position: "relative",
@@ -41,9 +42,12 @@ const KeyWrapper = styled("div")(({ theme }) => ({
border: "1px solid",
borderColor: alpha(theme.palette.text.secondary, 0.2),
borderRadius: "2px",
padding: "1px 1px",
padding: "1px 5px",
margin: "2px 0",
marginRight: 8,
},
".delimiter": {
lineHeight: "25px",
padding: "0 2px",
},
}));
@@ -54,6 +58,7 @@ interface Props {
export const HotkeyInput = (props: Props) => {
const { value, onChange } = props;
const { t } = useTranslation();
const changeRef = useRef<string[]>([]);
const [keys, setKeys] = useState(value);
@@ -83,17 +88,22 @@ export const HotkeyInput = (props: Props) => {
/>
<div className="list">
{keys.map((key) => (
<div key={key} className="item">
{key}
</div>
{keys.map((key, index) => (
<Box display="flex">
<span className="delimiter" hidden={index === 0}>
+
</span>
<div key={key} className="item">
{key}
</div>
</Box>
))}
</div>
</KeyWrapper>
<IconButton
size="small"
title="Delete"
title={t("Delete")}
color="inherit"
onClick={() => {
onChange([]);
@@ -79,7 +79,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
title={t("Layout Setting")}
contentSx={{ width: 450 }}
disableOk
cancelBtn={t("Cancel")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
@@ -127,7 +127,7 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</Item>
<Item>
<ListItemText primary={t("Menu Icon")} />
<ListItemText primary={t("Nav Icon")} />
<GuardState
value={verge?.menu_icon ?? "monochrome"}
onCatch={onError}
+43 -22
View File
@@ -8,9 +8,11 @@ import {
MenuItem,
Select,
TextField,
InputAdornment,
} from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
@@ -81,12 +83,12 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
size="small"
sx={{ width: 100, "> div": { py: "7.5px" } }}
value={values.appLogLevel}
onChange={(e) => {
onChange={(e) =>
setValues((v) => ({
...v,
appLogLevel: e.target.value as string,
}));
}}
}))
}
>
{["trace", "debug", "info", "warn", "error", "silent"].map((i) => (
<MenuItem value={i} key={i}>
@@ -97,13 +99,18 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Auto Close Connections")} />
<ListItemText
primary={t("Auto Close Connections")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Auto Close Connections Info")} />
<Switch
edge="end"
checked={values.autoCloseConnection}
onChange={(_, c) =>
setValues((v) => ({ ...v, autoCloseConnection: c }))
}
sx={{ marginLeft: "auto" }}
/>
</ListItem>
@@ -119,31 +126,36 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Enable Builtin Enhanced")} />
<ListItemText
primary={t("Enable Builtin Enhanced")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Enable Builtin Enhanced Info")} />
<Switch
edge="end"
checked={values.enableBuiltinEnhanced}
onChange={(_, c) =>
setValues((v) => ({ ...v, enableBuiltinEnhanced: c }))
}
sx={{ marginLeft: "auto" }}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Layout Column")} />
<ListItemText primary={t("Proxy Layout Columns")} />
<Select
size="small"
sx={{ width: 135, "> div": { py: "7.5px" } }}
value={values.proxyLayoutColumn}
onChange={(e) => {
onChange={(e) =>
setValues((v) => ({
...v,
proxyLayoutColumn: e.target.value as number,
}));
}}
}))
}
>
<MenuItem value={6} key={6}>
Auto
{t("Auto Columns")}
</MenuItem>
{[1, 2, 3, 4, 5].map((i) => (
<MenuItem value={i} key={i}>
@@ -159,35 +171,39 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
size="small"
sx={{ width: 135, "> div": { py: "7.5px" } }}
value={values.autoLogClean}
onChange={(e) => {
onChange={(e) =>
setValues((v) => ({
...v,
autoLogClean: e.target.value as number,
}));
}}
}))
}
>
{[
{ key: "Never Clean", value: 0 },
{ key: "Retain 7 Days", value: 1 },
{ key: "Retain 30 Days", value: 2 },
{ key: "Retain 90 Days", value: 3 },
{ key: t("Never Clean"), value: 0 },
{ key: t("Retain _n Days", { n: 7 }), value: 1 },
{ key: t("Retain _n Days", { n: 30 }), value: 2 },
{ key: t("Retain _n Days", { n: 90 }), value: 3 },
].map((i) => (
<MenuItem key={i.value} value={i.value}>
{t(i.key)}
{i.key}
</MenuItem>
))}
</Select>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Test")} />
<ListItemText
primary={t("Default Latency Test")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Default Latency Test Info")} />
<TextField
size="small"
autoComplete="off"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
sx={{ width: 250, marginLeft: "auto" }}
value={values.defaultLatencyTest}
placeholder="http://1.1.1.1"
onChange={(e) =>
@@ -199,9 +215,9 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Default Latency Timeout")} />
<TextField
autoComplete="off"
size="small"
type="number"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
@@ -214,6 +230,11 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
defaultLatencyTimeout: parseInt(e.target.value),
}))
}
InputProps={{
endAdornment: (
<InputAdornment position="end">{t("millis")}</InputAdornment>
),
}}
/>
</ListItem>
</List>
@@ -41,8 +41,11 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
const onInstall = useLockFn(async () => {
try {
await installService();
mutateCheck();
await mutateCheck();
setOpen(false);
setTimeout(() => {
mutateCheck();
}, 2000);
Notice.success(t("Service Installed Successfully"));
} catch (err: any) {
mutateCheck();
+34 -9
View File
@@ -1,34 +1,59 @@
import React, { ReactNode } from "react";
import React, { ReactNode, useState } from "react";
import {
Box,
List,
ListItem,
ListItemButton,
ListItemText,
ListSubheader,
} from "@mui/material";
import { ChevronRightRounded } from "@mui/icons-material";
import CircularProgress from "@mui/material/CircularProgress";
import isAsyncFunction from "@/utils/is-async-function";
interface ItemProps {
label: ReactNode;
extra?: ReactNode;
children?: ReactNode;
secondary?: ReactNode;
onClick?: () => void | Promise<any>;
}
export const SettingItem: React.FC<ItemProps> = (props) => {
const { label, extra, children, secondary } = props;
const { label, extra, children, secondary, onClick } = props;
const clickable = !!onClick;
const primary = !extra ? (
const primary = (
<Box sx={{ display: "flex", alignItems: "center", fontSize: "14px" }}>
<span>{label}</span>
</Box>
) : (
<Box sx={{ display: "flex", alignItems: "center", fontSize: "14px" }}>
<span>{label}</span>
{extra}
{extra ? extra : null}
</Box>
);
return (
const [isLoading, setIsLoading] = useState(false);
const handleClick = () => {
if (onClick) {
if (isAsyncFunction(onClick)) {
setIsLoading(true);
onClick()!.finally(() => setIsLoading(false));
} else {
onClick();
}
}
};
return clickable ? (
<ListItem disablePadding>
<ListItemButton onClick={handleClick} disabled={isLoading}>
<ListItemText primary={primary} secondary={secondary} />
{isLoading ? (
<CircularProgress color="inherit" size={20} />
) : (
<ChevronRightRounded />
)}
</ListItemButton>
</ListItem>
) : (
<ListItem sx={{ pt: "5px", pb: "5px" }}>
<ListItemText primary={primary} secondary={secondary} />
{children}
@@ -1,11 +1,4 @@
import { useTranslation } from "react-i18next";
import { Button, ButtonGroup, Tooltip } from "@mui/material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import getSystem from "@/utils/get-system";
import useSWR from "swr";
const isWIN = getSystem() === "windows";
import { Button, ButtonGroup } from "@mui/material";
interface Props {
value?: string;
@@ -14,58 +7,30 @@ interface Props {
export const StackModeSwitch = (props: Props) => {
const { value, onChange } = props;
const { verge } = useVerge();
const { enable_service_mode } = verge ?? {};
// service mode
const { data: serviceStatus } = useSWR(
isWIN ? "checkService" : null,
checkService,
{
revalidateIfStale: false,
shouldRetryOnError: false,
}
);
const { t } = useTranslation();
return (
<Tooltip
title={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
? t("System and Mixed Can Only be Used in Service Mode")
: ""
}
>
<ButtonGroup size="small" sx={{ my: "4px" }}>
<Button
variant={value?.toLowerCase() === "system" ? "contained" : "outlined"}
onClick={() => onChange?.("system")}
disabled={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
}
sx={{ textTransform: "capitalize" }}
>
System
</Button>
<Button
variant={value?.toLowerCase() === "gvisor" ? "contained" : "outlined"}
onClick={() => onChange?.("gvisor")}
sx={{ textTransform: "capitalize" }}
>
gVisor
</Button>
<Button
variant={value?.toLowerCase() === "mixed" ? "contained" : "outlined"}
onClick={() => onChange?.("mixed")}
disabled={
isWIN && (serviceStatus !== "active" || !enable_service_mode)
}
sx={{ textTransform: "capitalize" }}
>
Mixed
</Button>
</ButtonGroup>
</Tooltip>
<ButtonGroup size="small" sx={{ my: "4px" }}>
<Button
variant={value?.toLowerCase() === "system" ? "contained" : "outlined"}
onClick={() => onChange?.("system")}
sx={{ textTransform: "capitalize" }}
>
System
</Button>
<Button
variant={value?.toLowerCase() === "gvisor" ? "contained" : "outlined"}
onClick={() => onChange?.("gvisor")}
sx={{ textTransform: "capitalize" }}
>
gVisor
</Button>
<Button
variant={value?.toLowerCase() === "mixed" ? "contained" : "outlined"}
onClick={() => onChange?.("mixed")}
sx={{ textTransform: "capitalize" }}
>
Mixed
</Button>
</ButtonGroup>
);
};
+98 -75
View File
@@ -2,7 +2,6 @@ import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
Box,
InputAdornment,
List,
ListItem,
@@ -17,12 +16,23 @@ import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { Edit } from "@mui/icons-material";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { BaseFieldset } from "@/components/base/base-fieldset";
import getSystem from "@/utils/get-system";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}`;
export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
let validReg;
if (getSystem() === "windows") {
validReg =
/^((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\*|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+|localhost|<local>)(;((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\d{1,3}\.\*|\d{1,3}\.\*|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+|localhost|<local>))*;?$/;
} else {
validReg =
/^((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?|localhost|<local>)(,((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){1,3}\d{1,3}(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|\d{1,3}\.\*(\/\d{1,2}|\/3[0-2])?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?|localhost|<local>))*,?$/;
}
const [open, setOpen] = useState(false);
const [editorOpen, setEditorOpen] = useState(false);
@@ -39,6 +49,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
proxy_auto_config,
pac_file_content,
enable_proxy_guard,
use_default_bypass,
system_proxy_bypass,
proxy_guard_duration,
} = verge ?? {};
@@ -47,6 +58,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
guard: enable_proxy_guard,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
});
@@ -58,6 +70,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
guard: enable_proxy_guard,
bypass: system_proxy_bypass,
duration: proxy_guard_duration ?? 10,
use_default: use_default_bypass ?? true,
pac: proxy_auto_config,
pac_content: pac_file_content ?? DEFAULT_PAC,
});
@@ -87,10 +100,16 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (value.pac !== proxy_auto_config) {
patch.proxy_auto_config = value.pac;
}
if (value.use_default !== use_default_bypass) {
patch.use_default_bypass = value.use_default;
}
if (value.pac_content !== pac_file_content) {
patch.pac_file_content = value.pac_content;
}
if (value.bypass && !validReg.test(value.bypass)) {
Notice.error(t("Invalid Bypass Format"));
return;
}
try {
await patchVerge(patch);
setOpen(false);
@@ -103,7 +122,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<BaseDialog
open={open}
title={t("System Proxy Setting")}
contentSx={{ width: 450, maxHeight: 300 }}
contentSx={{ width: 450, maxHeight: 565 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
@@ -111,6 +130,36 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
onOk={onSave}
>
<List>
<BaseFieldset label={t("Current System Proxy")} padding="15px 10px">
<FlexBox>
<Typography className="label">{t("Enable status")}</Typography>
<Typography className="value">
{value.pac
? autoproxy?.enable
? t("Enabled")
: t("Disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
</Typography>
</FlexBox>
{!value.pac && (
<>
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="value">
{sysproxy?.server ? sysproxy.server : t("Not available")}
</Typography>
</FlexBox>
</>
)}
{value.pac && (
<FlexBox>
<Typography className="label">{t("PAC URL")}</Typography>
<Typography className="value">{autoproxy?.url || "-"}</Typography>
</FlexBox>
)}
</BaseFieldset>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Use PAC Mode")} />
<Switch
@@ -120,19 +169,26 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
onChange={(_, e) => setValue((v) => ({ ...v, pac: e }))}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Guard")} />
<ListItemText
primary={t("Proxy Guard")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Proxy Guard Info")} />
<Switch
edge="end"
disabled={!enabled}
checked={value.guard}
onChange={(_, e) => setValue((v) => ({ ...v, guard: e }))}
sx={{ marginLeft: "auto" }}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} />
<TextField
autoComplete="off"
disabled={!enabled}
size="small"
value={value.duration}
@@ -148,29 +204,45 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
}}
/>
</ListItem>
{!value.pac && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Always use Default Bypass")} />
<Switch
edge="end"
disabled={!enabled}
checked={value.use_default}
onChange={(_, e) => setValue((v) => ({ ...v, use_default: e }))}
/>
</ListItem>
)}
{!value.pac && (
<>
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
<ListItemText
primary={t("Proxy Bypass")}
sx={{ padding: "3px 0" }}
/>
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Bypass")} />
<TextField
autoComplete="off"
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
size="small"
multiline
rows={4}
sx={{ width: "100%" }}
value={value.bypass}
onChange={(e) => {
setValue((v) => ({ ...v, bypass: e.target.value }));
}}
/>
<ListItemText primary={t("Bypass")} />
<FlexBox>
<TextField
disabled={!enabled}
size="small"
autoComplete="off"
disabled={true}
size="small"
multiline
rows={4}
sx={{ width: "100%" }}
value={value.bypass}
placeholder={sysproxy?.bypass || `-`}
onChange={(e) =>
setValue((v) => ({ ...v, bypass: e.target.value }))
}
value={sysproxy?.bypass || "-"}
/>
</ListItem>
</FlexBox>
</>
)}
{value.pac && (
@@ -190,72 +262,23 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
{t("Edit")} PAC
</Button>
<EditorViewer
title={`${t("Edit")} PAC`}
mode="text"
property={value.pac_content ?? ""}
open={editorOpen}
title={`${t("Edit")} PAC`}
initialData={Promise.resolve(value.pac_content ?? "")}
language="javascript"
onChange={(content) => {
onSave={(_prev, curr) => {
let pac = DEFAULT_PAC;
if (content && content.trim().length > 0) {
pac = content;
if (curr && curr.trim().length > 0) {
pac = curr;
}
setValue((v) => ({ ...v, pac_content: pac }));
}}
onClose={() => {
setEditorOpen(false);
}}
onClose={() => setEditorOpen(false)}
/>
</ListItem>
</>
)}
</List>
<Box sx={{ mt: 2.5 }}>
<Typography variant="body1" sx={{ fontSize: "18px", mb: 1 }}>
{t("Current System Proxy")}
</Typography>
<FlexBox>
<Typography className="label">{t("Enable status")}</Typography>
<Typography className="value">
{value.pac
? (!!autoproxy?.enable).toString()
: (!!sysproxy?.enable).toString()}
</Typography>
</FlexBox>
{!value.pac && (
<>
<FlexBox>
<Typography className="label">{t("Server Addr")}</Typography>
<Typography className="value">
{sysproxy?.server || "-"}
</Typography>
</FlexBox>
<FlexBox>
<Typography className="label">{t("Bypass")}</Typography>
</FlexBox>
<FlexBox>
<TextField
disabled={true}
size="small"
autoComplete="off"
multiline
rows={4}
sx={{ width: "100%" }}
value={sysproxy?.bypass || "-"}
/>
</FlexBox>
</>
)}
{value.pac && (
<FlexBox>
<Typography className="label">{t("PAC URL")}</Typography>
<Typography className="value">{autoproxy?.url || "-"}</Typography>
</FlexBox>
)}
</Box>
</BaseDialog>
);
});
@@ -266,6 +289,6 @@ const FlexBox = styled("div")`
.label {
flex: none;
width: 85px;
//width: 85px;
}
`;
+5 -6
View File
@@ -81,7 +81,7 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
title={t("Theme Setting")}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
contentSx={{ width: 400, maxHeight: 300, overflow: "auto", pb: 0 }}
contentSx={{ width: 400, maxHeight: 505, overflow: "auto", pb: 0 }}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
@@ -124,13 +124,12 @@ export const ThemeViewer = forwardRef<DialogRef>((props, ref) => {
{t("Edit")} CSS
</Button>
<EditorViewer
title={`${t("Edit")} CSS`}
mode="text"
property={theme.css_injection ?? ""}
open={editorOpen}
title={`${t("Edit")} CSS`}
initialData={Promise.resolve(theme.css_injection ?? "")}
language="css"
onChange={(content) => {
theme.css_injection = content;
onSave={(_prev, curr) => {
theme.css_injection = curr;
handleChange("css_injection");
}}
onClose={() => {
+9 -9
View File
@@ -23,7 +23,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
stack: "gvisor",
device: "Meta",
device: "Mihomo",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53"],
@@ -36,7 +36,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
setOpen(true);
setValues({
stack: clash?.tun.stack ?? "gvisor",
device: clash?.tun.device ?? "Meta",
device: clash?.tun.device ?? "Mihomo",
autoRoute: clash?.tun["auto-route"] ?? true,
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
@@ -51,7 +51,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
try {
let tun = {
stack: values.stack,
device: values.device === "" ? "Meta" : values.device,
device: values.device === "" ? "Mihomo" : values.device,
"auto-route": values.autoRoute,
"auto-detect-interface": values.autoDetectInterface,
"dns-hijack": values.dnsHijack[0] === "" ? [] : values.dnsHijack,
@@ -90,7 +90,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
onClick={async () => {
let tun = {
stack: "gvisor",
device: "Meta",
device: "Mihomo",
"auto-route": true,
"auto-detect-interface": true,
"dns-hijack": ["any:53"],
@@ -99,7 +99,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
};
setValues({
stack: "gvisor",
device: "Meta",
device: "Mihomo",
autoRoute: true,
autoDetectInterface: true,
dnsHijack: ["any:53"],
@@ -144,14 +144,14 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Device")} />
<TextField
size="small"
autoComplete="off"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
sx={{ width: 250 }}
value={values.device}
placeholder="Meta"
placeholder="Mihomo"
onChange={(e) =>
setValues((v) => ({ ...v, device: e.target.value }))
}
@@ -190,8 +190,8 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("DNS Hijack")} />
<TextField
size="small"
autoComplete="off"
size="small"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
@@ -207,9 +207,9 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("MTU")} />
<TextField
autoComplete="off"
size="small"
type="number"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
+36 -5
View File
@@ -1,15 +1,15 @@
import useSWR from "swr";
import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
import { useLockFn } from "ahooks";
import { Box, LinearProgress } from "@mui/material";
import { useRecoilState } from "recoil";
import { Box, LinearProgress, Button } from "@mui/material";
import { useTranslation } from "react-i18next";
import { relaunch } from "@tauri-apps/api/process";
import { checkUpdate, installUpdate } from "@tauri-apps/api/updater";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { atomUpdateState } from "@/services/states";
import { useUpdateState, useSetUpdateState } from "@/services/states";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
import { portableFlag } from "@/pages/_layout";
import { open as openUrl } from "@tauri-apps/api/shell";
import ReactMarkdown from "react-markdown";
let eventListener: UnlistenFn | null = null;
@@ -18,7 +18,9 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const [updateState, setUpdateState] = useRecoilState(atomUpdateState);
const updateState = useUpdateState();
const setUpdateState = useSetUpdateState();
const { data: updateInfo } = useSWR("checkUpdate", checkUpdate, {
errorRetryCount: 2,
@@ -42,11 +44,23 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
return updateInfo?.manifest?.body;
}, [updateInfo]);
const breakChangeFlag = useMemo(() => {
if (!updateInfo?.manifest?.body) {
return false;
}
return updateInfo?.manifest?.body.toLowerCase().includes("break change");
}, [updateInfo]);
const onUpdate = useLockFn(async () => {
if (portableFlag) {
Notice.error(t("Portable Updater Error"));
return;
}
if (!updateInfo?.manifest?.body) return;
if (breakChangeFlag) {
Notice.error(t("Break Change Update Error"));
return;
}
if (updateState) return;
setUpdateState(true);
if (eventListener !== null) {
@@ -75,7 +89,24 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
return (
<BaseDialog
open={open}
title={`New Version v${updateInfo?.manifest?.version}`}
title={
<Box display="flex" justifyContent="space-between">
{`New Version v${updateInfo?.manifest?.version}`}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.manifest?.version}`
);
}}
>
{t("Go to Release Page")}
</Button>
</Box>
</Box>
}
contentSx={{ minWidth: 360, maxWidth: 400, height: "50vh" }}
okBtn={t("Update")}
cancelBtn={t("Cancel")}
+9 -7
View File
@@ -13,6 +13,7 @@ import {
EditRounded,
OpenInNewRounded,
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
interface Props {
value?: string;
@@ -35,22 +36,23 @@ export const WebUIItem = (props: Props) => {
const [editing, setEditing] = useState(false);
const [editValue, setEditValue] = useState(value);
const { t } = useTranslation();
if (editing || onlyEdit) {
return (
<>
<Stack spacing={0.75} direction="row" mt={1} mb={1} alignItems="center">
<TextField
autoComplete="off"
fullWidth
size="small"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
placeholder={`Support %host %port %secret`}
autoComplete="off"
placeholder={t("Support %host, %port, %secret")}
/>
<IconButton
size="small"
title="Save"
title={t("Save")}
color="inherit"
onClick={() => {
onChange(editValue);
@@ -61,7 +63,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
title="Cancel"
title={t("Cancel")}
color="inherit"
onClick={() => {
onCancel?.();
@@ -100,7 +102,7 @@ export const WebUIItem = (props: Props) => {
/>
<IconButton
size="small"
title="Open URL"
title={t("Open URL")}
color="inherit"
onClick={() => onOpenUrl?.(value)}
>
@@ -108,7 +110,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
title="Edit"
title={t("Edit")}
color="inherit"
onClick={() => {
setEditing(true);
@@ -119,7 +121,7 @@ export const WebUIItem = (props: Props) => {
</IconButton>
<IconButton
size="small"
title="Delete"
title={t("Delete")}
color="inherit"
onClick={onDelete}
>
@@ -23,7 +23,7 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
}));
const webUIList = verge?.web_ui_list || [
"https://d.metacubex.one/#?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",
];
@@ -98,17 +98,16 @@ export const WebUIViewer = forwardRef<DialogRef>((props, ref) => {
overflowY: "auto",
userSelect: "text",
}}
cancelBtn={t("Back")}
cancelBtn={t("Close")}
disableOk
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
>
{!editing && webUIList.length === 0 && (
<BaseEmpty
text="Empty List"
extra={
<Typography mt={2} sx={{ fontSize: "12px" }}>
Replace host, port, secret with "%host" "%port" "%secret"
{t("Replace host, port, secret with %host, %port, %secret")}
</Typography>
}
/>
+34 -78
View File
@@ -1,15 +1,8 @@
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import {
TextField,
Select,
MenuItem,
Typography,
IconButton,
Tooltip,
} from "@mui/material";
import { ArrowForward, Settings, Shuffle } from "@mui/icons-material";
import { TextField, Select, MenuItem, Typography } from "@mui/material";
import { Settings, Shuffle } from "@mui/icons-material";
import { DialogRef, Notice, Switch } from "@/components/base";
import { useClash } from "@/hooks/use-clash";
import { GuardState } from "./mods/guard-state";
@@ -22,6 +15,7 @@ import { invoke_uwp_tool } from "@/services/cmds";
import getSystem from "@/utils/get-system";
import { useVerge } from "@/hooks/use-verge";
import { updateGeoData } from "@/services/api";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
const isWIN = getSystem() === "windows";
@@ -51,14 +45,14 @@ const SettingClash = ({ onError }: Props) => {
const onChangeVerge = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
};
const onUpdateGeo = useLockFn(async () => {
const onUpdateGeo = async () => {
try {
await updateGeoData();
Notice.success(t("GeoData Updated"));
} catch (err: any) {
Notice.error(err?.response.data.message || err.toString());
}
});
};
return (
<SettingList title={t("Clash Setting")}>
@@ -115,27 +109,24 @@ const SettingClash = ({ onError }: Props) => {
<SettingItem
label={t("Port Config")}
extra={
<Tooltip title={t("Random Port")}>
<IconButton
color={enable_random_port ? "primary" : "inherit"}
size="small"
onClick={() => {
Notice.success(t("Restart Application to Apply Modifications"), 1000);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });
}}
>
<Shuffle
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
<TooltipIcon
title={t("Random Port")}
color={enable_random_port ? "primary" : "inherit"}
icon={Shuffle}
onClick={() => {
Notice.success(
t("Restart Application to Apply Modifications"),
1000
);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });
}}
/>
}
>
<TextField
disabled={enable_random_port}
autoComplete="off"
disabled={enable_random_port}
size="small"
value={verge_mixed_port ?? 7897}
sx={{ width: 100, input: { py: "7.5px", cursor: "pointer" } }}
@@ -146,69 +137,34 @@ const SettingClash = ({ onError }: Props) => {
/>
</SettingItem>
<SettingItem label={t("External")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => ctrlRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={() => ctrlRef.current?.open()}
label={t("External")}
/>
<SettingItem label={t("Web UI")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => webRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={() => webRef.current?.open()} label={t("Web UI")} />
<SettingItem
label={t("Clash Core")}
extra={
<IconButton
color="inherit"
size="small"
<TooltipIcon
icon={Settings}
onClick={() => coreRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
/>
}
>
<Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography>
</SettingItem>
{isWIN && (
<SettingItem label={t("Open UWP tool")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={invoke_uwp_tool}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={invoke_uwp_tool}
label={t("Open UWP tool")}
extra={<TooltipIcon title={t("Open UWP tool Info")} />}
/>
)}
<SettingItem label={t("Update GeoData")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onUpdateGeo}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={onUpdateGeo} label={t("Update GeoData")} />
</SettingList>
);
};
+21 -41
View File
@@ -1,8 +1,7 @@
import useSWR from "swr";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { IconButton, Tooltip } from "@mui/material";
import { PrivacyTipRounded, Settings, InfoRounded } from "@mui/icons-material";
import { PrivacyTipRounded, Settings } from "@mui/icons-material";
import { checkService } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { DialogRef, Switch } from "@/components/base";
@@ -11,6 +10,7 @@ import { GuardState } from "./mods/guard-state";
import { ServiceViewer } from "./mods/service-viewer";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
interface Props {
onError?: (err: Error) => void;
@@ -54,26 +54,11 @@ const SettingSystem = ({ onError }: Props) => {
<SettingItem
label={t("Tun Mode")}
extra={
<>
<Tooltip title={t("Tun Mode Info")} placement="top">
<IconButton color="inherit" size="small">
<InfoRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
<IconButton
color="inherit"
size="small"
onClick={() => tunRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</>
<TooltipIcon
title={t("Tun Mode Info")}
icon={Settings}
onClick={() => tunRef.current?.open()}
/>
}
>
<GuardState
@@ -91,16 +76,11 @@ const SettingSystem = ({ onError }: Props) => {
<SettingItem
label={t("Service Mode")}
extra={
<IconButton
color="inherit"
size="small"
<TooltipIcon
title={t("Service Mode Info")}
icon={PrivacyTipRounded}
onClick={() => serviceRef.current?.open()}
>
<PrivacyTipRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
/>
}
>
<GuardState
@@ -123,16 +103,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingItem
label={t("System Proxy")}
extra={
<IconButton
color="inherit"
size="small"
onClick={() => sysproxyRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
<>
<TooltipIcon
title={t("System Proxy Info")}
icon={Settings}
onClick={() => sysproxyRef.current?.open()}
/>
</IconButton>
</>
}
>
<GuardState
@@ -160,7 +137,10 @@ const SettingSystem = ({ onError }: Props) => {
</GuardState>
</SettingItem>
<SettingItem label={t("Silent Start")}>
<SettingItem
label={t("Silent Start")}
extra={<TooltipIcon title={t("Silent Start Info")} />}
>
<GuardState
value={enable_silent_start ?? false}
valueProps="checked"
+35 -124
View File
@@ -1,15 +1,7 @@
import { useRef } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/api/dialog";
import {
Button,
IconButton,
MenuItem,
Select,
Input,
Typography,
} from "@mui/material";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material";
import {
exitApp,
openAppDir,
@@ -17,7 +9,6 @@ import {
openLogsDir,
openDevTools,
} from "@/services/cmds";
import { ArrowForward } from "@mui/icons-material";
import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
@@ -63,7 +54,7 @@ const SettingVerge = ({ onError }: Props) => {
mutateVerge({ ...verge, ...patch }, false);
};
const onCheckUpdate = useLockFn(async () => {
const onCheckUpdate = async () => {
try {
const info = await checkUpdate();
if (!info?.shouldUpdate) {
@@ -74,7 +65,7 @@ const SettingVerge = ({ onError }: Props) => {
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
};
return (
<SettingList title={t("Verge Setting")}>
@@ -213,128 +204,48 @@ const SettingVerge = ({ onError }: Props) => {
></Input>
</GuardState>
</SettingItem>
<SettingItem label={t("Theme Setting")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => themeRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Layout Setting")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => layoutRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={() => themeRef.current?.open()}
label={t("Theme Setting")}
/>
<SettingItem label={t("Miscellaneous")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => miscRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={() => layoutRef.current?.open()}
label={t("Layout Setting")}
/>
<SettingItem label={t("Hotkey Setting")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => hotkeyRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={() => miscRef.current?.open()}
label={t("Miscellaneous")}
/>
<SettingItem label={t("Runtime Config")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => configRef.current?.open()}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={() => hotkeyRef.current?.open()}
label={t("Hotkey Setting")}
/>
<SettingItem label={t("Open App Dir")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={openAppDir}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
/>
<SettingItem label={t("Open Core Dir")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={openCoreDir}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={openAppDir} label={t("Open App Dir")} />
<SettingItem label={t("Open Logs Dir")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={openLogsDir}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
<SettingItem label={t("Check for Updates")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={onCheckUpdate}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
<SettingItem label={t("Open Dev Tools")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={openDevTools}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
<SettingItem label={t("Exit")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={() => {
exitApp();
}}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
<SettingItem
onClick={() => {
exitApp();
}}
label={t("Exit")}
/>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
+1 -1
View File
@@ -175,7 +175,7 @@ export const TestItem = (props: Props) => {
":hover": { bgcolor: alpha(palette.primary.main, 0.15) },
})}
>
Check
{t("Test")}
</Widget>
)}
+1 -1
View File
@@ -27,7 +27,7 @@ export const useClash = () => {
const version = versionData?.premium
? `${versionData.version} Premium`
: versionData?.meta
? `${versionData.version} Meta`
? `${versionData.version} Mihomo`
: versionData?.version || "-";
return {
+57
View File
@@ -0,0 +1,57 @@
import useSWRSubscription from "swr/subscription";
import { useEnableLog } from "../services/states";
import { createSockette } from "../utils/websocket";
import { useClashInfo } from "./use-clash";
import dayjs from "dayjs";
import { getClashLogs } from "../services/cmds";
const MAX_LOG_NUM = 1000;
export const useLogData = () => {
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
!enableLog || !clashInfo;
return useSWRSubscription<ILogItem[], any, "getClashLog" | null>(
enableLog && clashInfo ? "getClashLog" : null,
(_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
// populate the initial logs
getClashLogs().then(
(logs) => next(null, logs),
(err) => next(err)
);
const s = createSockette(
`ws://${server}/logs?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
const data = JSON.parse(event.data) as ILogItem;
// append new log item on socket message
next(null, (l = []) => {
const time = dayjs().format("MM-DD HH:mm:ss");
if (l.length >= MAX_LOG_NUM) l.shift();
return [...l, { ...data, time }];
});
},
onerror(event) {
this.close();
next(event);
},
}
);
return () => {
s.close();
};
},
{
fallbackData: [],
keepPreviousData: true,
}
);
};
-60
View File
@@ -1,60 +0,0 @@
import { useRef } from "react";
export type WsMsgFn = (event: MessageEvent<any>) => void;
export interface WsOptions {
errorCount?: number; // default is 5
retryInterval?: number; // default is 2500
onError?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
}
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
const wsRef = useRef<WebSocket | null>(null);
const timerRef = useRef<any>(null);
const disconnect = () => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
const connect = (url: string) => {
let errorCount = options?.errorCount ?? 5;
if (!url) return;
const connectHelper = () => {
disconnect();
const ws = new WebSocket(url);
wsRef.current = ws;
ws.addEventListener("message", (event) => {
errorCount = 0; // reset counter
onMessage(event);
});
ws.addEventListener("error", (event) => {
errorCount -= 1;
if (errorCount >= 0) {
timerRef.current = setTimeout(connectHelper, 2500);
} else {
disconnect();
options?.onError?.(event);
}
});
ws.addEventListener("close", (event) => {
options?.onClose?.(event);
});
};
connectHelper();
};
return { connect, disconnect };
};
+250 -174
View File
@@ -1,60 +1,36 @@
{
"millis": "millis",
"mins": "mins",
"Back": "Back",
"Close": "Close",
"Cancel": "Cancel",
"Confirm": "Confirm",
"Maximize": "Maximize",
"Minimize": "Minimize",
"Format document": "Format document",
"Empty": "Empty",
"New": "New",
"Edit": "Edit",
"Save": "Save",
"Delete": "Delete",
"Enable": "Enable",
"Disable": "Disable",
"Label-Proxies": "Proxies",
"Label-Test": "Test",
"Label-Profiles": "Profiles",
"Label-Connections": "Connections",
"Label-Logs": "Logs",
"Label-Rules": "Rules",
"Label-Logs": "Logs",
"Label-Test": "Test",
"Label-Settings": "Settings",
"Connections": "Connections",
"Logs": "Logs",
"Clear": "Clear",
"Proxies": "Proxies",
"Proxy Groups": "Proxy Groups",
"Test": "Test",
"Proxy Provider": "Proxy Provider",
"Update All": "Update All",
"Update At": "Update At",
"rule": "rule",
"global": "global",
"direct": "direct",
"script": "script",
"Create Test": "Create Test",
"Edit Test": "Edit Test",
"Edit": "Edit",
"Icon": "Icon",
"Test URL": "Test URL",
"Test All": "Test All",
"Paste": "Paste",
"Profiles": "Profiles",
"Profile URL": "Profile URL",
"Import": "Import",
"New": "New",
"Create Profile": "Create Profile",
"Edit Profile": "Edit Profile",
"Choose File": "Choose File",
"Close": "Close",
"Close All": "Close All",
"Home": "Home",
"Select": "Select",
"Edit Info": "Edit Info",
"Edit File": "Edit File",
"Open File": "Open File",
"Update": "Update",
"Update(Proxy)": "Update(Proxy)",
"Delete": "Delete",
"Enable": "Enable",
"Disable": "Disable",
"Refresh": "Refresh",
"To Top": "To Top",
"To End": "To End",
"Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config",
"Reactivate Profiles": "Reactivate Profiles",
"Confirm deletion": "Confirm deletion",
"This operation is not reversible": "This operation is not reversible",
"Script Console": "Script Console",
"Location": "Location",
"Delay check": "Delay check",
"Sort by default": "Sort by default",
@@ -64,13 +40,101 @@
"Delay check to cancel fixed": "Delay check to cancel fixed",
"Proxy basic": "Proxy basic",
"Proxy detail": "Proxy detail",
"Filter": "Filter",
"Filter conditions": "Filter conditions",
"Refresh profiles": "Refresh profiles",
"Rules": "Rules",
"Update All": "Update All",
"Update At": "Update At",
"Profiles": "Profiles",
"Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config",
"Reactivate Profiles": "Reactivate Profiles",
"Paste": "Paste",
"Profile URL": "Profile URL",
"Import": "Import",
"From": "From",
"Update Time": "Update Time",
"Used / Total": "Used / Total",
"Expire Time": "Expire Time",
"Create Profile": "Create Profile",
"Edit Profile": "Edit Profile",
"Edit Proxies": "Edit Proxies",
"Edit Rules": "Edit Rules",
"Rule Type": "Rule Type",
"Rule Content": "Rule Content",
"Proxy Policy": "Proxy Policy",
"No Resolve": "No Resolve",
"Prepend Rule": "Prepend Rule",
"Append Rule": "Append Rule",
"Delete Rule": "Delete Rule",
"Rule Condition Required": "Rule Condition Required",
"Invalid Rule": "Invalid Rule",
"DOMAIN": "Matches the full domain name",
"DOMAIN-SUFFIX": "Matches the domain suffix",
"DOMAIN-KEYWORD": "Matches the domain keyword",
"DOMAIN-REGEX": "Matches the domain using regular expressions",
"GEOSITE": "Matches domains within the Geosite",
"GEOIP": "Matches the country code of the IP address",
"SRC-GEOIP": "Matches the country code of the source IP address",
"IP-ASN": "Matches the IP address's ASN",
"SRC-IP-ASN": "Matches the source IP address's ASN",
"IP-CIDR": "Matches the IP address range",
"IP-CIDR6": "Matches the IPv6 address range",
"SRC-IP-CIDR": "Matches the source IP address range",
"IP-SUFFIX": "Matches the IP address suffix range",
"SRC-IP-SUFFIX": "Matches the source IP address suffix range",
"SRC-PORT": "Matches the source port range",
"DST-PORT": "Matches the destination port range",
"IN-PORT": "Matches the inbound port",
"DSCP": "DSCP marking (only for tproxy UDP inbound)",
"PROCESS-NAME": "Matches the process name (Android package name)",
"PROCESS-PATH": "Matches the full process path",
"PROCESS-NAME-REGEX": "Matches the full process name using regular expressions (Android package name)",
"PROCESS-PATH-REGEX": "Matches the full process path using regular expressions",
"NETWORK": "Matches the transport protocol (tcp/udp)",
"UID": "Matches the Linux USER ID",
"IN-TYPE": "Matches the inbound type",
"IN-USER": "Matches the inbound username",
"IN-NAME": "Matches the inbound name",
"SUB-RULE": "Sub-rule",
"RULE-SET": "Matches the rule set",
"AND": "Logical AND",
"OR": "Logical OR",
"NOT": "Logical NOT",
"MATCH": "Matches all requests",
"DIRECT": "Data goes directly outbound",
"REJECT": "Intercepts requests",
"REJECT-DROP": "Discards requests",
"PASS": "Skips this rule when matched",
"Edit Groups": "Edit Proxy Groups",
"Extend Config": "Extend Config",
"Extend Script": "Extend Script",
"Global Merge": "Global Extend Config",
"Global Script": "Global Extend Script",
"Type": "Type",
"Name": "Name",
"Descriptions": "Descriptions",
"Subscription URL": "Subscription URL",
"Update Interval": "Update Interval",
"Choose File": "Choose File",
"Use System Proxy": "Use System Proxy",
"Use Clash Proxy": "Use Clash Proxy",
"Accept Invalid Certs (Danger)": "Allows Invalid Certificates (Danger)",
"Refresh": "Refresh",
"Home": "Home",
"Select": "Select",
"Edit Info": "Edit Info",
"Edit File": "Edit File",
"Open File": "Open File",
"Update": "Update",
"Update(Proxy)": "Update(Proxy)",
"Confirm deletion": "Confirm deletion",
"This operation is not reversible": "This operation is not reversible",
"Script Console": "Script Console",
"To Top": "To Top",
"To End": "To End",
"Connections": "Connections",
"Table View": "Table View",
"List View": "List View",
"Close All": "Close All",
"Default": "Default",
"Download Speed": "Download Speed",
"Upload Speed": "Upload Speed",
"Host": "Host",
"Downloaded": "Downloaded",
"Uploaded": "Uploaded",
@@ -83,89 +147,107 @@
"Source": "Source",
"Destination IP": "Destination IP",
"Close Connection": "Close Connection",
"Type": "Type",
"Name": "Name",
"Descriptions": "Descriptions",
"Subscription URL": "Subscription URL",
"Update Interval": "Update Interval",
"Use System Proxy": "Use System Proxy",
"Use Clash Proxy": "Use Clash Proxy",
"Accept Invalid Certs (Danger)": "Allows Invalid Certificates (Danger)",
"Rules": "Rules",
"Rule Provider": "Rule Provider",
"Logs": "Logs",
"Pause": "Pause",
"Clear": "Clear",
"Test": "Test",
"Test All": "Test All",
"Create Test": "Create Test",
"Edit Test": "Edit Test",
"Icon": "Icon",
"Test URL": "Test URL",
"Settings": "Settings",
"Clash Setting": "Clash Setting",
"System Setting": "System Setting",
"Verge Setting": "Verge Setting",
"Allow Lan": "Allow Lan",
"Tun Mode": "Tun Mode",
"Reset to Default": "Reset to Default",
"Tun Mode Info": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"Stack": "Tun Stack",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
"Device": "Device Name",
"Auto Route": "Auto Route",
"Strict Route": "Strict Route",
"Auto Detect Interface": "Auto Detect Interface",
"DNS Hijack": "DNS Hijack",
"MTU": "Max Transmission Unit",
"Service Mode": "Service Mode",
"Service Mode Info": "The core launched by the service can obtain corresponding permissions after installation and authorization of the service",
"Current State": "Current State",
"pending": "pending",
"installed": "installed",
"uninstall": "uninstalled",
"active": "active",
"unknown": "unknown",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Information: Please make sure that the Clash Verge Service is installed and enabled",
"Install": "Install",
"Uninstall": "Uninstall",
"Disable Service Mode": "Disable Service Mode",
"System Proxy": "System Proxy",
"System Proxy Info": "Enable to modify the operating system's proxy settings. If enabling fails, modify the operating system's proxy settings manually",
"System Proxy Setting": "System Proxy Setting",
"Current System Proxy": "Current System Proxy",
"Enable status": "Enable Status:",
"Enabled": "Enabled",
"Disabled": "Disabled",
"Server Addr": "Server Addr: ",
"Not available": "Not available",
"Proxy Guard": "Proxy Guard",
"Proxy Guard Info": "Enable to prevent other software from modifying the operating system's proxy settings",
"Guard Duration": "Guard Duration",
"Always use Default Bypass": "Always use Default Bypass",
"Proxy Bypass": "Proxy Bypass Settings: ",
"Bypass": "Bypass: ",
"Use PAC Mode": "Use PAC Mode",
"PAC Script Content": "PAC Script Content",
"PAC URL": "PAC URL: ",
"Auto Launch": "Auto Launch",
"Silent Start": "Silent Start",
"Silent Start Info": "Start the program in background mode without displaying the panel",
"TG Channel": "Telegram Channel",
"Manual": "Manual",
"Github Repo": "Github Repo",
"Clash Setting": "Clash Setting",
"Allow Lan": "Allow LAN",
"IPv6": "IPv6",
"Log Level": "Log Level",
"Port Config": "Port Config",
"Random Port": "Random Port",
"Restart Application to Apply Modifications": "Restart Application to Apply Modifications",
"Mixed Port": "Mixed Port",
"Socks Port": "Socks Port",
"Http Port": "Http(s) Port",
"Redir Port": "Redir Port",
"Tproxy Port": "Tproxy Port",
"External": "External",
"External Controller": "External Controller",
"Core Secret": "Core Secret",
"Recommended": "Recommended",
"Open URL": "Open URL",
"Replace host, port, secret with %host, %port, %secret": "Replace host, port, secret with %host, %port, %secret",
"Support %host, %port, %secret": "Support %host, %port, %secret",
"Clash Core": "Clash Core",
"Grant": "Grant",
"Upgrade": "Upgrade",
"Restart": "Restart",
"Release Version": "Release Version",
"Alpha Version": "Alpha Version",
"Tun mode requires": "Tun mode requires",
"Tun Mode": "Tun Mode",
"Service Mode": "Service Mode",
"Auto Launch": "Auto Launch",
"Silent Start": "Silent Start",
"System Proxy": "System Proxy",
"System Proxy Setting": "System Proxy Setting",
"Grant": "Grant",
"Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
"Update GeoData": "Update GeoData",
"Use PAC Mode": "Use PAC Mode",
"PAC URL": "PAC URL",
"PAC Script Content": "PAC Script Content",
"Proxy Guard": "Proxy Guard",
"Guard Duration": "Guard Duration",
"Proxy Bypass": "Proxy Bypass",
"Current System Proxy": "Current System Proxy",
"Enable status": "Enable status",
"Server Addr": "Server Addr",
"Bypass": "Bypass",
"Verge Setting": "Verge Setting",
"Language": "Language",
"Theme Mode": "Theme Mode",
"theme.light": "Light",
"theme.dark": "Dark",
"theme.system": "System",
"Tray Click Event": "Tray Click Event",
"Show Main Window": "Show Main Window",
"Copy Env Type": "Copy Env Type",
"Start Page": "Start Page",
"Startup Script": "Startup Script",
"Browse": "Browse",
"Show Main Window": "Show Main Window",
"Theme Setting": "Theme Setting",
"Layout Setting": "Layout Setting",
"Miscellaneous": "Miscellaneous",
"Hotkey Setting": "Hotkey Setting",
"Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage",
"Proxy Group Icon": "Proxy Group Icon",
"Tray Icon": "Tray Icon",
"Menu Icon": "Menu Icon",
"Monochrome": "Monochrome",
"Colorful": "Colorful",
"Common Tray Icon": "Common Tray Icon",
"System Proxy Tray Icon": "System Proxy Tray Icon",
"Tun Tray Icon": "Tun Tray Icon",
"Language": "Language",
"Open App Dir": "Open App Dir",
"Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates",
"Open Dev Tools": "Open Dev Tools",
"Verge Version": "Verge Version",
"theme.light": "Light",
"theme.dark": "Dark",
"theme.system": "System",
"Clash Field": "Clash Field",
"Runtime Config": "Runtime Config",
"ReadOnly": "ReadOnly",
"ReadOnlyMessage": "Cannot edit in read-only editor",
"Restart": "Restart",
"Upgrade": "Upgrade",
"Primary Color": "Primary Color",
"Secondary Color": "Secondary Color",
"Primary Text": "Primary Text",
@@ -176,82 +258,76 @@
"Success Color": "Success Color",
"Font Family": "Font Family",
"CSS Injection": "CSS Injection",
"Back": "Back",
"Save": "Save",
"Cancel": "Cancel",
"Exit": "Exit",
"Confirm": "Confirm",
"Default": "Default",
"Download Speed": "Download Speed",
"Upload Speed": "Upload Speed",
"Layout Setting": "Layout Setting",
"Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage",
"Memory Cleanup": "Tap to clean up memory",
"Proxy Group Icon": "Proxy Group Icon",
"Nav Icon": "Nav Icon",
"Monochrome": "Monochrome",
"Colorful": "Colorful",
"Tray Icon": "Tray Icon",
"Common Tray Icon": "Common Tray Icon",
"System Proxy Tray Icon": "System Proxy Tray Icon",
"Tun Tray Icon": "Tun Tray Icon",
"Miscellaneous": "Miscellaneous",
"App Log Level": "App Log Level",
"Auto Close Connections": "Auto Close Connections",
"Auto Close Connections Info": "Terminate established connections when the proxy group selection or proxy mode changes",
"Auto Check Update": "Auto Check Update",
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
"Enable Builtin Enhanced Info": "Compatibility handling for the configuration file",
"Proxy Layout Columns": "Proxy Layout Columns",
"Auto Columns": "Auto Columns",
"Auto Log Clean": "Auto Log Clean",
"Never Clean": "Never Clean",
"Retain _n Days": "Retain {{n}} Days",
"Default Latency Test": "Default Latency Test",
"Default Latency Test Info": "Used for HTTP client request testing only and won't make a difference to the configuration file",
"Default Latency Timeout": "Default Latency Timeout",
"Hotkey Setting": "Hotkey Setting",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Rule Mode",
"clash_mode_global": "Global Mode",
"clash_mode_direct": "Direct Mode",
"toggle_system_proxy": "Enable/Disable System Proxy",
"toggle_tun_mode": "Enable/Disable Tun Mode",
"App Log Level": "App Log Level",
"Auto Close Connections": "Auto Close Connections",
"Auto Check Update": "Auto Check Update",
"Enable Builtin Enhanced": "Enable Builtin Enhanced",
"Proxy Layout Column": "Proxy Layout Column",
"Default Latency Test": "Default Latency Test",
"Default Latency Timeout": "Default Latency Timeout",
"Auto Log Clean": "Auto Log Clean",
"Never Clean": "Never Clean",
"Retain 7 Days": "Retain 7 Days",
"Retain 30 Days": "Retain 30 Days",
"Retain 90 Days": "Retain 90 Days",
"Stack": "Tun Stack",
"Device": "Device Name",
"Auto Route": "Auto Route",
"Strict Route": "Strict Route",
"Auto Detect Interface": "Auto Detect Interface",
"DNS Hijack": "DNS Hijack",
"MTU": "Max Transmission Unit",
"Reset to Default": "Reset to Default",
"Current State": "Current State",
"pending": "pending",
"installed": "installed",
"uninstall": "uninstalled",
"active": "active",
"unknown": "unknown",
"Disable Service Mode": "Disable Service Mode",
"Install": "Install",
"Uninstall": "Uninstall",
"Runtime Config": "Runtime Config",
"Open App Dir": "Open App Dir",
"Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates",
"Go to Release Page": "Go to Release Page",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
"Tun Mode Info": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Information: Please make sure that the Clash Verge Service is installed and enabled",
"Break Change Update Error": "This version is a major update and does not support in-app updates. Please uninstall it and manually download and install the new version",
"Open Dev Tools": "Open Dev Tools",
"Exit": "Exit",
"Verge Version": "Verge Version",
"ReadOnly": "ReadOnly",
"ReadOnlyMessage": "Cannot edit in read-only editor",
"Filter": "Filter",
"Filter conditions": "Filter conditions",
"Match Case": "Match Case",
"Match Whole Word": "Match Whole Word",
"Use Regular Expression": "Use Regular Expression",
"External Controller Address Modified": "External Controller Address Modified",
"Clash Port Modified": "Clash Port Modified",
"Currently on the Latest Version": "Currently on the Latest Version",
"Only YAML Files Supported": "Only YAML Files Supported",
"Port Conflict": "Port Conflict",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Proxy Daemon Duration Cannot be Less than 1 Second",
"Profile Imported Successfully": "Profile Imported Successfully",
"Clash Config Updated": "Clash Config Updated",
"Profile Switched": "Profile Switched",
"Profile Reactivated": "Profile Reactivated",
"Clash Config Updated": "Clash Config Updated",
"Only YAML Files Supported": "Only YAML Files Supported",
"Settings Applied": "Settings Applied",
"Service Installed Successfully": "Service Installed Successfully",
"Service Uninstalled Successfully": "Service Uninstalled Successfully",
"GeoData Updated": "GeoData Updated",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Proxy Daemon Duration Cannot be Less than 1 Second",
"Invalid Bypass Format": "Invalid Bypass Format",
"Clash Port Modified": "Clash Port Modified",
"Port Conflict": "Port Conflict",
"Restart Application to Apply Modifications": "Restart Application to Apply Modifications",
"External Controller Address Modified": "External Controller Address Modified",
"Permissions Granted Successfully for _clash Core": "Permissions Granted Successfully for {{core}} Core",
"Profile Imported Successfully": "Profile Imported Successfully",
"Core Version Updated": "Core Version Updated",
"Clash Core Restarted": "Clash Core Restarted",
"Switched to _clash Core": "Switched to {{core}} Core",
"Core Version Updated": "Core Version Updated"
"GeoData Updated": "GeoData Updated",
"Currently on the Latest Version": "Currently on the Latest Version"
}

Some files were not shown because too many files have changed in this diff Show More