Compare commits

...
88 Commits
Author SHA1 Message Date
MystiPanda 8ff2d687e4 Release 1.7.2 2024-07-03 13:26:06 +08:00
MystiPanda e616e8f9aa fix: Insertion order
#1300
2024-07-03 13:13:54 +08:00
MystiPanda 1fbc67fe98 fix: dialog styles 2024-07-03 13:13:29 +08:00
MystiPanda a49e4d8d0c fix: panic 2024-07-03 12:49:29 +08:00
MystiPanda fa4ac00504 feat: support edit rules file 2024-07-03 12:37:08 +08:00
MystiPanda dc066edec4 fix: edit groups error 2024-07-03 10:58:57 +08:00
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
102 changed files with 3895 additions and 1980 deletions
+1 -1
View File
@@ -2,4 +2,4 @@ pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
cargo tauri build --target $INPUT_TARGET
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
+10 -8
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,9 +46,8 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Tauri CLI
run: cargo install --git https://github.com/tauri-apps/tauri --branch 1.x tauri-cli
cache-all-crates: true
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@v4
@@ -80,7 +83,7 @@ jobs:
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: cargo tauri
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
@@ -161,9 +164,8 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Tauri CLI
run: cargo install --git https://github.com/tauri-apps/tauri --branch 1.x tauri-cli
cache-all-crates: true
cache-on-failure: true
- name: Install Node
uses: actions/setup-node@v4
@@ -196,7 +198,7 @@ jobs:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: cargo tauri
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename
+1
View File
@@ -39,6 +39,7 @@ jobs:
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
cache-all-crates: true
- name: Install Node
uses: actions/setup-node@v4
+115
View File
@@ -1,3 +1,118 @@
## v1.7.2
### 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.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
+17 -13
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "1.6.6",
"version": "1.7.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "tauri dev",
@@ -24,40 +24,44 @@
"@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.1",
"@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.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",
"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",
@@ -72,10 +76,10 @@
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"sass": "^1.77.5",
"sass": "^1.77.6",
"terser": "^5.31.1",
"typescript": "^5.4.5",
"vite": "^5.2.13",
"typescript": "^5.5.3",
"vite": "^5.3.2",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},
+382 -321
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");
+165 -68
View File
@@ -224,7 +224,7 @@ dependencies = [
"futures-io",
"futures-lite 2.3.0",
"parking",
"polling 3.7.1",
"polling 3.7.2",
"rustix 0.38.34",
"slab",
"tracing",
@@ -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",
]
@@ -783,7 +784,7 @@ dependencies = [
[[package]]
name = "clash-verge"
version = "1.6.6"
version = "1.7.2"
dependencies = [
"anyhow",
"auto-launch",
@@ -801,7 +802,7 @@ dependencies = [
"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]]
@@ -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"
@@ -2268,9 +2275,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.9.3"
version = "1.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0e7a4dd27b9476dc40cb050d3632d3bba3a70ddbff012285f7f8559a1e7e545"
checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9"
[[package]]
name = "httpdate"
@@ -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]]
@@ -2563,14 +2571,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"icu_normalizer",
"icu_properties",
"smallvec",
"utf8_iter",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
@@ -2694,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",
]
@@ -3079,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"
@@ -3146,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",
@@ -3391,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",
]
@@ -3586,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",
]
@@ -3760,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",
]
@@ -4058,13 +4064,13 @@ dependencies = [
[[package]]
name = "polling"
version = "3.7.1"
version = "3.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1"
checksum = "a3ed00ed3fbf728b5816498ecd316d1716eecaced9c0c8d2c5a6740ca214985b"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"hermit-abi 0.4.0",
"pin-project-lite",
"rustix 0.38.34",
"tracing",
@@ -4180,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"
@@ -4307,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",
]
@@ -4429,7 +4482,7 @@ dependencies = [
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"sync_wrapper 0.1.2",
"system-configuration",
"tokio",
"tokio-native-tls",
@@ -4445,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",
@@ -4470,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",
@@ -4606,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",
@@ -5178,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"
@@ -5210,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"
@@ -5382,8 +5442,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
[[package]]
name = "tauri"
version = "1.6.7"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e3b2c32de5bf5bed15a376064286643862b84e4e091d093f7bcdf1fda4b9758"
dependencies = [
"anyhow",
"base64 0.21.7",
@@ -5445,8 +5506,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "1.5.2"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c6ec7a5c3296330c7818478948b422967ce4649094696c985f61d50076d29c"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5463,8 +5525,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "1.4.3"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "1.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1aed706708ff1200ec12de9cfbf2582b5d8ec05f6a7293911091effbd22036b"
dependencies = [
"base64 0.21.7",
"brotli",
@@ -5488,8 +5551,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "1.4.4"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88f831d2973ae4f81a706a0004e67dac87f2e4439973bbe98efbd73825d8ede"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -5501,8 +5565,9 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "0.14.3"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3068ed62b63dedc705558f4248c7ecbd5561f0f8050949859ea0db2326f26012"
dependencies = [
"gtk",
"http 0.2.12",
@@ -5521,8 +5586,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "0.14.8"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4c3db170233096aa30330feadcd895bf9317be97e624458560a20e814db7955"
dependencies = [
"arboard",
"cocoa 0.24.1",
@@ -5541,8 +5607,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "1.5.4"
source = "git+https://github.com/tauri-apps/tauri?branch=1.x#d2786bf699ffca1d5e9c234a3b1b4d5ec173af87"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2826db448309d382dac14d520f0c0a40839b87b57b977e59cf5f296b3ace6a93"
dependencies = [
"brotli",
"ctor",
@@ -5782,6 +5849,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.38.0"
@@ -5824,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",
@@ -6112,12 +6194,27 @@ dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.11.0"
@@ -6147,9 +6244,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.1"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
@@ -6542,9 +6639,9 @@ dependencies = [
[[package]]
name = "webpki-roots"
version = "0.26.2"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3"
checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd"
dependencies = [
"rustls-pki-types",
]
@@ -7261,9 +7358,9 @@ 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",
@@ -7299,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.6"
version = "1.7.2"
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/tauri-apps/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/tauri-apps/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

-9
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> {
+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),
+196 -39
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 {
@@ -107,6 +117,11 @@ impl PrfOption {
.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),
@@ -135,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}\""),
}
@@ -153,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()),
@@ -166,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())),
@@ -188,7 +240,11 @@ impl PrfItem {
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();
// 使用软件自己的代理
@@ -282,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,
@@ -307,7 +357,7 @@ impl PrfItem {
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?;
@@ -323,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()),
@@ -332,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()),
@@ -341,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,
@@ -357,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,
@@ -383,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();
+63 -75
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);
@@ -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()?;
-48
View File
@@ -1,48 +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}");
}
}
-1
View File
@@ -4,7 +4,6 @@ mod core;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod manager;
pub mod service;
pub mod sysopt;
pub mod timer;
+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(())
}
+14 -6
View File
@@ -42,8 +42,8 @@ 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 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();
@@ -55,15 +55,23 @@ fn get_bypass() -> String {
};
#[cfg(target_os = "windows")]
let bypass = if custom_bypass.is_empty() {
bypass
DEFAULT_BYPASS.to_string()
} else {
format!("{};{}", bypass, custom_bypass)
if use_default {
format!("{};{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
#[cfg(not(target_os = "windows"))]
let bypass = if custom_bypass.is_empty() {
bypass
DEFAULT_BYPASS.to_string()
} else {
format!("{},{}", bypass, custom_bypass)
if use_default {
format!("{},{}", DEFAULT_BYPASS, custom_bypass)
} else {
custom_bypass
}
};
bypass
+19 -3
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,
}
@@ -92,8 +108,8 @@ impl ChainSupport {
(self, core.as_str()),
(ChainSupport::All, _)
| (ChainSupport::Clash, "clash")
| (ChainSupport::ClashMeta, "clash-meta")
| (ChainSupport::ClashMetaAlpha, "clash-meta-alpha")
| (ChainSupport::ClashMeta, "verge-mihomo")
| (ChainSupport::ClashMetaAlpha, "verge-mihomo-alpha")
),
None => true,
}
+1 -14
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();
+5 -49
View File
@@ -1,14 +1,5 @@
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) {
@@ -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
}
+132 -30
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() {
@@ -149,7 +251,7 @@ pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
});
}
config = use_tun(config, enable_tun);
config = use_tun(config, enable_tun).await;
config = use_sort(config);
let mut exists_set = HashSet::new();
+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 mut 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_or(&Sequence::default()).clone()
});
let mut seq = origin_seq.clone();
prepend.reverse();
for item in prepend {
seq.insert(0, item);
}
for item in append {
seq.push(item);
}
let mut delete_names = Vec::new();
for item in delete {
let item = item.clone();
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
}
}
+5 -6
View File
@@ -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();
}
@@ -200,23 +200,23 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
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() {
@@ -292,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)?;
+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<()> {
+24 -31
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());
@@ -101,9 +98,7 @@ pub fn resolve_setup(app: &mut App) {
if argvs.len() > 1 {
let param = argvs[1].as_str();
if param.starts_with("clash:") {
tauri::async_runtime::block_on(async {
resolve_scheme(argvs[1].to_owned()).await;
});
log_err!(resolve_scheme(argvs[1].to_owned()).await);
}
}
}
@@ -111,7 +106,10 @@ pub fn resolve_setup(app: &mut App) {
/// 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
@@ -238,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(())
}
+30 -29
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,40 +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 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?;
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?;
}
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(())
}
@@ -87,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: []
";
+4 -3
View File
@@ -2,7 +2,7 @@
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": {
"productName": "Clash Verge",
"version": "1.6.6"
"version": "1.7.2"
},
"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."
+6 -6
View File
@@ -10,16 +10,16 @@
"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"]
}
}
}
+19
View File
@@ -14,6 +14,25 @@
"exceptionDomain": "",
"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
}
}
}
}
+32
View File
@@ -441,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"
+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>
);
+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>
-38
View File
@@ -1,38 +0,0 @@
import dayjs from "dayjs";
import { useEffect } from "react";
import { getClashLogs } from "@/services/cmds";
import { useClashInfo } from "@/hooks/use-clash";
import { useEnableLog, useSetLogData } 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] = useEnableLog();
const setLogData = useSetLogData();
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>
+183 -127
View File
@@ -1,182 +1,238 @@
import { ReactNode, useEffect, useRef } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import {
Button,
ButtonGroup,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
} from "@mui/material";
import FormatPaintIcon from "@mui/icons-material/FormatPaint";
import OpenInFullIcon from "@mui/icons-material/OpenInFull";
import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen";
import { useThemeMode } from "@/services/states";
import { readProfileFile, saveProfileFile } from "@/services/cmds";
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 = 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} />
<DialogContent sx={{ width: "auto", height: "calc(100vh - 185px)" }}>
<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>
+175 -37
View File
@@ -17,12 +17,18 @@ import {
} from "@mui/material";
import { RefreshRounded, DragIndicator } from "@mui/icons-material";
import { useLoadingCache, useSetLoadingCache } from "@/services/states";
import { updateProfile, deleteProfile, viewProfile } from "@/services/cmds";
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); }
@@ -36,10 +42,13 @@ 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 });
@@ -49,10 +58,11 @@ export const ProfileItem = (props: Props) => {
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">
@@ -312,7 +394,7 @@ export const ProfileItem = (props: Props) => {
</Typography>
) : (
hasUrl && (
<Typography noWrap title={`From ${from}`}>
<Typography noWrap title={`${t("From")} ${from}`}>
{from}
</Typography>
)
@@ -323,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>
@@ -334,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
@@ -364,6 +450,7 @@ export const ProfileItem = (props: Props) => {
<MenuItem
key={item.label}
onClick={item.handler}
disabled={item.disabled}
sx={[
{
minWidth: 120,
@@ -385,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?.groups ?? "")}
language="yaml"
onSave={async (prev, curr) => {
await saveProfileFile(option?.groups ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setGroupsOpen(false)}
/>
<EditorViewer
open={mergeOpen}
initialData={readProfileFile(option?.merge ?? "")}
language="yaml"
schema="clash"
onSave={async (prev, curr) => {
await saveProfileFile(option?.merge ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setMergeOpen(false)}
/>
<EditorViewer
open={scriptOpen}
initialData={readProfileFile(option?.script ?? "")}
language="javascript"
onSave={async (prev, curr) => {
await saveProfileFile(option?.script ?? "", curr ?? "");
onSave && onSave(prev, curr);
}}
onClose={() => setScriptOpen(false)}
/>
<ConfirmViewer
title="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,678 @@
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";
import MonacoEditor from "react-monaco-editor";
import { useThemeMode } from "@/services/states";
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 themeMode = useThemeMode();
const [prevData, setPrevData] = useState("");
const [currData, setCurrData] = useState("");
const [visible, setVisible] = useState(true);
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);
setCurrData(data);
};
useEffect(() => {
if (currData === "") return;
if (visible !== true) return;
let obj = yaml.load(currData) as { prepend: []; append: []; delete: [] };
setPrependSeq(obj.prepend || []);
setAppendSeq(obj.append || []);
setDeleteSeq(obj.delete || []);
}, [visible]);
useEffect(() => {
if (prependSeq && appendSeq && deleteSeq)
setCurrData(
yaml.dump({ prepend: prependSeq, append: appendSeq, delete: deleteSeq })
);
}, [prependSeq, appendSeq, deleteSeq]);
const fetchProfile = async () => {
let data = await readProfileFile(profileUid);
let groupsObj = yaml.load(data) as { "proxy-groups": [] };
let rulesObj = yaml.load(data) as { rules: [] };
let ruleSetObj = yaml.load(data) as { "rule-providers": [] };
let subRuleObj = yaml.load(data) as { "sub-rules": [] };
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 {
await saveProfileFile(property, currData);
onSave?.(prevData, currData);
onClose();
} catch (err: any) {
Notice.error(err.message || err.toString());
}
});
return (
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>
{
<Box display="flex" justifyContent="space-between">
{t("Edit Rules")}
<Box>
<Button
variant="contained"
size="small"
onClick={() => {
setVisible((prev) => !prev);
}}
>
{visible ? t("Advanced") : t("Visible")}
</Button>
</Box>
</Box>
}
</DialogTitle>
<DialogContent
sx={{ display: "flex", width: "auto", height: "calc(100vh - 185px)" }}
>
{visible ? (
<>
<List
sx={{
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={{
width: "50%",
padding: "0 10px",
}}
>
<BaseSearchBox
matchCase={false}
onSearch={(match) => setMatch(() => match)}
/>
<Virtuoso
style={{ height: "calc(100% - 24px)", marginTop: "8px" }}
totalCount={
filteredRuleList.length +
(prependSeq.length > 0 ? 1 : 0) +
(appendSeq.length > 0 ? 1 : 0)
}
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>
</>
) : (
<MonacoEditor
height="100%"
language="yaml"
value={currData}
theme={themeMode === "light" ? "vs" : "vs-dark"}
options={{
tabSize: 2, // 根据语言类型设置缩进大小
minimap: {
enabled: document.documentElement.clientWidth >= 1500, // 超过一定宽度显示minimap滚动条
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
quickSuggestions: {
strings: true, // 字符串类型的建议
comments: true, // 注释类型的建议
other: true, // 其他类型的建议
},
padding: {
top: 33, // 顶部padding防止遮挡snippets
},
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: true, // 连字符
smoothScrolling: true, // 平滑滚动
}}
onChange={(value) => setCurrData(value)}
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={handleSave} variant="contained">
{t("Save")}
</Button>
</DialogActions>
</Dialog>
);
};
const Item = styled(ListItem)(() => ({
padding: "5px 2px",
}));
+4 -6
View File
@@ -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)}
>
+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>
+4 -6
View File
@@ -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)}
>
@@ -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();
@@ -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>
);
};
+35 -24
View File
@@ -1,8 +1,6 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Box, IconButton, Tooltip } from "@mui/material";
import { InfoRounded } from "@mui/icons-material";
import {
InputAdornment,
List,
@@ -20,6 +18,7 @@ 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;";
}`;
@@ -29,10 +28,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
let validReg;
if (getSystem() === "windows") {
validReg =
/^(\*?\w+(\.\w+)*|\d{1,3}(\.\d{1,3}){0,2}\.\*|\d{1,3}(\.\d{1,3}){3})(;(\*?\w+(\.\w+)*|\d{1,3}(\.\d{1,3}){0,2}\.\*|\d{1,3}(\.\d{1,3}){3}))*$/;
/^((\*\.)?([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}\.){3}\d{1,3}(\/\d{1,2})?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?)(,((\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}|(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(\/\d{1,3})?))*$/;
/^((\*\.)?([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);
@@ -50,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 ?? {};
@@ -58,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,
});
@@ -69,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,
});
@@ -98,6 +100,9 @@ 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;
}
@@ -166,26 +171,24 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem>
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Proxy Guard")} />
<Tooltip title={t("Proxy Guard Info")}>
<IconButton color="inherit" size="small">
<InfoRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
<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}
@@ -201,14 +204,25 @@ 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 && (
<>
<ListItemText primary={t("Proxy Bypass")} />
<TextField
autoComplete="off"
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
size="small"
autoComplete="off"
multiline
rows={4}
sx={{ width: "100%" }}
@@ -220,9 +234,9 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItemText primary={t("Bypass")} />
<FlexBox>
<TextField
autoComplete="off"
disabled={true}
size="small"
autoComplete="off"
multiline
rows={4}
sx={{ width: "100%" }}
@@ -248,21 +262,18 @@ 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>
</>
+4 -5
View File
@@ -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"
@@ -44,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) {
+1 -1
View File
@@ -43,12 +43,12 @@ export const WebUIItem = (props: Props) => {
<>
<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={t("Support %host, %port, %secret")}
autoComplete="off"
/>
<IconButton
size="small"
@@ -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,14 +98,13 @@ 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"
extra={
<Typography mt={2} sx={{ fontSize: "12px" }}>
{t("Replace host, port, secret with %host, %port, %secret")}
+25 -39
View File
@@ -1,14 +1,7 @@
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import {
TextField,
Select,
MenuItem,
Typography,
IconButton,
Tooltip,
} from "@mui/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";
@@ -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";
@@ -115,30 +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" } }}
@@ -159,23 +147,21 @@ const SettingClash = ({ onError }: Props) => {
<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 onClick={invoke_uwp_tool} label={t("Open UWP tool")} />
<SettingItem
onClick={invoke_uwp_tool}
label={t("Open UWP tool")}
extra={<TooltipIcon title={t("Open UWP tool Info")} />}
/>
)}
<SettingItem onClick={onUpdateGeo} label={t("Update GeoData")} />
+19 -49
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
@@ -124,24 +104,11 @@ const SettingSystem = ({ onError }: Props) => {
label={t("System Proxy")}
extra={
<>
<Tooltip title={t("System Proxy Info")} placement="top">
<IconButton color="inherit" size="small">
<InfoRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
</Tooltip>
<IconButton
color="inherit"
size="small"
<TooltipIcon
title={t("System Proxy Info")}
icon={Settings}
onClick={() => sysproxyRef.current?.open()}
>
<Settings
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
/>
</>
}
>
@@ -170,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"
-1
View File
@@ -1,5 +1,4 @@
import { useRef } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/api/dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material";
+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 };
};
+89 -19
View File
@@ -1,16 +1,20 @@
{
"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-Profiles": "Profiles",
"Label-Connections": "Connections",
@@ -18,7 +22,6 @@
"Label-Logs": "Logs",
"Label-Test": "Test",
"Label-Settings": "Settings",
"Proxies": "Proxies",
"Proxy Groups": "Proxy Groups",
"Proxy Provider": "Proxy Provider",
@@ -37,7 +40,6 @@
"Delay check to cancel fixed": "Delay check to cancel fixed",
"Proxy basic": "Proxy basic",
"Proxy detail": "Proxy detail",
"Profiles": "Profiles",
"Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config",
@@ -45,8 +47,67 @@
"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",
"Advanced": "Advanced",
"Visible": "Visible",
"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",
@@ -69,7 +130,6 @@
"Script Console": "Script Console",
"To Top": "To Top",
"To End": "To End",
"Connections": "Connections",
"Table View": "Table View",
"List View": "List View",
@@ -89,21 +149,17 @@
"Source": "Source",
"Destination IP": "Destination IP",
"Close Connection": "Close Connection",
"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",
"System Setting": "System Setting",
"Tun Mode": "Tun Mode",
@@ -118,6 +174,7 @@
"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",
@@ -140,6 +197,7 @@
"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",
@@ -147,13 +205,21 @@
"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",
"Allow Lan": "Allow LAN",
"IPv6": "IPv6",
"Log Level": "Log Level",
"Port Config": "Port Config",
"Random Port": "Random Port",
"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",
@@ -164,11 +230,13 @@
"Clash Core": "Clash Core",
"Upgrade": "Upgrade",
"Restart": "Restart",
"Release Version": "Release Version",
"Alpha Version": "Alpha Version",
"Tun mode requires": "Tun mode requires",
"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",
"Verge Setting": "Verge Setting",
"Language": "Language",
"Theme Mode": "Theme Mode",
@@ -195,8 +263,9 @@
"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",
"Menu Icon": "Menu Icon",
"Nav Icon": "Nav Icon",
"Monochrome": "Monochrome",
"Colorful": "Colorful",
"Tray Icon": "Tray Icon",
@@ -206,15 +275,17 @@
"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",
"Proxy Layout Column": "Proxy Layout Column",
"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 7 Days": "Retain 7 Days",
"Retain 30 Days": "Retain 30 Days",
"Retain 90 Days": "Retain 90 Days",
"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",
@@ -230,10 +301,10 @@
"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",
"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",
@@ -241,7 +312,6 @@
"Match Case": "Match Case",
"Match Whole Word": "Match Whole Word",
"Use Regular Expression": "Use Regular Expression",
"Profile Imported Successfully": "Profile Imported Successfully",
"Clash Config Updated": "Clash Config Updated",
"Profile Switched": "Profile Switched",
+86 -18
View File
@@ -1,16 +1,20 @@
{
"millis": "میلی‌ثانیه",
"mins": "دقیقه",
"Back": "بازگشت",
"Close": "بستن",
"Cancel": "لغو",
"Confirm": "تأیید",
"Maximize": "بزرگ‌نمایی",
"Minimize": "کوچک‌نمایی",
"Format document": "فرمت‌بندی سند",
"Empty": "خالی خالی",
"New": "جدید",
"Edit": "ویرایش",
"Save": "ذخیره",
"Delete": "حذف",
"Enable": "فعال کردن",
"Disable": "غیرفعال کردن",
"Label-Proxies": "پراکسی‌ها",
"Label-Profiles": "پروفایل‌ها",
"Label-Connections": "اتصالات",
@@ -18,7 +22,6 @@
"Label-Logs": "لاگ‌ها",
"Label-Test": "آزمون",
"Label-Settings": "تنظیمات",
"Proxies": "پراکسی‌ها",
"Proxy Groups": "گروه‌های پراکسی",
"Proxy Provider": "تأمین‌کننده پروکسی",
@@ -37,7 +40,6 @@
"Delay check to cancel fixed": "بررسی تأخیر برای لغو ثابت",
"Proxy basic": "پراکسی پایه",
"Proxy detail": "جزئیات پراکسی",
"Profiles": "پروفایل‌ها",
"Update All Profiles": "به‌روزرسانی همه پروفایل‌ها",
"View Runtime Config": "مشاهده پیکربندی زمان اجرا",
@@ -45,8 +47,65 @@
"Paste": "چسباندن",
"Profile URL": "آدرس پروفایل",
"Import": "وارد کردن",
"From": "از",
"Update Time": "زمان به‌روزرسانی",
"Used / Total": "استفاده‌شده / کل",
"Expire Time": "زمان انقضا",
"Create Profile": "ایجاد پروفایل",
"Edit Profile": "ویرایش پروفایل",
"Edit Proxies": "ویرایش پروکسی‌ها",
"Edit Rules": "ویرایش قوانین",
"Rule Type": "نوع قانون",
"Rule Content": "محتوای قانون",
"Proxy Policy": "سیاست پروکسی",
"No Resolve": "بدون حل",
"Prepend Rule": "اضافه کردن قانون به ابتدا",
"Append Rule": "اضافه کردن قانون به انتها",
"Delete Rule": "حذف قانون",
"Rule Condition Required": "شرط قانون الزامی است",
"Invalid Rule": "قانون نامعتبر",
"DOMAIN": "مطابقت با نام کامل دامنه",
"DOMAIN-SUFFIX": "مطابقت با پسوند دامنه",
"DOMAIN-KEYWORD": "مطابقت با کلمه کلیدی دامنه",
"DOMAIN-REGEX": "مطابقت با دامنه با استفاده از عبارات منظم",
"GEOSITE": "مطابقت با دامنه‌های درون Geosite",
"GEOIP": "مطابقت با کد کشور IP",
"SRC-GEOIP": "مطابقت با کد کشور IP مبدا",
"IP-ASN": "مطابقت با ASN آدرس IP",
"SRC-IP-ASN": "مطابقت با ASN آدرس IP مبدا",
"IP-CIDR": "مطابقت با محدوده آدرس IP",
"IP-CIDR6": "مطابقت با محدوده آدرس IPv6",
"SRC-IP-CIDR": "مطابقت با محدوده آدرس IP مبدا",
"IP-SUFFIX": "مطابقت با محدوده پسوند آدرس IP",
"SRC-IP-SUFFIX": "مطابقت با محدوده پسوند آدرس IP مبدا",
"SRC-PORT": "مطابقت با محدوده پورت مبدا",
"DST-PORT": "مطابقت با محدوده پورت مقصد",
"IN-PORT": "مطابقت با پورت ورودی",
"DSCP": "علامت‌گذاری DSCP (فقط برای tproxy UDP ورودی)",
"PROCESS-NAME": "مطابقت با نام فرآیند (نام بسته Android)",
"PROCESS-PATH": "مطابقت با مسیر کامل فرآیند",
"PROCESS-NAME-REGEX": "مطابقت با نام فرآیند با استفاده از عبارات منظم (نام بسته Android)",
"PROCESS-PATH-REGEX": "مطابقت با مسیر کامل فرآیند با استفاده از عبارات منظم",
"NETWORK": "مطابقت با پروتکل انتقال (tcp/udp)",
"UID": "مطابقت با شناسه کاربری Linux",
"IN-TYPE": "مطابقت با نوع ورودی",
"IN-USER": "مطابقت با نام کاربری ورودی",
"IN-NAME": "مطابقت با نام ورودی",
"SUB-RULE": "قانون فرعی",
"RULE-SET": "مطابقت با مجموعه قوانین",
"AND": "منطق AND",
"OR": "منطق OR",
"NOT": "منطق NOT",
"MATCH": "مطابقت با تمام درخواست‌ها",
"DIRECT": "داده‌ها به صورت مستقیم خروجی می‌شوند",
"REJECT": "درخواست‌ها را متوقف می‌کند",
"REJECT-DROP": "درخواست‌ها را نادیده می‌گیرد",
"PASS": "این قانون را در صورت تطابق نادیده می‌گیرد",
"Edit Groups": "ویرایش گروه‌های پروکسی",
"Extend Config": "توسعه پیکربندی",
"Extend Script": "ادغام اسکریپت",
"Global Merge": "تنظیمات گسترده‌ی سراسری",
"Global Script": "اسکریپت گسترش سراسری",
"Type": "نوع",
"Name": "نام",
"Descriptions": "توضیحات",
@@ -69,7 +128,6 @@
"Script Console": "کنسول اسکریپت",
"To Top": "به بالا",
"To End": "به پایان",
"Connections": "اتصالات",
"Table View": "نمای جدولی",
"List View": "نمای لیستی",
@@ -89,21 +147,17 @@
"Source": "منبع",
"Destination IP": "آدرس IP مقصد",
"Close Connection": "بستن اتصال",
"Rules": "قوانین",
"Rule Provider": "تأمین‌کننده قانون",
"Logs": "لاگ‌ها",
"Pause": "توقف",
"Clear": "پاک کردن",
"Test": "آزمون",
"Test All": "آزمون همه",
"Create Test": "ایجاد آزمون",
"Edit Test": "ویرایش آزمون",
"Icon": "آیکون",
"Test URL": "آدرس آزمون",
"Settings": "تنظیمات",
"System Setting": "تنظیمات سیستم",
"Tun Mode": "حالت Tun",
@@ -118,6 +172,7 @@
"DNS Hijack": "ربایش DNS",
"MTU": "واحد حداکثر انتقال",
"Service Mode": "حالت سرویس",
"Service Mode Info": "هسته راه اندازی شده توسط سرویس می تواند مجوزهای مربوطه را پس از نصب و مجوز سرویس دریافت کند",
"Current State": "وضعیت فعلی",
"pending": "در انتظار",
"installed": "نصب شده",
@@ -140,6 +195,7 @@
"Proxy Guard": "محافظ پراکسی",
"Proxy Guard Info": "امکان جلوگیری از نرم‌افزارهای دیگر از تغییر تنظیمات پروکسی سیستم عامل را فعال کنید",
"Guard Duration": "مدت محافظت",
"Always use Default Bypass": "همیشه از دور زدن پیش‌فرض استفاده کنید",
"Proxy Bypass": "دور زدن پراکسی: ",
"Bypass": "دور زدن: ",
"Use PAC Mode": "استفاده از حالت PAC",
@@ -147,13 +203,18 @@
"PAC URL": "PAC URL: ",
"Auto Launch": "راه‌اندازی خودکار",
"Silent Start": "شروع بی‌صدا",
"Silent Start Info": "برنامه را در حالت پس‌زمینه بدون نمایش پانل اجرا کنید",
"Clash Setting": "تنظیمات Clash",
"Allow Lan": "اجازه LAN",
"IPv6": "IPv6",
"Log Level": "سطح لاگ",
"Port Config": "پیکربندی پورت",
"Random Port": "پورت تصادفی",
"Mixed Port": "پورت پروکسی ترکیبی",
"Socks Port": "پورت پروکسی Socks",
"Http Port": "پورت پروکسی Http(s)",
"Redir Port": "پورت پروکسی شفاف Redir",
"Tproxy Port": "پورت پروکسی شفاف Tproxy",
"External": "خارجی",
"External Controller": "کنترل‌کننده خارجی",
"Core Secret": "رمز اصلی",
@@ -164,11 +225,16 @@
"Clash Core": "هسته Clash",
"Upgrade": "ارتقاء",
"Restart": "راه‌اندازی مجدد",
"Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا",
"Tun mode requires": "Tun mode نیاز دارد",
"Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",
"Update GeoData": "به‌روزرسانی GeoData",
"TG Channel": "کانال تلگرام",
"Manual": "راهنما",
"Github Repo": "مخزن GitHub",
"Verge Setting": "تنظیمات Verge",
"Language": "زبان",
"Theme Mode": "حالت تم",
@@ -195,8 +261,9 @@
"Layout Setting": "تنظیمات چیدمان",
"Traffic Graph": "نمودار ترافیک",
"Memory Usage": "استفاده از حافظه",
"Memory Cleanup": "برای پاکسازی حافظه ضربه بزنید",
"Proxy Group Icon": "آیکون گروه پراکسی",
"Menu Icon": "آیکون منو",
"Nav Icon": "آیکون ناوبری",
"Monochrome": "تک رنگ",
"Colorful": "رنگارنگ",
"Tray Icon": "آیکون سینی سیستم",
@@ -206,15 +273,17 @@
"Miscellaneous": "متفرقه",
"App Log Level": "سطح لاگ برنامه",
"Auto Close Connections": "بستن خودکار اتصالات",
"Auto Close Connections Info": "اتصالات برقرار شده را هنگام تغییر انتخاب گروه پروکسی یا حالت پروکسی خاتمه دهید",
"Auto Check Update": "بررسی خودکار به‌روزرسانی",
"Enable Builtin Enhanced": "فعال کردن تقویت داخلی",
"Proxy Layout Column": "ستون چیدمان پراکسی",
"Enable Builtin Enhanced Info": "مدیریت سازگاری برای فایل پیکربندی",
"Proxy Layout Columns": "ستون چیدمان پراکسی",
"Auto Columns": "ستون‌های خودکار",
"Auto Log Clean": "پاکسازی خودکار لاگ",
"Never Clean": "هرگز پاک نکن",
"Retain 7 Days": "نگهداری به مدت 7 روز",
"Retain 30 Days": "نگهداری به مدت 30 روز",
"Retain 90 Days": "نگهداری به مدت 90 روز",
"Retain _n Days": "نگهداری به مدت {{n}} روز",
"Default Latency Test": "آزمون تأخیر پیش‌فرض",
"Default Latency Test Info": "فقط برای تست درخواست‌های کلاینت HTTP استفاده می‌شود و بر فایل پیکربندی تأثیری نخواهد داشت",
"Default Latency Timeout": "زمان انتظار تأخیر پیش‌فرض",
"Hotkey Setting": "تنظیمات کلیدهای میانبر",
"open_or_close_dashboard": "باز/بستن داشبورد",
@@ -230,10 +299,10 @@
"Check for Updates": "بررسی برای به‌روزرسانی‌ها",
"Go to Release Page": "رفتن به صفحه انتشار",
"Portable Updater Error": "نسخه پرتابل از به‌روزرسانی درون برنامه‌ای پشتیبانی نمی‌کند. لطفاً به صورت دستی دانلود و جایگزین کنید",
"Break Change Update Error": "این نسخه یک به‌روزرسانی اساسی است و پشتیبانی از به‌روزرسانی درون برنامه را پشتیبانی نمی‌کند. لطفاً پس از حذف، دستی دانلود و نصب کنید.",
"Open Dev Tools": "باز کردن ابزارهای توسعه‌دهنده",
"Exit": "خروج",
"Verge Version": "نسخه Verge",
"ReadOnly": "فقط خواندنی",
"ReadOnlyMessage": "نمی‌توان در ویرایشگر فقط خواندنی ویرایش کرد",
"Filter": "فیلتر",
@@ -241,7 +310,6 @@
"Match Case": "تطبیق حروف کوچک و بزرگ",
"Match Whole Word": "تطبیق کل کلمه",
"Use Regular Expression": "استفاده از عبارت منظم",
"Profile Imported Successfully": "پروفایل با موفقیت وارد شد",
"Clash Config Updated": "پیکربندی Clash به‌روزرسانی شد",
"Profile Switched": "پروفایل تغییر یافت",
+86 -18
View File
@@ -1,16 +1,20 @@
{
"millis": "миллисекунды",
"mins": "минуты",
"Back": "Назад",
"Close": "Закрыть",
"Cancel": "Отмена",
"Confirm": "Подтвердить",
"Maximize": "Развернуть",
"Minimize": "Свернуть",
"Format document": "Форматировать документ",
"Empty": "Пусто",
"New": "Новый",
"Edit": "Редактировать",
"Save": "Сохранить",
"Delete": "Удалить",
"Enable": "Включить",
"Disable": "Отключить",
"Label-Proxies": "Прокси",
"Label-Profiles": "Профили",
"Label-Connections": "Соединения",
@@ -18,7 +22,6 @@
"Label-Logs": "Логи",
"Label-Test": "Тест",
"Label-Settings": "Настройки",
"Proxies": "Прокси",
"Proxy Groups": "Группы прокси",
"Proxy Provider": "Провайдер прокси",
@@ -37,7 +40,6 @@
"Delay check to cancel fixed": "Проверка задержки для отмены фиксированного",
"Proxy basic": "Резюме о прокси",
"Proxy detail": "Подробности о прокси",
"Profiles": "Профили",
"Update All Profiles": "Обновить все профили",
"View Runtime Config": "Просмотреть используемый конфиг",
@@ -45,8 +47,65 @@
"Paste": "Вставить",
"Profile URL": "URL профиля",
"Import": "Импорт",
"From": "От",
"Update Time": "Время обновления",
"Used / Total": "Использовано / Всего",
"Expire Time": "Время окончания",
"Create Profile": "Создать профиль",
"Edit Profile": "Изменить профиль",
"Edit Proxies": "Редактировать прокси",
"Edit Rules": "Редактировать правила",
"Rule Type": "Тип правила",
"Rule Content": "Содержимое правила",
"Proxy Policy": "Политика прокси",
"No Resolve": "Без разрешения",
"Prepend Rule": "Добавить правило в начало",
"Append Rule": "Добавить правило в конец",
"Delete Rule": "Удалить правило",
"Rule Condition Required": "Требуется условие правила",
"Invalid Rule": "Недействительное правило",
"DOMAIN": "Соответствует полному доменному имени",
"DOMAIN-SUFFIX": "Соответствует суффиксу домена",
"DOMAIN-KEYWORD": "Соответствует ключевому слову домена",
"DOMAIN-REGEX": "Соответствует домену с использованием регулярных выражений",
"GEOSITE": "Соответствует доменам в Geosite",
"GEOIP": "Соответствует коду страны IP-адреса",
"SRC-GEOIP": "Соответствует коду страны исходного IP-адреса",
"IP-ASN": "Соответствует ASN IP-адреса",
"SRC-IP-ASN": "Соответствует ASN исходного IP-адреса",
"IP-CIDR": "Соответствует диапазону IP-адресов",
"IP-CIDR6": "Соответствует диапазону IPv6-адресов",
"SRC-IP-CIDR": "Соответствует диапазону исходных IP-адресов",
"IP-SUFFIX": "Соответствует диапазону суффиксов IP-адресов",
"SRC-IP-SUFFIX": "Соответствует диапазону суффиксов исходных IP-адресов",
"SRC-PORT": "Соответствует диапазону исходных портов",
"DST-PORT": "Соответствует диапазону целевых портов",
"IN-PORT": "Соответствует входящему порту",
"DSCP": "Маркировка DSCP (только для tproxy UDP входящего)",
"PROCESS-NAME": "Соответствует имени процесса (имя пакета Android)",
"PROCESS-PATH": "Соответствует полному пути процесса",
"PROCESS-NAME-REGEX": "Соответствует имени процесса с использованием регулярных выражений (имя пакета Android)",
"PROCESS-PATH-REGEX": "Соответствует полному пути процесса с использованием регулярных выражений",
"NETWORK": "Соответствует транспортному протоколу (tcp/udp)",
"UID": "Соответствует USER ID в Linux",
"IN-TYPE": "Соответствует типу входящего соединения",
"IN-USER": "Соответствует имени пользователя входящего соединения",
"IN-NAME": "Соответствует имени входящего соединения",
"SUB-RULE": "Подправило",
"RULE-SET": "Соответствует набору правил",
"AND": "Логическое И",
"OR": "Логическое ИЛИ",
"NOT": "Логическое НЕ",
"MATCH": "Соответствует всем запросам",
"DIRECT": "Данные направляются напрямую наружу",
"REJECT": "Перехватывает запросы",
"REJECT-DROP": "Отклоняет запросы",
"PASS": "Пропускает это правило при совпадении",
"Edit Groups": "Редактировать группы прокси",
"Extend Config": "Изменить Merge.",
"Extend Script": "Изменить Script",
"Global Merge": "Глобальный расширенный Настройки",
"Global Script": "Глобальный расширенный скрипт",
"Type": "Тип",
"Name": "Название",
"Descriptions": "Описания",
@@ -69,7 +128,6 @@
"Script Console": "Консоль скрипта",
"To Top": "Наверх",
"To End": "Вниз",
"Connections": "Соединения",
"Table View": "Tablichnyy vid",
"List View": "Spiskovyy vid",
@@ -89,21 +147,17 @@
"Source": "Исходный адрес",
"Destination IP": "IP-адрес назначения",
"Close Connection": "Закрыть соединение",
"Rules": "Правила",
"Rule Provider": "Провайдер правило",
"Logs": "Логи",
"Pause": "Пауза",
"Clear": "Очистить",
"Test": "Тест",
"Test All": "Тест Все",
"Create Test": "Создать тест",
"Edit Test": "Редактировать тест",
"Icon": "Икона",
"Test URL": "Тестовый URL",
"Settings": "Настройки",
"System Setting": "Настройки системы",
"Tun Mode": "Режим туннеля",
@@ -118,6 +172,7 @@
"DNS Hijack": "DNS-перехват",
"MTU": "Максимальная единица передачи",
"Service Mode": "Режим сервиса",
"Service Mode Info": "Ядро, запущенное сервисом, может получить соответствующие разрешения после установки и авторизации сервиса",
"Current State": "Текущее состояние",
"pending": "Ожидающий",
"installed": "Установленный",
@@ -140,6 +195,7 @@
"Proxy Guard": "Защита прокси",
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим программным обеспечением",
"Guard Duration": "Период защиты",
"Always use Default Bypass": "Всегда использовать стандартное обходное решение",
"Proxy Bypass": "Игнорирование прокси: ",
"Bypass": "Игнорирование: ",
"Use PAC Mode": "Используйте режим PAC",
@@ -147,13 +203,21 @@
"PAC URL": "Адрес PAC: ",
"Auto Launch": "Автозапуск",
"Silent Start": "Тихий запуск",
"Silent Start Info": "Запускать программу в фоновом режиме без отображения панели",
"TG Channel": "Канал Telegram",
"Manual": "Документация",
"Github Repo": "GitHub репозиторий",
"Clash Setting": "Настройки Clash",
"Allow Lan": "Разрешить локальную сеть",
"IPv6": "IPv6",
"Log Level": "Уровень логов",
"Port Config": "Настройка порта",
"Random Port": "Случайный порт",
"Mixed Port": "Смешанный прокси-порт",
"Socks Port": "Порт Socks-прокси",
"Http Port": "Порт Http(s)-прокси",
"Redir Port": "Порт прозрачного прокси Redir",
"Tproxy Port": "Порт прозрачного прокси Tproxy",
"External": "Внешний",
"External Controller": "Адрес прослушивания внешнего контроллера",
"Core Secret": "Секрет",
@@ -164,11 +228,13 @@
"Clash Core": "Ядра Clash",
"Upgrade": "Обновлять",
"Restart": "Перезапуск",
"Release Version": "Официальная версия",
"Alpha Version": "Альфа-версия",
"Tun mode requires": "Требуется Режим туннеля",
"Grant": "Предоставить",
"Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",
"Update GeoData": "Обновление GeoData",
"Verge Setting": "Настройки Verge",
"Language": "Язык",
"Theme Mode": "Режим темы",
@@ -195,8 +261,9 @@
"Layout Setting": "Настройки раскладки",
"Traffic Graph": "График трафика",
"Memory Usage": "Использование памяти",
"Memory Cleanup": "Нажмите, чтобы очистить память",
"Proxy Group Icon": "Иконка Группы прокси",
"Menu Icon": "Иконка меню",
"Nav Icon": "Иконка навигации",
"Monochrome": "Монохромный",
"Colorful": "Полноцветный",
"Tray Icon": "Иконка лотка",
@@ -206,15 +273,17 @@
"Miscellaneous": "Настройки Прочие",
"App Log Level": "Уровень журнала приложения",
"Auto Close Connections": "Автоматическое закрытие соединений",
"Auto Close Connections Info": "Завершить установленные соединения при изменении выбора группы прокси или режима прокси",
"Auto Check Update": "Автоматическая проверка обновлений",
"Enable Builtin Enhanced": "Включить встроенные улучшения",
"Proxy Layout Column": "Количество столбцов в макете прокси",
"Enable Builtin Enhanced Info": "Обработка совместимости для файла конфигурации",
"Proxy Layout Columns": "Количество столбцов в макете прокси",
"Auto Columns": "Авто колонки",
"Auto Log Clean": "Автоматическая очистка журналов",
"Never Clean": "Никогда не очищать",
"Retain 7 Days": "Сохранять 7 дней",
"Retain 30 Days": "Сохранять 30 дней",
"Retain 90 Days": "Сохранять 90 дней",
"Retain _n Days": "Сохранять {{n}} дней",
"Default Latency Test": "Ссылка на тестирование задержки по умолчанию",
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
"Default Latency Timeout": "Таймаут задержки по умолчанию",
"Hotkey Setting": "Настройки клавиатурных сокращений",
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
@@ -230,10 +299,10 @@
"Check for Updates": "Проверить обновления",
"Go to Release Page": "Перейти на страницу релизов",
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную",
"Break Change Update Error": "Это крупное обновление, которое не поддерживает обновление внутри приложения. Пожалуйста, удалите его и загрузите установочный файл вручную.",
"Open Dev Tools": "Открыть инструменты разработчика",
"Exit": "Выход",
"Verge Version": "Версия Verge",
"ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Filter": "Фильтр",
@@ -241,7 +310,6 @@
"Match Case": "Учитывать регистр",
"Match Whole Word": "Полное совпадение слова",
"Use Regular Expression": "Использовать регулярные выражения",
"Profile Imported Successfully": "Профиль успешно импортирован",
"Clash Config Updated": "Clash конфигурация Обновлена",
"Profile Switched": "Профиль изменен",
+91 -21
View File
@@ -1,16 +1,20 @@
{
"millis": "毫秒",
"mins": "分钟",
"Back": "返回",
"Close": "关闭",
"Cancel": "取消",
"Confirm": "确认",
"Maximize": "最大化",
"Minimize": "最小化",
"Format document": "格式化文档",
"Empty": "空空如也",
"New": "新建",
"Edit": "编辑",
"Save": "保存",
"Delete": "删除",
"Enable": "启用",
"Disable": "禁用",
"Label-Proxies": "代 理",
"Label-Profiles": "订 阅",
"Label-Connections": "连 接",
@@ -18,7 +22,6 @@
"Label-Logs": "日 志",
"Label-Test": "测 试",
"Label-Settings": "设 置",
"Proxies": "代理",
"Proxy Groups": "代理组",
"Proxy Provider": "代理集合",
@@ -37,7 +40,6 @@
"Delay check to cancel fixed": "进行延迟测试,以取消固定",
"Proxy basic": "隐藏节点细节",
"Proxy detail": "展示节点细节",
"Profiles": "订阅",
"Update All Profiles": "更新所有订阅",
"View Runtime Config": "查看运行时订阅",
@@ -45,8 +47,67 @@
"Paste": "粘贴",
"Profile URL": "订阅文件链接",
"Import": "导入",
"From": "来自",
"Update Time": "更新时间",
"Used / Total": "已使用 / 总量",
"Expire Time": "到期时间",
"Create Profile": "新建配置",
"Edit Profile": "编辑配置",
"Edit Proxies": "编辑节点",
"Edit Rules": "编辑规则",
"Rule Type": "规则类型",
"Rule Content": "规则内容",
"Proxy Policy": "代理策略",
"No Resolve": "跳过DNS解析",
"Prepend Rule": "添加前置规则",
"Append Rule": "添加后置规则",
"Delete Rule": "删除规则",
"Rule Condition Required": "规则条件缺失",
"Invalid Rule": "无效规则",
"Advanced": "高级",
"Visible": "可视化",
"DOMAIN": "匹配完整域名",
"DOMAIN-SUFFIX": "匹配域名后缀",
"DOMAIN-KEYWORD": "匹配域名关键字",
"DOMAIN-REGEX": "匹配域名正则表达式",
"GEOSITE": "匹配Geosite内的域名",
"GEOIP": "匹配IP所属国家代码",
"SRC-GEOIP": "匹配来源IP所属国家代码",
"IP-ASN": "匹配IP所属ASN",
"SRC-IP-ASN": "匹配来源IP所属ASN",
"IP-CIDR": "匹配IP地址范围",
"IP-CIDR6": "匹配IP地址范围",
"SRC-IP-CIDR": "匹配来源IP地址范围",
"IP-SUFFIX": "匹配IP后缀范围",
"SRC-IP-SUFFIX": "匹配来源IP后缀范围",
"SRC-PORT": "匹配请求来源端口范围",
"DST-PORT": "匹配请求目标端口范围",
"IN-PORT": "匹配入站端口",
"DSCP": "DSCP标记(仅限tproxy udp入站)",
"PROCESS-NAME": "匹配进程名称(Android包名)",
"PROCESS-PATH": "匹配完整进程路径",
"PROCESS-NAME-REGEX": "正则匹配完整进程名称(Android包名)",
"PROCESS-PATH-REGEX": "正则匹配完整进程路径",
"NETWORK": "匹配传输协议(tcp/udp)",
"UID": "匹配Linux USER ID",
"IN-TYPE": "匹配入站类型",
"IN-USER": "匹配入站用户名",
"IN-NAME": "匹配入站名称",
"SUB-RULE": "子规则",
"RULE-SET": "匹配规则集",
"AND": "逻辑和",
"OR": "逻辑或",
"NOT": "逻辑非",
"MATCH": "匹配所有请求",
"DIRECT": "直连",
"REJECT": "拦截请求",
"REJECT-DROP": "抛弃请求",
"PASS": "跳过此规则",
"Edit Groups": "编辑代理组",
"Extend Config": "扩展配置",
"Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置",
"Global Script": "全局扩展脚本",
"Type": "类型",
"Name": "名称",
"Descriptions": "描述",
@@ -54,7 +115,7 @@
"Update Interval": "更新间隔",
"Choose File": "选择文件",
"Use System Proxy": "使用系统代理更新",
"Use Clash Proxy": "使用 Clash 代理更新",
"Use Clash Proxy": "使用内核代理更新",
"Accept Invalid Certs (Danger)": "允许无效证书 (危险)",
"Refresh": "刷新",
"Home": "首页",
@@ -69,7 +130,6 @@
"Script Console": "脚本控制台输出",
"To Top": "移到最前",
"To End": "移到末尾",
"Connections": "连接",
"Table View": "表格视图",
"List View": "列表视图",
@@ -89,21 +149,17 @@
"Source": "源地址",
"Destination IP": "目标地址",
"Close Connection": "关闭连接",
"Rules": "规则",
"Rule Provider": "规则集合",
"Logs": "日志",
"Pause": "暂停",
"Clear": "清除",
"Test": "测试",
"Test All": "测试全部",
"Create Test": "新建测试",
"Edit Test": "编辑测试",
"Icon": "图标",
"Test URL": "测试地址",
"Settings": "设置",
"System Setting": "系统设置",
"Tun Mode": "Tun 模式",
@@ -118,6 +174,7 @@
"DNS Hijack": "DNS 劫持",
"MTU": "最大传输单元",
"Service Mode": "服务模式",
"Service Mode Info": "安装并授权服务后,由该服务启动的内核进程可获得相关权限",
"Current State": "当前状态",
"pending": "等待中",
"installed": "已安装",
@@ -129,7 +186,7 @@
"Uninstall": "卸载",
"Disable Service Mode": "禁用服务模式",
"System Proxy": "系统代理",
"System Proxy Info": "打开此开关修改操作系统的代理设置,如果开启失败,可手动修改操作系统的代理设置",
"System Proxy Info": "修改操作系统的代理设置,如果开启失败,可手动修改操作系统的代理设置",
"System Proxy Setting": "系统代理设置",
"Current System Proxy": "当前系统代理",
"Enable status": "开启状态:",
@@ -140,6 +197,7 @@
"Proxy Guard": "系统代理守卫",
"Proxy Guard Info": "开启以防止其他软件修改操作系统的代理设置",
"Guard Duration": "代理守卫间隔",
"Always use Default Bypass": "始终使用默认绕过",
"Proxy Bypass": "代理绕过设置:",
"Bypass": "当前绕过:",
"Use PAC Mode": "使用PAC模式",
@@ -147,13 +205,21 @@
"PAC URL": "PAC地址:",
"Auto Launch": "开机自启",
"Silent Start": "静默启动",
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
"TG Channel": "Telegram 频道",
"Manual": "使用手册",
"Github Repo": "GitHub 项目地址",
"Clash Setting": "Clash 设置",
"Allow Lan": "局域网连接",
"IPv6": "IPv6",
"Log Level": "日志等级",
"Port Config": "端口设置",
"Random Port": "随机端口",
"Mixed Port": "混合代理端口",
"Socks Port": "Socks代理端口",
"Http Port": "Http(s)代理端口",
"Redir Port": "Redir透明代理端口",
"Tproxy Port": "Tproxy透明代理端口",
"External": "外部控制",
"External Controller": "外部控制器监听地址",
"Core Secret": "API 访问密钥",
@@ -164,11 +230,13 @@
"Clash Core": "Clash 内核",
"Upgrade": "升级内核",
"Restart": "重启内核",
"Release Version": "正式版",
"Alpha Version": "预览版",
"Tun mode requires": "如需启用 Tun 模式需要授权",
"Grant": "授权",
"Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
"Update GeoData": "更新 GeoData",
"Verge Setting": "Verge 设置",
"Language": "语言设置",
"Theme Mode": "主题模式",
@@ -194,9 +262,10 @@
"CSS Injection": "CSS 注入",
"Layout Setting": "界面设置",
"Traffic Graph": "流量图显",
"Memory Usage": "内存使用",
"Memory Usage": "内存占用",
"Memory Cleanup": "点击清理内存",
"Proxy Group Icon": "代理组图标",
"Menu Icon": "菜单图标",
"Nav Icon": "导航栏图标",
"Monochrome": "单色图标",
"Colorful": "彩色图标",
"Tray Icon": "托盘图标",
@@ -206,15 +275,17 @@
"Miscellaneous": "杂项设置",
"App Log Level": "App日志等级",
"Auto Close Connections": "自动关闭连接",
"Auto Close Connections Info": "当代理组选中节点或代理模式变动时,关闭已建立的连接",
"Auto Check Update": "自动检查更新",
"Enable Builtin Enhanced": "内置增强功能",
"Proxy Layout Column": "代理页布局列数",
"Enable Builtin Enhanced Info": "配置文件的兼容性处理",
"Proxy Layout Columns": "代理页布局列数",
"Auto Columns": "自动列数",
"Auto Log Clean": "自动清理日志",
"Never Clean": "不清理",
"Retain 7 Days": "保留7天",
"Retain 30 Days": "保留30天",
"Retain 90 Days": "保留90天",
"Retain _n Days": "保留{{n}}天",
"Default Latency Test": "默认测试链接",
"Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响",
"Default Latency Timeout": "测试超时时间",
"Hotkey Setting": "热键设置",
"open_or_close_dashboard": "打开/关闭面板",
@@ -230,10 +301,10 @@
"Check for Updates": "检查更新",
"Go to Release Page": "前往发布页",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
"Break Change Update Error": "此版本为重大更新,不支持应用内更新,请卸载后手动下载安装",
"Open Dev Tools": "打开开发者工具",
"Exit": "退出",
"Verge Version": "Verge 版本",
"ReadOnly": "只读",
"ReadOnlyMessage": "无法在只读模式下编辑",
"Filter": "过滤节点",
@@ -241,7 +312,6 @@
"Match Case": "区分大小写",
"Match Whole Word": "全字匹配",
"Use Regular Expression": "使用正则表达式",
"Profile Imported Successfully": "导入订阅成功",
"Clash Config Updated": "Clash 配置已更新",
"Profile Switched": "订阅已切换",
+8 -11
View File
@@ -16,7 +16,6 @@ import Layout from "./pages/_layout";
import "./services/i18n";
import {
LoadingCacheProvider,
LogDataProvider,
ThemeModeProvider,
UpdateStateProvider,
} from "./services/states";
@@ -32,20 +31,18 @@ if (!container) {
document.addEventListener("keydown", (event) => {
// Disable WebView keyboard shortcuts
if (["F5", "F7"].includes(event.key)) {
event.preventDefault();
}
if (
(event.ctrlKey || event.metaKey) &&
["F", "H", "P", "Q", "R", "U"].includes(event.key.toUpperCase())
) {
event.preventDefault();
}
const disabledShortcuts =
["F5", "F7"].includes(event.key) ||
(event.altKey && ["ArrowLeft", "ArrowRight"].includes(event.key)) ||
((event.ctrlKey || event.metaKey) &&
["F", "G", "H", "J", "P", "Q", "R", "U"].includes(
event.key.toUpperCase()
));
disabledShortcuts && event.preventDefault();
});
const contexts = [
<ThemeModeProvider />,
<LogDataProvider />,
<LoadingCacheProvider />,
<UpdateStateProvider />,
];
+67 -54
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { Virtuoso } from "react-virtuoso";
@@ -8,7 +8,6 @@ import { closeAllConnections } from "@/services/api";
import { useConnectionSetting } from "@/services/states";
import { useClashInfo } from "@/hooks/use-clash";
import { BaseEmpty, BasePage } from "@/components/base";
import { useWebsocket } from "@/hooks/use-websocket";
import { ConnectionItem } from "@/components/connection/connection-item";
import { ConnectionTable } from "@/components/connection/connection-table";
import {
@@ -19,8 +18,14 @@ import parseTraffic from "@/utils/parse-traffic";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseSearchBox } from "@/components/base/base-search-box";
import { BaseStyledSelect } from "@/components/base/base-styled-select";
import useSWRSubscription from "swr/subscription";
import { createSockette } from "@/utils/websocket";
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
const initConn: IConnections = {
uploadTotal: 0,
downloadTotal: 0,
connections: [],
};
type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
@@ -31,7 +36,6 @@ const ConnectionsPage = () => {
const isDark = theme.palette.mode === "dark";
const [match, setMatch] = useState(() => (_: string) => true);
const [curOrderOpt, setOrderOpt] = useState("Default");
const [connData, setConnData] = useState<IConnections>(initConn);
const [setting, setSetting] = useConnectionSetting();
@@ -49,6 +53,63 @@ const ConnectionsPage = () => {
list.sort((a, b) => b.curDownload! - a.curDownload!),
};
const { data: connData = initConn } = useSWRSubscription<
IConnections,
any,
"getClashConnections" | null
>(clashInfo ? "getClashConnections" : null, (_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
const s = createSockette(
`ws://${server}/connections?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
// meta v1.15.0 出现 data.connections 为 null 的情况
const data = JSON.parse(event.data) as IConnections;
// 尽量与前一次 connections 的展示顺序保持一致
next(null, (old = initConn) => {
const oldConn = old.connections;
const maxLen = data.connections?.length;
const connections: IConnectionsItem[] = [];
const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) {
const old = oldConn[index];
each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download;
connections[index] = each;
return false;
}
return true;
});
for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) {
connections[i] = rest.shift()!;
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
return { ...data, connections };
});
},
onerror(event) {
next(event);
},
},
3
);
return () => {
s.close();
};
});
const [filterConn, download, upload] = useMemo(() => {
const orderFunc = orderOpts[curOrderOpt];
let connections = connData.connections.filter((conn) =>
@@ -56,6 +117,7 @@ const ConnectionsPage = () => {
);
if (orderFunc) connections = orderFunc(connections);
let download = 0;
let upload = 0;
connections.forEach((x) => {
@@ -65,55 +127,6 @@ const ConnectionsPage = () => {
return [connections, download, upload];
}, [connData, match, curOrderOpt]);
const { connect, disconnect } = useWebsocket(
(event) => {
// meta v1.15.0 出现data.connections为null的情况
const data = JSON.parse(event.data) as IConnections;
// 尽量与前一次connections的展示顺序保持一致
setConnData((old) => {
const oldConn = old.connections;
const maxLen = data.connections?.length;
const connections: typeof oldConn = [];
const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) {
const old = oldConn[index];
each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download;
connections[index] = each;
return false;
}
return true;
});
for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) {
connections[i] = rest.shift()!;
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
return { ...data, connections };
});
},
{ errorCount: 3, retryInterval: 1000 }
);
useEffect(() => {
if (!clashInfo) return;
const { server = "", secret = "" } = clashInfo;
connect(`ws://${server}/connections?token=${encodeURIComponent(secret)}`);
return () => {
disconnect();
};
}, [clashInfo]);
const onCloseAll = useLockFn(closeAllConnections);
const detailRef = useRef<ConnectionDetailRef>(null!);
@@ -195,7 +208,7 @@ const ConnectionsPage = () => {
}}
>
{filterConn.length === 0 ? (
<BaseEmpty text="No Connections" />
<BaseEmpty />
) : isTableLayout ? (
<ConnectionTable
connections={filterConn}
+13 -10
View File
@@ -6,17 +6,18 @@ import {
PlayCircleOutlineRounded,
PauseCircleOutlineRounded,
} from "@mui/icons-material";
import { useEnableLog, useLogData, useSetLogData } from "@/services/states";
import { useLogData } from "@/hooks/use-log-data";
import { useEnableLog } from "@/services/states";
import { BaseEmpty, BasePage } from "@/components/base";
import LogItem from "@/components/log/log-item";
import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseSearchBox } from "@/components/base/base-search-box";
import { BaseStyledSelect } from "@/components/base/base-styled-select";
import { mutate } from "swr";
const LogPage = () => {
const { t } = useTranslation();
const logData = useLogData();
const setLogData = useSetLogData();
const { data: logData = [] } = useLogData();
const [enableLog, setEnableLog] = useEnableLog();
const { theme } = useCustomTheme();
const isDark = theme.palette.mode === "dark";
@@ -24,11 +25,11 @@ const LogPage = () => {
const [match, setMatch] = useState(() => (_: string) => true);
const filterLogs = useMemo(() => {
return logData
.filter((data) =>
logState === "all" ? true : data.type.includes(logState)
)
.filter((data) => match(data.payload));
return logData.filter(
(data) =>
(logState === "all" ? true : data.type.includes(logState)) &&
match(data.payload)
);
}, [logData, logState, match]);
return (
@@ -54,7 +55,9 @@ const LogPage = () => {
<Button
size="small"
variant="contained"
onClick={() => setLogData([])}
// useSWRSubscription adds a prefix "$sub$" to the cache key
// https://github.com/vercel/swr/blob/1585a3e37d90ad0df8097b099db38f1afb43c95d/src/subscription/index.ts#L37
onClick={() => mutate("$sub$getClashLog", [])}
>
{t("Clear")}
</Button>
@@ -99,7 +102,7 @@ const LogPage = () => {
followOutput={"smooth"}
/>
) : (
<BaseEmpty text="No Logs" />
<BaseEmpty />
)}
</Box>
</BasePage>
+64 -81
View File
@@ -41,8 +41,8 @@ import {
ProfileViewer,
ProfileViewerRef,
} from "@/components/profile/profile-viewer";
import { ProfileItem } from "@/components/profile/profile-item";
import { ProfileMore } from "@/components/profile/profile-more";
import { ProfileItem } from "@/components/profile/profile-item";
import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es";
@@ -56,7 +56,7 @@ const ProfilePage = () => {
const [url, setUrl] = useState("");
const [disabled, setDisabled] = useState(false);
const [activating, setActivating] = useState("");
const [activatings, setActivatings] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const sensors = useSensors(
useSensor(PointerSensor),
@@ -105,29 +105,24 @@ const ProfilePage = () => {
getRuntimeLogs
);
const chain = profiles.chain || [];
const viewerRef = useRef<ProfileViewerRef>(null);
const configRef = useRef<DialogRef>(null);
// distinguish type
const { regularItems, enhanceItems } = useMemo(() => {
const profileItems = useMemo(() => {
const items = profiles.items || [];
const chain = profiles.chain || [];
const type1 = ["local", "remote"];
const type2 = ["merge", "script"];
const regularItems = items.filter((i) => i && type1.includes(i.type!));
const restItems = items.filter((i) => i && type2.includes(i.type!));
const restMap = Object.fromEntries(restItems.map((i) => [i.uid, i]));
const enhanceItems = chain
.map((i) => restMap[i]!)
.filter(Boolean)
.concat(restItems.filter((i) => !chain.includes(i.uid)));
const profileItems = items.filter((i) => i && type1.includes(i.type!));
return { regularItems, enhanceItems };
return profileItems;
}, [profiles]);
const currentActivatings = () => {
return [...new Set([profiles.current ?? ""])].filter(Boolean);
};
const onImport = async () => {
if (!url) return;
setLoading(true);
@@ -138,13 +133,13 @@ const ProfilePage = () => {
setUrl("");
setLoading(false);
getProfiles().then((newProfiles) => {
getProfiles().then(async (newProfiles) => {
mutate("getProfiles", newProfiles);
const remoteItem = newProfiles.items?.find((e) => e.type === "remote");
if (!newProfiles.current && remoteItem) {
const current = remoteItem.uid;
patchProfiles({ current });
await patchProfiles({ current });
mutateLogs();
setTimeout(() => activateSelected(), 2000);
}
@@ -171,7 +166,9 @@ const ProfilePage = () => {
const onSelect = useLockFn(async (current: string, force: boolean) => {
if (!force && current === profiles.current) return;
// 避免大多数情况下loading态闪烁
const reset = setTimeout(() => setActivating(current), 100);
const reset = setTimeout(() => {
setActivatings([...currentActivatings(), current]);
}, 100);
try {
await patchProfiles({ current });
mutateLogs();
@@ -182,59 +179,38 @@ const ProfilePage = () => {
Notice.error(err?.message || err.toString(), 4000);
} finally {
clearTimeout(reset);
setActivating("");
setActivatings([]);
}
});
const onEnhance = useLockFn(async () => {
setActivatings(currentActivatings());
try {
await enhanceProfiles();
mutateLogs();
Notice.success(t("Profile Reactivated"), 1000);
} catch (err: any) {
Notice.error(err.message || err.toString(), 3000);
} finally {
setActivatings([]);
}
});
const onEnable = useLockFn(async (uid: string) => {
if (chain.includes(uid)) return;
const newChain = [...chain, uid];
await patchProfiles({ chain: newChain });
mutateLogs();
});
const onDisable = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = chain.filter((i) => i !== uid);
await patchProfiles({ chain: newChain });
mutateLogs();
});
const onDelete = useLockFn(async (uid: string) => {
const current = profiles.current === uid;
try {
await onDisable(uid);
setActivatings([...(current ? currentActivatings() : []), uid]);
await deleteProfile(uid);
mutateProfiles();
mutateLogs();
current && (await onEnhance());
} catch (err: any) {
Notice.error(err?.message || err.toString());
} finally {
setActivatings([]);
}
});
const onMoveTop = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = [uid].concat(chain.filter((i) => i !== uid));
await patchProfiles({ chain: newChain });
mutateLogs();
});
const onMoveEnd = useLockFn(async (uid: string) => {
if (!chain.includes(uid)) return;
const newChain = chain.filter((i) => i !== uid).concat([uid]);
await patchProfiles({ chain: newChain });
mutateLogs();
});
// 更新所有订阅
const setLoadingCache = useSetLoadingCache();
const onUpdateAll = useLockFn(async () => {
@@ -253,7 +229,7 @@ const ProfilePage = () => {
return new Promise((resolve) => {
setLoadingCache((cache) => {
// 获取没有正在更新的订阅
const items = regularItems.filter(
const items = profileItems.filter(
(e) => e.type === "remote" && !cache[e.uid]
);
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
@@ -268,6 +244,7 @@ const ProfilePage = () => {
const text = await readText();
if (text) setUrl(text);
};
const mode = useThemeMode();
const islight = mode === "light" ? true : false;
const dividercolor = islight
@@ -387,19 +364,25 @@ const ProfilePage = () => {
<Box sx={{ mb: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<SortableContext
items={regularItems.map((x) => {
items={profileItems.map((x) => {
return x.uid;
})}
>
{regularItems.map((item) => (
{profileItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<ProfileItem
id={item.uid}
selected={profiles.current === item.uid}
activating={activating === item.uid}
activating={activatings.includes(item.uid)}
itemData={item}
onSelect={(f) => onSelect(item.uid, f)}
onEdit={() => viewerRef.current?.edit(item)}
onSave={async (prev, curr) => {
if (prev !== curr && profiles.current === item.uid) {
await onEnhance();
}
}}
onDelete={() => onDelete(item.uid)}
/>
</Grid>
))}
@@ -407,38 +390,38 @@ const ProfilePage = () => {
</Grid>
</Box>
</DndContext>
{enhanceItems.length > 0 && (
<Divider
variant="middle"
flexItem
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
)}
{enhanceItems.length > 0 && (
<Box sx={{ mt: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
{enhanceItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<ProfileMore
selected={!!chain.includes(item.uid)}
itemData={item}
enableNum={chain.length || 0}
logInfo={chainLogs[item.uid]}
onEnable={() => onEnable(item.uid)}
onDisable={() => onDisable(item.uid)}
onDelete={() => onDelete(item.uid)}
onMoveTop={() => onMoveTop(item.uid)}
onMoveEnd={() => onMoveEnd(item.uid)}
onEdit={() => viewerRef.current?.edit(item)}
/>
</Grid>
))}
<Divider
variant="middle"
flexItem
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
<Box sx={{ mt: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid item xs={12} sm={6} md={6} lg={6}>
<ProfileMore
id="Merge"
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance();
}
}}
/>
</Grid>
</Box>
)}
<Grid item xs={12} sm={6} md={6} lg={6}>
<ProfileMore
id="Script"
logInfo={chainLogs["Script"]}
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance();
}
}}
/>
</Grid>
</Grid>
</Box>
</Box>
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
<ConfigViewer ref={configRef} />
</BasePage>
+3 -8
View File
@@ -1,8 +1,8 @@
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { useEffect } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Box, Button, ButtonGroup, Paper } from "@mui/material";
import { Box, Button, ButtonGroup } from "@mui/material";
import {
closeAllConnections,
getClashConfig,
@@ -24,12 +24,7 @@ const ProxyPage = () => {
const { verge } = useVerge();
const modeList = useMemo(() => {
if (verge?.clash_core?.includes("clash-meta")) {
return ["rule", "global", "direct"];
}
return ["rule", "global", "direct", "script"];
}, [verge?.clash_core]);
const modeList = ["rule", "global", "direct"];
const curMode = clashConfig?.mode?.toLowerCase();
+1 -1
View File
@@ -62,7 +62,7 @@ const RulesPage = () => {
followOutput={"smooth"}
/>
) : (
<BaseEmpty text="No Rules" />
<BaseEmpty />
)}
</Box>
</BasePage>
+17 -4
View File
@@ -2,7 +2,7 @@ import { Box, ButtonGroup, Grid, IconButton } from "@mui/material";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineSharp } from "@mui/icons-material";
import { GitHub, HelpOutlineSharp, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge";
import SettingClash from "@/components/setting/setting-clash";
@@ -21,7 +21,11 @@ const SettingPage = () => {
});
const toGithubDoc = useLockFn(() => {
return openWebUrl("https://clash-verge-rev.github.io/guide/log.html");
return openWebUrl("https://clash-verge-rev.github.io/index.html");
});
const toTelegramChannel = useLockFn(() => {
return openWebUrl("https://t.me/clash_verge_re");
});
const mode = useThemeMode();
@@ -35,7 +39,7 @@ const SettingPage = () => {
<IconButton
size="medium"
color="inherit"
title="@clash-verge-rev/clash-verge-rev.github.io"
title={t("Manual")}
onClick={toGithubDoc}
>
<HelpOutlineSharp fontSize="inherit" />
@@ -43,7 +47,16 @@ const SettingPage = () => {
<IconButton
size="medium"
color="inherit"
title="@clash-verge-rev/clash-verge-rev"
title={t("TG Channel")}
onClick={toTelegramChannel}
>
<Telegram fontSize="inherit" />
</IconButton>
<IconButton
size="medium"
color="inherit"
title={t("Github Repo")}
onClick={toGithubRepo}
>
<GitHub fontSize="inherit" />
+48 -15
View File
@@ -134,25 +134,37 @@ export const getProxies = async () => {
const { GLOBAL: global, DIRECT: direct, REJECT: reject } = proxyRecord;
let groups = Object.values(proxyRecord)
.filter((each) => each.name !== "GLOBAL" && each.all)
.map((each) => ({
...each,
all: each.all!.map((item) => generateItem(item)),
}));
if (global?.all) {
let globalGroups = global.all
.filter((name) => proxyRecord[name]?.all)
.map((name) => proxyRecord[name])
.map((each) => ({
let groups: IProxyGroupItem[] = Object.values(proxyRecord).reduce<
IProxyGroupItem[]
>((acc, each) => {
if (each.name !== "GLOBAL" && each.all) {
acc.push({
...each,
all: each.all!.map((item) => generateItem(item)),
}));
let globalNames = globalGroups.map((each) => each.name);
});
}
return acc;
}, []);
if (global?.all) {
let globalGroups: IProxyGroupItem[] = global.all.reduce<IProxyGroupItem[]>(
(acc, name) => {
if (proxyRecord[name]?.all) {
acc.push({
...proxyRecord[name],
all: proxyRecord[name].all!.map((item) => generateItem(item)),
});
}
return acc;
},
[]
);
let globalNames = new Set(globalGroups.map((each) => each.name));
groups = groups
.filter((group) => {
return !globalNames.includes(group.name);
return !globalNames.has(group.name);
})
.concat(globalGroups);
}
@@ -259,3 +271,24 @@ export const getGroupProxyDelays = async (
);
return result as any as Record<string, number>;
};
// Is debug enabled
export const isDebugEnabled = async () => {
try {
const instance = await getAxios();
await instance.get("/debug/pprof");
return true;
} catch {
return false;
}
};
// GC
export const gc = async () => {
try {
const instance = await getAxios();
await instance.put("/debug/gc");
} catch (error) {
console.error(`Error gcing: ${error}`);
}
};
+15 -20
View File
@@ -7,23 +7,22 @@ export async function getClashLogs() {
const newRegex = /(.+?)\s+(.+?)\s+(.+)/;
const logs = await invoke<string[]>("get_clash_logs");
return logs
.map((log) => {
const result = log.match(regex);
if (result) {
const [_, _time, type, payload] = result;
const time = dayjs(_time).format("MM-DD HH:mm:ss");
return { time, type, payload };
}
return logs.reduce<ILogItem[]>((acc, log) => {
const result = log.match(regex);
if (result) {
const [_, _time, type, payload] = result;
const time = dayjs(_time).format("MM-DD HH:mm:ss");
acc.push({ time, type, payload });
return acc;
}
const result2 = log.match(newRegex);
if (result2) {
const [_, time, type, payload] = result2;
return { time, type, payload };
}
return null;
})
.filter(Boolean) as ILogItem[];
const result2 = log.match(newRegex);
if (result2) {
const [_, time, type, payload] = result2;
acc.push({ time, type, payload });
}
return acc;
}, []);
}
export async function getProfiles() {
@@ -142,10 +141,6 @@ export async function restartSidecar() {
return invoke<void>("restart_sidecar");
}
export async function grantPermission(core: string) {
return invoke<void>("grant_permission", { core });
}
export async function getAppDir() {
return invoke<string>("get_app_dir");
}
-7
View File
@@ -5,10 +5,6 @@ const [ThemeModeProvider, useThemeMode, useSetThemeMode] = createContextState<
"light" | "dark"
>("light");
const [LogDataProvider, useLogData, useSetLogData] = createContextState<
ILogItem[]
>([]);
export const useEnableLog = () => useLocalStorage("enable-log", true);
interface IConnectionSetting {
@@ -39,9 +35,6 @@ export {
ThemeModeProvider,
useThemeMode,
useSetThemeMode,
LogDataProvider,
useLogData,
useSetLogData,
LoadingCacheProvider,
useLoadingCache,
useSetLoadingCache,

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