Compare commits

...
99 Commits
Author SHA1 Message Date
MystiPanda 3271c96531 fix: enhance when change tun config 2024-05-04 16:12:10 +08:00
dongchengjie 05fa6b9aba chore: profile template typo 2024-05-04 14:24:11 +08:00
MystiPanda 7edd7f27cb fix: try to set env 2024-05-03 18:00:55 +08:00
dongchengjie 4a6b12eda1 fix: #974 2024-05-03 12:50:04 +08:00
MystiPanda a354e1a0eb chore: update locale 2024-05-02 23:44:09 +08:00
dongchengjie c2c419522a chore: add service mode locale 2024-05-02 22:16:05 +08:00
MystiPanda 7a3cc7d242 feat: Support drag and drop local files 2024-05-02 20:41:43 +08:00
MystiPanda bd6f02f6af fix: change script execution path 2024-05-01 11:39:09 +08:00
MystiPanda bd33728fd7 Release 1.6.1 2024-04-30 23:43:51 +08:00
MystiPanda 8400ee8a3d fix: MacOS tray click 2024-04-30 23:21:25 +08:00
lxk955 80ee7aef8e feat: Support for using regular expressions on the log page #707 (#959) 2024-04-30 22:59:42 +08:00
MystiPanda 41762be3a5 fix: service install path 2024-04-30 20:20:39 +08:00
MystiPanda 605dda7b76 build: Restore core version 2024-04-30 09:42:25 +08:00
dongchengjie 8d6e3b5a58 fixup: wrong version 2024-04-30 02:13:24 +08:00
dongchengjie 53920fc368 chore: update pnpm-lock.yaml (#952)
* chore: update schema version

* Update pnpm-lock.yaml
2024-04-30 01:44:05 +08:00
dongchengjie 96d10423d9 chore: update schema version (#950) 2024-04-29 22:46:29 +08:00
MystiPanda 42be9b5b51 chore: Config issue template 2024-04-29 15:43:43 +08:00
MystiPanda 0add041516 docs: Update FAQ URL 2024-04-28 20:04:03 +08:00
MystiPanda dee76cac8d build: cargo update 2024-04-28 19:59:46 +08:00
MystiPanda 9646fab22c fix: Open the link with browser 2024-04-28 18:46:49 +08:00
dongchengjie a54e9cb244 fixup: can't edit file (#943) 2024-04-28 17:00:33 +08:00
dongchengjie f8339c7a9a chore: disable WebView keyboard shortcuts (#942) 2024-04-28 16:19:13 +08:00
lxk955 9e2c864117 feat: support display current profile when the mouse is placed on the tray icon #782 (#938) 2024-04-27 20:54:02 +08:00
PlayerNeo e1ccd71455 fix: disable left click menu on macOS (#930) 2024-04-27 20:52:29 +08:00
LiuTianYuandtyliu9 0e82426ea1 fix: #730 icon not change when the window is opened maximized (#924)
Co-authored-by: tyliu9 <tyliu9@toycloud.com>
2024-04-24 22:36:15 +08:00
wonfen 6c7afd168a Release 1.6.0 2024-04-23 14:25:09 +08:00
MystiPanda e014fdf3da feat: support ico format for tray icon (#911) 2024-04-22 20:43:15 +08:00
dongchengjie 2074da05c8 fix: #907 (#908) 2024-04-22 01:40:25 +08:00
dongchengjie 84cd87b70a chore: update locale (#904)
* chore: missing locale

* chore: External Controller locale
2024-04-21 14:10:34 +08:00
dongchengjie 75f87044f6 chore: Proxy Bypass placeholder (#901) 2024-04-20 19:27:17 +08:00
dongchengjie f71b51e64a fix: minor glitches (#900)
* feat: show actual proxy name instead of proxy group when hovering on a group outbound

* fix: open empty edit form and save will cause `UID not found`

* chore: tauri.conf.json json schema

* chore: missing locales
2024-04-20 18:02:15 +08:00
MystiPanda 9d741cdd63 fix: startup script blocking 2024-04-20 17:52:54 +08:00
MystiPanda 56be17000a fix: default value for tun 2024-04-20 17:42:36 +08:00
MystiPanda f6aacbc31d build: add depends 2024-04-19 15:55:46 +08:00
MystiPanda ad4b57c327 chore: update lock file 2024-04-19 15:38:32 +08:00
dongchengjie 3794b2f0de fix: 使用npm安装meta-json-schema (#895)
* feat: allow manual selection of url-test group

* feat: fixed proxy indicator

* fix: try to fix traffic websocket no longer updating

* fixup: group delay test use defined url

* feat: connections sorted by start by default

* feat: Connection details show the full path of the process

* fix: editor no hints and add yaml support

* feat: quick suggestions

* chore: use monaco-editor

* chore: update schema url

* chore: change default merge config content

* fix: load schema via npm

* feat: runtime config viewer style auto adjust

* feat: adjust fixed proxy style

* fix: headState "showType" won't toggle hover text

* chore: switch version

* chore: Update pnpm lockfile
2024-04-19 13:54:16 +08:00
MystiPanda e0e8412728 refactor: change default value of mtu 2024-04-18 10:27:09 +08:00
dongchengjie 05b8175aad chore: 修改Merge配置文件默认内容 (#889) 2024-04-17 22:56:54 +08:00
MystiPanda 339c89dd1c chore: Update pnpm lockfile 2024-04-17 22:16:50 +08:00
dongchengjie 402299e9c8 feat: Clash配置、Merge配置提供JSON Schema语法支持、[连接]界面调整 (#887) 2024-04-17 21:19:37 +08:00
MystiPanda d0e8f450fd fix: SymbolicLink
#750
2024-04-13 15:46:11 +08:00
MystiPanda e97b1ccd7a fix: service install path 2024-04-13 15:12:41 +08:00
汐殇 68691b91ef fix: Allow core files in specific directories to be upgraded normally (#857) 2024-04-12 01:16:57 +08:00
dongchengjie 213d417481 feat: url-test支持手动选择、节点组fixed节点使用角标展示 (#840)
* feat: allow manual selection of url-test group

* feat: fixed proxy indicator

* fix: try to fix traffic websocket no longer updating

* fixup: group delay test use defined url
2024-04-09 13:15:45 +08:00
MystiPanda 2ec841eb61 fix: Update the home while updating profile 2024-04-04 15:07:55 +08:00
MystiPanda 60b5c54be1 fix: Use System Browser 2024-04-04 14:56:10 +08:00
Damian Johnson 448412a191 style: adjust confirm dialog & web ui settings dialog (#821) 2024-04-03 23:19:00 +08:00
Damian Johnson e06327936e fix: service mode install script download not found (#822) 2024-04-03 23:18:10 +08:00
dongchengjie 5f044e1aed feat: support URL Schema 'profile-web-page-url' (#816) 2024-04-01 19:28:28 +08:00
HZ is not Chatty d90fb6fc9b fixup! feat: Service Mode for Linux (#804) (#815)
* fixup! feat: Service Mode for Linux (#804)

* fixup! feat: Service Mode for Linux (#804)

* Partially revert "fixup! feat: Service Mode for Linux (#804)"

This reverts commit e6a5a2b496.
2024-04-01 19:25:58 +08:00
MystiPanda 1c583d2ea9 feat: Try support service mode for MacOS 2024-03-31 23:16:47 +08:00
MystiPanda ddab131102 fix: script error 2024-03-31 16:37:33 +08:00
HZ is not Chatty 3b06cf8a2a feat: Service Mode for Linux (#804) 2024-03-31 16:16:23 +08:00
wonfen c5d7c29f3d Sytle: fix mac logo padding 2024-03-30 16:31:17 +08:00
Damian Johnson 26af860eac fix: icon not change when toggle window maxinized (#799) 2024-03-30 01:16:40 +08:00
cismous ec9852eb98 Style filter input (#724)
* refactor: reduce duplicate code

* style: add a white background to the light color theme to avoid the gray text being too light
2024-03-30 01:14:03 +08:00
Damian Johnson f2b8c6d0ca fix: missing proxy group delay check animations (#788)
* fix: missing proxy group delay check animation

* chore: cleanup

* chore: adjust content style
2024-03-29 08:20:31 +08:00
wonfen 2cf7a048cf Release 1.5.11 2024-03-28 12:06:41 +08:00
MystiPanda 3dbb71a076 feat: Support Persian
#715
2024-03-21 20:51:33 +08:00
MystiPanda ddf6760543 fix: Duplicate icon display error
#719
2024-03-21 20:29:16 +08:00
MystiPanda a6fa323f33 fix: The update button cannot be clicked
#716
2024-03-21 19:56:30 +08:00
MystiPanda 2df8f1acdb fix: Check if the install directory is empty when uninstall 2024-03-21 15:24:12 +08:00
MystiPanda 74a3290abc chore: fix typo 2024-03-21 14:09:59 +08:00
MystiPanda ab306d21f3 Release 1.5.10 2024-03-21 13:47:39 +08:00
MystiPanda 1bd7795851 chore: Change a little 2024-03-21 12:55:22 +08:00
MystiPanda 302c142346 build: Update depends 2024-03-21 11:53:25 +08:00
MystiPanda 7c254c9b45 fix: a little 2024-03-21 11:43:16 +08:00
MystiPanda b1c458359d feat: Confirm before deletion
#703
2024-03-21 11:39:01 +08:00
MystiPanda 2d925c62f5 feat: Support config redir port and tproxy port 2024-03-21 10:54:56 +08:00
MystiPanda ef4711987f chore: limit port config
#699
2024-03-20 21:23:10 +08:00
MystiPanda 8910dfaecf fix: Limit icon width
#697
2024-03-20 20:38:45 +08:00
MystiPanda e8b2e79c7d fix: service logs are not cleared
#695
2024-03-20 20:31:00 +08:00
MystiPanda 333ee1e1f7 docs: Improve the issue template 2024-03-20 18:06:02 +08:00
MystiPanda ba013bbe96 feat: Optimize Linux tray menu 2024-03-18 11:11:18 +08:00
MystiPanda 636e66fe7a Revert "fix: remove activation policy" 2024-03-18 09:53:48 +08:00
MystiPanda 372a45e667 Release 1.5.9 2024-03-17 11:25:25 +08:00
MystiPanda ab6b796ce2 fix: drag error
#643
2024-03-16 20:37:39 +08:00
MystiPanda 71e6c02897 fix: drag error 2024-03-16 17:06:59 +08:00
MystiPanda 3edeaa7038 fix: Try to fix touch drag
#456
2024-03-16 10:54:19 +08:00
MystiPanda f2f75d4015 build: Update to Tauri 1.6.1 2024-03-16 00:03:34 +08:00
MystiPanda 69adade738 ci: update alpha script 2024-03-15 21:52:56 +08:00
MystiPanda 1bd88828e4 ci: remove 2024-03-15 21:42:55 +08:00
MystiPanda 977fcbe6cd feat: Try to support more architecture 2024-03-15 21:14:05 +08:00
MystiPanda aa76a5c436 refactor: Try to migrate to boa_engine
#634
2024-03-15 19:58:22 +08:00
MystiPanda ea5fad428d fix: Avoid empty user-agent 2024-03-15 18:27:24 +08:00
MystiPanda 0e1e27b35a feat: Try to cache remote images
#603
2024-03-15 16:43:39 +08:00
screw-hand d41f67a156 fix: mac setting theme font-famil not working (#632) 2024-03-15 15:17:05 +08:00
MystiPanda 6f10dec808 fix: remove activation policy
#592
2024-03-15 12:44:25 +08:00
xkww3n 9b79da2cdf fix: Set REJECT-DROP policy the same text color as REJECT (#622) 2024-03-14 18:54:26 +08:00
MystiPanda c11b8519f1 Release 1.5.8 2024-03-13 14:17:06 +08:00
wonfen b9e23a0b59 Sytle: A little tweak 2024-03-13 14:09:08 +08:00
MystiPanda 75d41b6fe5 feat: Add border-radius for window on linux 2024-03-13 13:58:29 +08:00
Amnesiash 4256590735 update profile ui (#594) 2024-03-13 10:53:39 +08:00
MystiPanda 462fb05ea8 fix: Try to fix #577 again 2024-03-11 22:52:29 +08:00
MystiPanda 812dbfd836 fix: script 2024-03-11 22:10:20 +08:00
MystiPanda cfca837777 fix: typo 2024-03-11 22:05:56 +08:00
MystiPanda ac5fb1948a fix: Try to fix #577 2024-03-11 22:03:45 +08:00
MystiPanda 22d8b73625 feat: Allow open devtools 2024-03-11 20:19:21 +08:00
MystiPanda edde40c298 docs: Update Readme 2024-03-11 18:38:58 +08:00
98 changed files with 7038 additions and 3518 deletions
+33 -22
View File
@@ -1,23 +1,41 @@
name: Bug report name: 问题反馈 / Bug report
description: Create a report to help us improve title: "[BUG] "
title: "[BUG]" description: 反馈你遇到的问题 / Report the issue you are experiencing
labels: ["bug"] labels: ["bug"]
body: body:
- type: textarea - type: markdown
attributes: attributes:
label: Describe the bug value: |
description: A clear and concise description of what the bug is. ## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide.html) 以及 [常见问题](https://clash-verge-rev.github.io/faq/install/)
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保问题依然存在
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide.html) and [FAQ](https://clash-verge-rev.github.io/faq/install/)
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the problem still exists
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
- type: textarea
id: description
attributes:
label: 问题描述 / Describe the bug
description: 详细清晰地描述你遇到的问题 / A clear and concise description of what the bug is
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: To Reproduce label: 复现步骤 / To Reproduce
description: Steps to reproduce the behavior. description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations: validations:
required: true required: true
- type: dropdown - type: dropdown
attributes: attributes:
label: Platform label: 操作系统 / OS
options: options:
- Windows - Windows
- Linux - Linux
@@ -26,20 +44,13 @@ body:
required: true required: true
- type: input - type: input
attributes: attributes:
label: System Version label: 操作系统版本 / OS Version
placeholder: "e.g. macOS 10.15.7" description: 请提供你的操作系统版本,Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
validations:
required: true
- type: input
attributes:
label: Software Version
placeholder: "e.g. 1.4.3"
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Log label: 日志 / Log
description: "Log file content or screenshot" description: 请提供完整或相关部分的Debug日志 / Please provide the complete or relevant part of the Debug log
- type: textarea validations:
attributes: required: true
label: Additional Information
+4
View File
@@ -0,0 +1,4 @@
contact_links:
- name: 讨论交流 / Communication
url: https://t.me/clash_verge_rev
about: 在 Telegram 群组中与其他用户讨论交流 / Communicate with other users in the Telegram group
+26 -18
View File
@@ -1,27 +1,35 @@
name: Feature request name: 功能请求 / Feature request
description: Suggest an idea for this project title: "[Feature] "
title: "[Feature]" description: 提出你的功能请求 / Propose your feature request
labels: ["enhancement"] labels: ["enhancement"]
body: body:
- type: textarea - type: markdown
attributes: attributes:
label: Is your feature request related to a problem? Please describe. value: |
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] ## 在提交问题之前,请确认以下事项:
1. 请 **确保** 您已经查阅了 [Clash Verge Rev 官方文档](https://clash-verge-rev.github.io/guide.html) 确认软件不存在类似的功能
2. 请 **确保** [已有的问题](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue) 中没有人提交过相似issue,否则请在已有的issue下进行讨论
3. 请 **务必** 给issue填写一个简洁明了的标题,以便他人快速检索
4. 请 **务必** 先下载 [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) 版本测试,确保该功能还未实现
5. 请 **务必** 按照模板规范详细描述问题,否则issue将会被关闭
## Before submitting the issue, please make sure of the following checklist:
1. Please make sure you have read the [Clash Verge Rev official documentation](https://clash-verge-rev.github.io/guide.html) to confirm that the software does not have similar functions
2. Please make sure there is no similar issue in the [existing issues](https://github.com/clash-verge-rev/clash-verge-rev/issues?q=is%3Aissue), otherwise please discuss under the existing issue
3. Please be sure to fill in a concise and clear title for the issue so that others can quickly search
4. Please be sure to download the [Alpha](https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/alpha) version for testing to ensure that the function has not been implemented
5. Please describe the problem in detail according to the template specification, otherwise the issue will be closed
- type: textarea
id: description
attributes:
label: 功能描述 / Feature description
description: 详细清晰地描述你的功能请求 / A clear and concise description of what the feature is
validations: validations:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Describe the solution you'd like label: 使用场景 / Use case
description: A clear and concise description of what you want to happen. description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
validations: validations:
required: true required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
+12 -2
View File
@@ -14,7 +14,7 @@ elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
dpkg --add-architecture arm64 dpkg --add-architecture arm64
apt-get update apt-get update
apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64 apt-get install -y libncurses6:arm64 libtinfo6:arm64 linux-libc-dev:arm64 libncursesw6:arm64 libssl3:arm64 libcups2:arm64
apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libssl-dev:arm64 libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64 apt-get install -y --no-install-recommends g++-aarch64-linux-gnu libc6-dev-arm64-cross libwebkit2gtk-4.0-dev:arm64 libgtk-3-dev:arm64 patchelf:arm64 librsvg2-dev:arm64 libayatana-appindicator3-dev:arm64
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
@@ -24,12 +24,22 @@ elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
dpkg --add-architecture armhf dpkg --add-architecture armhf
apt-get update apt-get update
apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf apt-get install -y libncurses6:armhf libtinfo6:armhf linux-libc-dev:armhf libncursesw6:armhf libssl3:armhf libcups2:armhf
apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libssl-dev:armhf libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf apt-get install -y --no-install-recommends g++-arm-linux-gnueabihf libc6-dev-armhf-cross libwebkit2gtk-4.0-dev:armhf libgtk-3-dev:armhf patchelf:armhf librsvg2-dev:armhf libayatana-appindicator3-dev:armhf
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
export CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc export CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
export CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++ export CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1 export PKG_CONFIG_ALLOW_CROSS=1
elif [ "$INPUT_TARGET" = "riscv64gc-unknown-linux-gnu" ]; then
dpkg --add-architecture riscv64
apt-get update
apt-get install -y libncurses6:riscv64 libtinfo6:riscv64 linux-libc-dev:riscv64 libncursesw6:riscv64 libssl3:riscv64 libcups2:riscv64
apt-get install -y --no-install-recommends g++-riscv64-linux-gnu libc6-dev-riscv64-cross libwebkit2gtk-4.0-dev:riscv64 libgtk-3-dev:riscv64 patchelf:riscv64 librsvg2-dev:riscv64 libayatana-appindicator3-dev:riscv64
export CARGO_TARGET_RISCV64_UNKNOWN_LINUX_GNU_LINKER=riscv64-linux-gnu-gcc
export CC_riscv64_unknown_linux_gnu=riscv64-linux-gnu-gcc
export CXX_riscv64_unknown_linux_gnu=riscv64-linux-gnu-g++
export PKG_CONFIG_PATH=/usr/lib/riscv64-linux-gnu/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
else else
echo "Unknown target: $INPUT_TARGET" && exit 1 echo "Unknown target: $INPUT_TARGET" && exit 1
fi fi
+52 -16
View File
@@ -30,17 +30,6 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Apply Patch
if: matrix.target == 'aarch64-pc-windows-msvc'
run: |
git config --global user.email "clash-verge-rev@github.io"
git config --global user.name "clash-verge-rev"
git am patches/support-windows-aarch64.patch
- name: Init Submodule
if: matrix.target == 'aarch64-pc-windows-msvc'
run: git submodule update --init --recursive
- name: Install Rust Stable - name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -57,10 +46,10 @@ jobs:
with: with:
node-version: "20" node-version: "20"
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9
run_install: false run_install: false
- name: Pnpm install and check - name: Pnpm install and check
@@ -100,6 +89,8 @@ jobs:
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
- os: ubuntu-latest - os: ubuntu-latest
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@@ -112,30 +103,56 @@ jobs:
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
- name: Get Version - name: Get Version
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install jq sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- run: |
cat > release.txt << 'EOF'
### 我应该下载哪个版本?
- Windows x86_64架构: x64-setup.exe (不支持win7)
- Windows arm64架构: arm64-setup.exe
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg (提示文件损坏看下面FAQ)
- Linux x64架构: amd64.AppImage/amd64.deb
- Linux arm64架构: arm64.deb
- Linux armv7架构: armhf.deb
- Windows 便携板 x86_64架构: x64_portable.zip (不推荐使用,无法自动更新)
- Windows 便携板 arm64架构: arm64_portable.zip (不推荐使用,无法自动更新)
### FAQ
- [FAQ](https://clash-verge-rev.github.io/faq/install/)
Created at ${{ env.BUILDTIME }}.
EOF
- name: Upload Release - name: Upload Release
if: startsWith(matrix.target, 'x86_64') if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
tag_name: alpha tag_name: alpha
name: "Clash Verge Rev Alpha" name: "Clash Verge Rev Alpha"
body: "More new features are now supported." body_path: release.txt
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage* files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
tag_name: alpha tag_name: alpha
name: "Clash Verge Rev Alpha" name: "Clash Verge Rev Alpha"
body: "More new features are now supported." body_path: release.txt
prerelease: true prerelease: true
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
update_tag: update_tag:
name: Update tag name: Update tag
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -143,21 +160,40 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set Env - name: Set Env
run: | run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash shell: bash
- name: Update Tag - name: Update Tag
uses: richardsimko/update-tag@v1 uses: richardsimko/update-tag@v1
with: with:
tag_name: alpha tag_name: alpha
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: | - run: |
cat > release.txt << 'EOF' cat > release.txt << 'EOF'
## Clash Verge Rev Alpha ### 我应该下载哪个版本?
- Windows x86_64架构: x64-setup.exe (不支持win7)
- Windows arm64架构: arm64-setup.exe
- MacOS intel芯片: x64.dmg
- MacOS apple M芯片: aarch64.dmg (提示文件损坏看下面FAQ)
- Linux x64架构: amd64.AppImage/amd64.deb
- Linux arm64架构: arm64.deb
- Linux armv7架构: armhf.deb
- Windows 便携板 x86_64架构: x64_portable.zip (不推荐使用,无法自动更新)
- Windows 便携板 arm64架构: arm64_portable.zip (不推荐使用,无法自动更新)
### FAQ
- [FAQ](https://clash-verge-rev.github.io/faq/install/)
Created at ${{ env.BUILDTIME }}. Created at ${{ env.BUILDTIME }}.
EOF EOF
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
+7 -13
View File
@@ -27,17 +27,6 @@ jobs:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Apply Patch
if: matrix.target == 'aarch64-pc-windows-msvc'
run: |
git config --global user.email "clash-verge-rev@github.io"
git config --global user.name "clash-verge-rev"
git am patches/support-windows-aarch64.patch
- name: Init Submodule
if: matrix.target == 'aarch64-pc-windows-msvc'
run: git submodule update --init --recursive
- name: Install Rust Stable - name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
@@ -54,10 +43,10 @@ jobs:
with: with:
node-version: "20" node-version: "20"
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v3
name: Install pnpm name: Install pnpm
with: with:
version: 8 version: 9
run_install: false run_install: false
- name: Pnpm install and check - name: Pnpm install and check
@@ -97,6 +86,8 @@ jobs:
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
- os: ubuntu-latest - os: ubuntu-latest
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
@@ -109,11 +100,13 @@ jobs:
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
- name: Get Version - name: Get Version
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install jq sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Upload Release - name: Upload Release
if: startsWith(matrix.target, 'x86_64') if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
@@ -123,6 +116,7 @@ jobs:
body: "More new features are now supported." body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage* files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release - name: Upload Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
+2
View File
@@ -1,4 +1,5 @@
node_modules node_modules
.pnpm-store
.DS_Store .DS_Store
dist dist
dist-ssr dist-ssr
@@ -6,3 +7,4 @@ dist-ssr
update.json update.json
scripts/_env.sh scripts/_env.sh
.vscode .vscode
.tool-versions
+1
View File
@@ -0,0 +1 @@
nodejs 21.7.1
+5 -5
View File
@@ -9,6 +9,10 @@
A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>. A Clash Meta GUI based on <a href="https://github.com/tauri-apps/tauri">Tauri</a>.
</h3> </h3>
## Preview
![preview](./docs/preview.png)
## Install ## Install
请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br> 请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
@@ -43,13 +47,9 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core. - Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
- System proxy setting and guard. - System proxy setting and guard.
## Preview
![preview](./docs/preview.gif)
### FAQ ### FAQ
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq.html) Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/install/)
## Development ## Development
+116
View File
@@ -1,3 +1,119 @@
## v1.6.1
### Features
- 鼠标悬浮显示当前订阅的名称 [#938](https://github.com/clash-verge-rev/clash-verge-rev/pull/938)
- 日志过滤支持正则表达式 [#959](https://github.com/clash-verge-rev/clash-verge-rev/pull/959)
- 更新 Clash 内核到 1.18.4
### Bugs Fixes
- 修复 Linux KDE 环境下系统代理无法开启的问题
- 窗口最大化图标调整 [#924](https://github.com/clash-verge-rev/clash-verge-rev/pull/924)
- 修改 MacOS 托盘点击行为(左键菜单,右键点击事件)
- 修复 MacOS 服务模式安装失败的问题
---
## v1.6.0
### Features
- Meta(mihomo)内核回退 1.18.1(当前新版内核 hy2 协议有 bug,等修复后更新)
- 多处界面细节调整 [#724](https://github.com/clash-verge-rev/clash-verge-rev/pull/724) [#799](https://github.com/clash-verge-rev/clash-verge-rev/pull/799) [#900](https://github.com/clash-verge-rev/clash-verge-rev/pull/900) [#901](https://github.com/clash-verge-rev/clash-verge-rev/pull/901)
- Linux 下新增服务模式
- 新增订阅卡片右键可以打开机场首页
- url-test 支持手动选择、节点组 fixed 节点使用角标展示 [#840](https://github.com/clash-verge-rev/clash-verge-rev/pull/840)
- Clash 配置、Merge 配置提供 JSON Schema 语法支持、连接界面调整 [#887](https://github.com/clash-verge-rev/clash-verge-rev/pull/887)
- 修改 Merge 配置文件默认内容 [#889](https://github.com/clash-verge-rev/clash-verge-rev/pull/889)
- 修改 tun 模式默认 mtu 为 1500,老版本升级,需在 tun 模式设置下“重置为默认值”。
- 使用 npm 安装 meta-json-schema [#895](https://github.com/clash-verge-rev/clash-verge-rev/pull/895)
- 更新部分翻译 [#904](https://github.com/clash-verge-rev/clash-verge-rev/pull/904)
- 支持 ico 格式的任务栏图标
### Bugs Fixes
- 修复 Linux KDE 环境下系统代理无法开启的问题
- 修复延迟检测动画问题
- 窗口最大化图标调整 [#816](https://github.com/clash-verge-rev/clash-verge-rev/pull/816)
- 修复 Windows 某些情况下无法安装服务模式 [#822](https://github.com/clash-verge-rev/clash-verge-rev/pull/822)
- UI 细节修复 [#821](https://github.com/clash-verge-rev/clash-verge-rev/pull/821)
- 修复使用默认编辑器打开配置文件
- 修复内核文件在特定目录也可以更新的问题 [#857](https://github.com/clash-verge-rev/clash-verge-rev/pull/857)
- 修复服务模式的安装目录问题
- 修复删除配置文件的“更新间隔”出现的问题 [#907](https://github.com/clash-verge-rev/clash-verge-rev/issues/907)
### 已知问题(历史遗留问题,暂未找到有效解决方案)
- MacOS M 芯片下服务模式无法安装;临时解决方案:在内核 ⚙️ 下,手动授权,再打开 tun 模式。
- MacOS 下如果删除过网络配置,会导致无法正常打开系统代理;临时解决方案:使用浏览器代理插件或手动配置系统代理。
- Window 拨号连接下无法正确识别并打开系统代理;临时解决方案:使用浏览器代理插件或使用 tun 模式。
---
## v1.5.11
### Features
- Meta(mihomo)内核更新 1.18.2
### Bugs Fixes
- 升级图标无法点击的问题
- 卸载时检查安装目录是否为空
- 代理界面图标重合的问题
---
## v1.5.10
### Features
- 优化 Linux 托盘菜单显示
- 添加透明代理端口设置
- 删除订阅前确认
### Bugs Fixes
- 删除 MacOS 程序坞图标
- Windows 下 service 日志没有清理
- MacOS 无法开启系统代理
---
## v1.5.9
### Features
- 缓存代理组图标
- 使用`boa_engine` 代替 `rquickjs`
- 支持 Linux armv7
### Bugs Fixes
- Windows 首次安装无法点击
- Windows 触摸屏无法拖动
- 规则列表 `REJECT-DROP` 颜色
- MacOS Dock 栏不显示图标
- MacOS 自定义字体无效
- 避免使用空 UA 拉取订阅
---
## v1.5.8
### Features
- 优化 UI 细节
- Linux 绘制窗口圆角
- 开放 DevTools
### Bugs Fixes
- 修复 MacOS 下开启 Tun 内核崩溃的问题
---
## v1.5.7 ## v1.5.7
### Features ### Features
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

+29 -26
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "1.5.7", "version": "1.6.1",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "tauri dev", "dev": "tauri dev",
@@ -19,45 +19,48 @@
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.3", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.5",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.15.5", "@mui/icons-material": "^5.15.15",
"@mui/lab": "5.0.0-alpha.149", "@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.15.5", "@mui/material": "^5.15.15",
"@mui/x-data-grid": "^6.18.7", "@mui/x-data-grid": "^6.19.11",
"@tauri-apps/api": "^1.5.3", "@tauri-apps/api": "^1.5.4",
"ahooks": "^3.7.8", "@types/json-schema": "^7.0.15",
"axios": "^1.6.5", "ahooks": "^3.7.11",
"axios": "^1.6.8",
"dayjs": "1.11.5", "dayjs": "1.11.5",
"i18next": "^23.7.16", "i18next": "^23.11.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1", "meta-json-schema": "1.18.4-beta2",
"nanoid": "^5.0.4", "monaco-editor": "^0.47.0",
"react": "^18.2.0", "monaco-yaml": "^5.1.1",
"react-dom": "^18.2.0", "nanoid": "^5.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.51.3",
"react-i18next": "^13.5.0", "react-i18next": "^13.5.0",
"react-router-dom": "^6.21.2", "react-markdown": "^9.0.1",
"react-router-dom": "^6.23.0",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-virtuoso": "^4.6.2", "react-virtuoso": "^4.7.10",
"recoil": "^0.7.7", "recoil": "^0.7.7",
"snarkdown": "^2.0.0",
"swr": "^1.3.0", "swr": "^1.3.0",
"tar": "^6.2.0" "tar": "^6.2.1"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^5.1.1", "@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.5.9", "@tauri-apps/cli": "^1.5.12",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.48", "@types/react": "^18.3.1",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.3.0",
"@types/react-transition-group": "^4.4.10", "@types/react-transition-group": "^4.4.10",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.12",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1", "https-proxy-agent": "^5.0.1",
@@ -65,9 +68,9 @@
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"pretty-quick": "^3.3.1", "pretty-quick": "^3.3.1",
"sass": "^1.70.0", "sass": "^1.75.0",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"vite": "^5.0.11", "vite": "^5.2.10",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0" "vite-plugin-svgr": "^4.2.0"
}, },
-195
View File
@@ -1,195 +0,0 @@
From 871c9a6d1ed014c93da2436a437df03734e9f76c Mon Sep 17 00:00:00 2001
From: MystiPanda <mystipanda@proton.me>
Date: Sun, 10 Dec 2023 19:47:45 +0800
Subject: [PATCH] feat: Support windows aarch64
---
.gitmodules | 3 +
src-tauri/Cargo.toml | 2 +-
src-tauri/quick-rs | 1 +
src-tauri/src/enhance/script.rs | 130 +++++++++++++++++++-------------
4 files changed, 81 insertions(+), 55 deletions(-)
create mode 100644 .gitmodules
create mode 160000 src-tauri/quick-rs
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2eda7e4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src-tauri/quick-rs"]
+ path = src-tauri/quick-rs
+ url = https://github.com/clash-verge-rev/quick-rs.git
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 2f1a3be..d67f6ed 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -25,7 +25,6 @@ log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.30"
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
once_cell = "1.18"
@@ -33,6 +32,7 @@ port_scanner = "0.1.5"
delay_timer = "0.11.5"
parking_lot = "0.12"
percent-encoding = "2.3.1"
+quick-rs = { path = "quick-rs" }
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
diff --git a/src-tauri/quick-rs b/src-tauri/quick-rs
new file mode 160000
index 0000000..78277c4
--- /dev/null
+++ b/src-tauri/quick-rs
@@ -0,0 +1 @@
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
index 30a922f..d47dc33 100644
--- a/src-tauri/src/enhance/script.rs
+++ b/src-tauri/src/enhance/script.rs
@@ -3,61 +3,83 @@ use anyhow::Result;
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
- use rquickjs::{function::Func, Context, Runtime};
- use std::sync::{Arc, Mutex};
-
- let runtime = Runtime::new().unwrap();
- let context = Context::full(&runtime).unwrap();
- let outputs = Arc::new(Mutex::new(vec![]));
-
- let copy_outputs = outputs.clone();
- let result = context.with(|ctx| -> Result<Mapping> {
- ctx.globals().set(
- "__verge_log__",
- Func::from(move |level: String, data: String| {
- let mut out = copy_outputs.lock().unwrap();
- out.push((level, data));
- }),
- )?;
-
- ctx.eval(
- r#"var console = Object.freeze({
- log(data){__verge_log__("log",JSON.stringify(data))},
- info(data){__verge_log__("info",JSON.stringify(data))},
- error(data){__verge_log__("error",JSON.stringify(data))},
- debug(data){__verge_log__("debug",JSON.stringify(data))},
- });"#,
- )?;
-
- let config = use_lowercase(config.clone());
- let config_str = serde_json::to_string(&config)?;
-
- let code = format!(
- r#"try{{
+ use quick_rs::{context::Context, function::Function, module::Module, runtime::Runtime};
+
+ let config = use_lowercase(config.clone());
+ let config_str = serde_json::to_string(&config)?;
+
+ let runtime = Runtime::new();
+ let context = Context::from(&runtime);
+
+ let code = format!(
+ r#"
+ let output = [];
+
+ function __verge_log__(type, data) {{
+ output.push([type, data]);
+ }}
+
+ var console = Object.freeze({{
+ log(data) {{ __verge_log__("log", JSON.stringify(data)) }},
+ info(data) {{ __verge_log__("info", JSON.stringify(data)) }},
+ error(data) {{ __verge_log__("error", JSON.stringify(data)) }},
+ debug(data) {{ __verge_log__("debug", JSON.stringify(data)) }},
+ }});
+
{script};
- JSON.stringify(main({config_str})||'')
- }} catch(err) {{
- `__error_flag__ ${{err.toString()}}`
- }}"#
- );
- let result: String = ctx.eval(code.as_str())?;
- if result.starts_with("__error_flag__") {
- anyhow::bail!(result[15..].to_owned());
- }
- if result == "\"\"" {
- anyhow::bail!("main function should return object");
- }
- Ok(serde_json::from_str::<Mapping>(result.as_str())?)
- });
-
- let mut out = outputs.lock().unwrap();
- match result {
- Ok(config) => Ok((use_lowercase(config), out.to_vec())),
- Err(err) => {
- out.push(("exception".into(), err.to_string()));
- Ok((config, out.to_vec()))
- }
- }
+
+ export function _main(){{
+ try{{
+ let result = JSON.stringify(main({config_str})||"");
+ return JSON.stringify({{result, output}});
+ }} catch(err) {{
+ output.push(["exception", err.toString()]);
+ return JSON.stringify({{result: "__error__", output}});
+ }}
+ }}
+ "#
+ );
+ let value = context.eval_module(&code, "_main")?;
+ let module = Module::new(value)?;
+ let value = module.get("_main")?;
+ let function = Function::new(value)?;
+ let value = function.call(vec![])?;
+ let result = serde_json::from_str::<serde_json::Value>(&value.to_string()?)?;
+ result
+ .as_object()
+ .map(|obj| {
+ let result = obj.get("result").unwrap().as_str().unwrap();
+ let output = obj.get("output").unwrap();
+
+ let mut out = output
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|item| {
+ let item = item.as_array().unwrap();
+ (
+ item[0].as_str().unwrap().into(),
+ item[1].as_str().unwrap().into(),
+ )
+ })
+ .collect::<Vec<_>>();
+ if result.is_empty() {
+ anyhow::bail!("main function should return object");
+ }
+ if result == "__error__" {
+ return Ok((config, out.to_vec()));
+ }
+ let result = serde_json::from_str::<Mapping>(result);
+
+ match result {
+ Ok(config) => Ok((use_lowercase(config), out.to_vec())),
+ Err(err) => {
+ out.push(("exception".into(), err.to_string()));
+ Ok((config, out.to_vec()))
+ }
+ }
+ })
+ .unwrap_or_else(|| anyhow::bail!("Unknown result"))
}
#[test]
--
2.43.0.windows.1
+3340 -1890
View File
File diff suppressed because it is too large Load Diff
+63 -13
View File
@@ -21,6 +21,7 @@ const PLATFORM_MAP = {
"i686-unknown-linux-gnu": "linux", "i686-unknown-linux-gnu": "linux",
"aarch64-unknown-linux-gnu": "linux", "aarch64-unknown-linux-gnu": "linux",
"armv7-unknown-linux-gnueabihf": "linux", "armv7-unknown-linux-gnueabihf": "linux",
"riscv64gc-unknown-linux-gnu": "linux",
"loongarch64-unknown-linux-gnu": "linux", "loongarch64-unknown-linux-gnu": "linux",
}; };
const ARCH_MAP = { const ARCH_MAP = {
@@ -33,6 +34,7 @@ const ARCH_MAP = {
"i686-unknown-linux-gnu": "ia32", "i686-unknown-linux-gnu": "ia32",
"aarch64-unknown-linux-gnu": "arm64", "aarch64-unknown-linux-gnu": "arm64",
"armv7-unknown-linux-gnueabihf": "arm", "armv7-unknown-linux-gnueabihf": "arm",
"riscv64gc-unknown-linux-gnu": "riscv64",
"loongarch64-unknown-linux-gnu": "loong64", "loongarch64-unknown-linux-gnu": "loong64",
}; };
@@ -65,6 +67,7 @@ const META_ALPHA_MAP = {
"linux-ia32": "mihomo-linux-386", "linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64", "linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7", "linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64", "linux-loong64": "mihomo-linux-loong64",
}; };
@@ -111,6 +114,7 @@ const META_MAP = {
"linux-ia32": "mihomo-linux-386", "linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64", "linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7", "linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64", "linux-loong64": "mihomo-linux-loong64",
}; };
@@ -349,26 +353,62 @@ const resolvePlugin = async () => {
} }
}; };
// service chmod
const resolveServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service",
"install-service",
"uninstall-service",
];
const resDir = path.join(cwd, "src-tauri/resources");
for (let f of serviceExecutables) {
const targetPath = path.join(resDir, f);
if (await fs.pathExists(targetPath)) {
execSync(`chmod 755 ${targetPath}`);
console.log(`[INFO]: "${targetPath}" chmod finished`);
}
}
};
/** /**
* main * main
*/ */
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`; const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({ resolveResource({
file: "clash-verge-service.exe", file: "clash-verge-service" + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`, downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
}); });
const resolveInstall = () => };
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({ resolveResource({
file: "install-service.exe", file: "install-service" + ext,
downloadURL: `${SERVICE_URL}/install-service.exe`, downloadURL: `${SERVICE_URL}/install-service${ext}`,
}); });
const resolveUninstall = () => };
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({ resolveResource({
file: "uninstall-service.exe", file: "uninstall-service" + ext,
downloadURL: `${SERVICE_URL}/uninstall-service.exe`, downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
});
};
const resolveSetDnsScript = () =>
resolveResource({
file: "set_dns.sh",
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/set_dns.sh`,
});
const resolveUnSetDnsScript = () =>
resolveResource({
file: "unset_dns.sh",
downloadURL: `https://github.com/clash-verge-rev/set-dns-script/releases/download/script/unset_dns.sh`,
}); });
const resolveMmdb = () => const resolveMmdb = () =>
resolveResource({ resolveResource({
@@ -406,9 +446,11 @@ const tasks = [
retry: 5, retry: 5,
}, },
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true }, { name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true }, { name: "service", func: resolveService, retry: 5 },
{ name: "install", func: resolveInstall, retry: 5, winOnly: true }, { name: "install", func: resolveInstall, retry: 5 },
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true }, { name: "uninstall", func: resolveUninstall, retry: 5 },
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 }, { name: "mmdb", func: resolveMmdb, retry: 5 },
{ name: "geosite", func: resolveGeosite, retry: 5 }, { name: "geosite", func: resolveGeosite, retry: 5 },
{ name: "geoip", func: resolveGeoIP, retry: 5 }, { name: "geoip", func: resolveGeoIP, retry: 5 },
@@ -418,12 +460,20 @@ const tasks = [
retry: 5, retry: 5,
winOnly: true, winOnly: true,
}, },
{
name: "service_chmod",
func: resolveServicePermission,
retry: 1,
unixOnly: true,
},
]; ];
async function runTask() { async function runTask() {
const task = tasks.shift(); const task = tasks.shift();
if (!task) return; if (!task) return;
if (task.winOnly && process.platform !== "win32") return runTask(); if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
for (let i = 0; i < task.retry; i++) { for (let i = 0; i < task.retry; i++) {
try { try {
+1697 -738
View File
File diff suppressed because it is too large Load Diff
+6 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "1.5.7" version = "1.6.1"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -14,18 +14,16 @@ tauri-build = { version = "1", features = [] }
[dependencies] [dependencies]
warp = "0.3" warp = "0.3"
which = "6.0.0"
anyhow = "1.0" anyhow = "1.0"
dirs = "5.0" dirs = "5.0"
open = "5.0" open = "5.0"
log = "0.4" log = "0.4"
ctrlc = "3.4"
dunce = "1.0" dunce = "1.0"
log4rs = "1" log4rs = "1"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4" chrono = "0.4"
sysinfo = "0.30" sysinfo = "0.30"
rquickjs = "0.3" # 高版本不支持 Linux aarch64 boa_engine = "0.18"
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.9" serde_yaml = "0.9"
once_cell = "1.18" once_cell = "1.18"
@@ -36,17 +34,18 @@ percent-encoding = "2.3.1"
window-shadows = { version = "0.2" } window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] } reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" } sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" } auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { version = "1.5", features = [ "path-all", "protocol-asset", "dialog-open", "notification-all", "icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] } tauri = { version = "1.6", 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] [target.'cfg(windows)'.dependencies]
runas = "=1.0.0" # 高版本会返回错误 Status runas = "=1.2.0"
deelevate = "0.2.0" deelevate = "0.2.0"
winreg = "0.52.0" winreg = "0.52.0"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
#openssl #openssl
[features] [features]
+44 -26
View File
@@ -9,7 +9,7 @@ use anyhow::{Context, Result};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use sysproxy::Sysproxy; use sysproxy::Sysproxy;
use tauri::api; use tauri::{api, Manager};
type CmdResult<T = ()> = Result<T, String>; type CmdResult<T = ()> = Result<T, String>;
#[tauri::command] #[tauri::command]
@@ -65,6 +65,7 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok(_) => {
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
let _ = handle::Handle::update_systray_part();
Config::profiles().apply(); Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?; wrap_err!(Config::profiles().data().save_file())?;
Ok(()) Ok(())
@@ -275,6 +276,23 @@ pub fn get_app_dir() -> CmdResult<String> {
Ok(app_home_dir) Ok(app_home_dir)
} }
#[tauri::command]
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
let icon_cache_dir = wrap_err!(dirs::app_home_dir())?.join("icons").join("cache");
let icon_path = icon_cache_dir.join(name);
if !icon_cache_dir.exists() {
let _ = std::fs::create_dir_all(&icon_cache_dir);
}
if !icon_path.exists() {
let response = wrap_err!(reqwest::get(url).await)?;
let mut file = wrap_err!(std::fs::File::create(&icon_path))?;
let content = wrap_err!(response.bytes().await)?;
wrap_err!(std::io::copy(&mut content.as_ref(), &mut file))?;
}
Ok(icon_path.to_string_lossy().to_string())
}
#[tauri::command] #[tauri::command]
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> { pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
let file_path = std::path::Path::new(&path); let file_path = std::path::Path::new(&path);
@@ -282,9 +300,17 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
if !icon_dir.exists() { if !icon_dir.exists() {
let _ = std::fs::create_dir_all(&icon_dir); let _ = std::fs::create_dir_all(&icon_dir);
} }
let dest_path = icon_dir.join(name); let ext = match file_path.extension() {
Some(e) => e.to_string_lossy().to_string(),
None => "ico".to_string(),
};
let png_dest_path = icon_dir.join(format!("{name}.png"));
let ico_dest_path = icon_dir.join(format!("{name}.ico"));
let dest_path = icon_dir.join(format!("{name}.{ext}"));
if file_path.exists() { if file_path.exists() {
std::fs::remove_file(png_dest_path).unwrap_or_default();
std::fs::remove_file(ico_dest_path).unwrap_or_default();
match std::fs::copy(file_path, &dest_path) { match std::fs::copy(file_path, &dest_path) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()), Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
@@ -294,6 +320,17 @@ pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
} }
} }
#[tauri::command]
pub fn open_devtools(app_handle: tauri::AppHandle) {
if let Some(window) = app_handle.get_window("main") {
if !window.is_devtools_open() {
window.open_devtools();
} else {
window.close_devtools();
}
}
}
#[tauri::command] #[tauri::command]
pub fn exit_app(app_handle: tauri::AppHandle) { pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true); let _ = resolve::save_window_size_position(&app_handle, true);
@@ -303,42 +340,23 @@ pub fn exit_app(app_handle: tauri::AppHandle) {
std::process::exit(0); std::process::exit(0);
} }
#[cfg(windows)]
pub mod service { pub mod service {
use super::*; use super::*;
use crate::core::win_service; use crate::core::service;
#[tauri::command] #[tauri::command]
pub async fn check_service() -> CmdResult<win_service::JsonResponse> { pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(win_service::check_service().await) wrap_err!(service::check_service().await)
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult { pub async fn install_service() -> CmdResult {
wrap_err!(win_service::install_service().await) wrap_err!(service::install_service().await)
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult { pub async fn uninstall_service() -> CmdResult {
wrap_err!(win_service::uninstall_service().await) wrap_err!(service::uninstall_service().await)
}
}
#[cfg(not(windows))]
pub mod service {
use super::*;
#[tauri::command]
pub async fn check_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
Ok(())
} }
} }
+44 -3
View File
@@ -38,8 +38,11 @@ impl IClashTemp {
tun.insert("strict-route".into(), false.into()); tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into()); tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].into()); tun.insert("dns-hijack".into(), vec!["any:53"].into());
tun.insert("mtu".into(), 9000.into()); tun.insert("mtu".into(), 1500.into());
#[cfg(not(target_os = "windows"))]
map.insert("redir-port".into(), 7895.into());
#[cfg(target_os = "linux")]
map.insert("tproxy-port".into(), 7896.into());
map.insert("mixed-port".into(), 7897.into()); map.insert("mixed-port".into(), 7897.into());
map.insert("socks-port".into(), 7898.into()); map.insert("socks-port".into(), 7898.into());
map.insert("port".into(), 7899.into()); map.insert("port".into(), 7899.into());
@@ -54,11 +57,18 @@ impl IClashTemp {
} }
fn guard(mut config: Mapping) -> Mapping { fn guard(mut config: Mapping) -> Mapping {
#[cfg(not(target_os = "windows"))]
let redir_port = Self::guard_redir_port(&config);
#[cfg(target_os = "linux")]
let tproxy_port = Self::guard_tproxy_port(&config);
let mixed_port = Self::guard_mixed_port(&config); let mixed_port = Self::guard_mixed_port(&config);
let socks_port = Self::guard_socks_port(&config); let socks_port = Self::guard_socks_port(&config);
let port = Self::guard_port(&config); let port = Self::guard_port(&config);
let ctrl = Self::guard_server_ctrl(&config); let ctrl = Self::guard_server_ctrl(&config);
#[cfg(not(target_os = "windows"))]
config.insert("redir-port".into(), redir_port.into());
#[cfg(target_os = "linux")]
config.insert("tproxy-port".into(), tproxy_port.into());
config.insert("mixed-port".into(), mixed_port.into()); config.insert("mixed-port".into(), mixed_port.into());
config.insert("socks-port".into(), socks_port.into()); config.insert("socks-port".into(), socks_port.into());
config.insert("port".into(), port.into()); config.insert("port".into(), port.into());
@@ -110,6 +120,37 @@ impl IClashTemp {
}), }),
} }
} }
#[cfg(not(target_os = "windows"))]
pub fn guard_redir_port(config: &Mapping) -> u16 {
let mut port = config
.get("redir-port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7895);
if port == 0 {
port = 7895;
}
port
}
#[cfg(target_os = "linux")]
pub fn guard_tproxy_port(config: &Mapping) -> u16 {
let mut port = config
.get("tproxy-port")
.and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None,
})
.unwrap_or(7896);
if port == 0 {
port = 7896;
}
port
}
pub fn guard_mixed_port(config: &Mapping) -> u16 { pub fn guard_mixed_port(config: &Mapping) -> u16 {
let mut port = config let mut port = config
+16
View File
@@ -46,6 +46,10 @@ pub struct PrfItem {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub option: Option<PrfOption>, pub option: Option<PrfOption>,
/// profile web page url
#[serde(skip_serializing_if = "Option::is_none")]
pub home: Option<String>,
/// the file data /// the file data
#[serde(skip)] #[serde(skip)]
pub file_data: Option<String>, pub file_data: Option<String>,
@@ -161,6 +165,7 @@ impl PrfItem {
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
}) })
@@ -291,6 +296,14 @@ impl PrfItem {
}, },
}; };
let home = match header.get("profile-web-page-url") {
Some(value) => {
let str_value = value.to_str().unwrap_or("");
Some(str_value.to_string())
},
None => None,
};
let uid = help::get_uid("r"); let uid = help::get_uid("r");
let file = format!("{uid}.yaml"); let file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into())); let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
@@ -317,6 +330,7 @@ impl PrfItem {
selected: None, selected: None,
extra, extra,
option, option,
home,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()), file_data: Some(data.into()),
}) })
@@ -338,6 +352,7 @@ impl PrfItem {
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize), updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_MERGE.into()), file_data: Some(tmpl::ITEM_MERGE.into()),
}) })
@@ -356,6 +371,7 @@ impl PrfItem {
desc: Some(desc), desc: Some(desc),
file: Some(file), file: Some(file),
url: None, url: None,
home: None,
selected: None, selected: None,
extra: None, extra: None,
option: None, option: None,
+1 -1
View File
@@ -211,7 +211,7 @@ impl IProfiles {
if each.uid == some_uid { if each.uid == some_uid {
each.extra = item.extra; each.extra = item.extra;
each.updated = item.updated; each.updated = item.updated;
each.home = item.home;
// save the file data // save the file data
// move the field value after save // move the field value after save
if let Some(file_data) = item.file_data.take() { if let Some(file_data) = item.file_data.take() {
+15 -1
View File
@@ -126,7 +126,13 @@ pub struct IVerge {
/// 是否启用随机端口 /// 是否启用随机端口
pub enable_random_port: Option<bool>, pub enable_random_port: Option<bool>,
/// verge mixed port 用于覆盖 clash 的 mixed port /// verge 的各种 port 用于覆盖 clash 的各种 port
#[cfg(not(target_os = "windows"))]
pub verge_redir_port: Option<u16>,
#[cfg(target_os = "linux")]
pub verge_tproxy_port: Option<u16>,
pub verge_mixed_port: Option<u16>, pub verge_mixed_port: Option<u16>,
pub verge_socks_port: Option<u16>, pub verge_socks_port: Option<u16>,
@@ -190,6 +196,10 @@ impl IVerge {
enable_silent_start: Some(false), enable_silent_start: Some(false),
enable_system_proxy: Some(false), enable_system_proxy: Some(false),
enable_random_port: Some(false), enable_random_port: Some(false),
#[cfg(not(target_os = "windows"))]
verge_redir_port: Some(7895),
#[cfg(target_os = "linux")]
verge_tproxy_port: Some(7896),
verge_mixed_port: Some(7897), verge_mixed_port: Some(7897),
verge_socks_port: Some(7898), verge_socks_port: Some(7898),
verge_port: Some(7899), verge_port: Some(7899),
@@ -239,6 +249,10 @@ impl IVerge {
patch!(enable_auto_launch); patch!(enable_auto_launch);
patch!(enable_silent_start); patch!(enable_silent_start);
patch!(enable_random_port); patch!(enable_random_port);
#[cfg(not(target_os = "windows"))]
patch!(verge_redir_port);
#[cfg(target_os = "linux")]
patch!(verge_tproxy_port);
patch!(verge_mixed_port); patch!(verge_mixed_port);
patch!(verge_socks_port); patch!(verge_socks_port);
patch!(verge_port); patch!(verge_port);
+21 -60
View File
@@ -1,3 +1,4 @@
use super::service;
use super::{clash_api, logger::Logger}; use super::{clash_api, logger::Logger};
use crate::log_err; use crate::log_err;
use crate::{config::*, utils::dirs}; use crate::{config::*, utils::dirs};
@@ -93,10 +94,9 @@ impl CoreManager {
None => false, None => false,
}; };
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service"); log::debug!(target: "app", "stop the core by service");
log_err!(super::win_service::stop_core_by_service().await); log_err!(service::stop_core_by_service().await);
should_kill = true; should_kill = true;
} }
@@ -105,48 +105,27 @@ impl CoreManager {
sleep(Duration::from_millis(500)).await; sleep(Duration::from_millis(500)).await;
} }
#[cfg(target_os = "macos")] // 服务模式
{ let enable = { Config::verge().latest().enable_service_mode };
let enable_tun = Config::verge().latest().enable_tun_mode.clone(); let enable = enable.unwrap_or(false);
let enable_tun = enable_tun.unwrap_or(false);
log::debug!(target: "app", "try to set system dns");
if enable_tun {
let script = include_str!("./script/set_dns.sh");
match (|| async { Command::new("bash").args([script]).output() })().await {
Ok(_) => return Ok(()),
Err(err) => {
log::error!(target: "app", "{err}");
}
}
}
}
#[cfg(target_os = "windows")] *self.use_service_mode.lock() = enable;
{
use super::win_service;
// 服务模式 if enable {
let enable = { Config::verge().latest().enable_service_mode }; // 服务模式启动失败就直接运行sidecar
let enable = enable.unwrap_or(false); log::debug!(target: "app", "try to run core in service mode");
*self.use_service_mode.lock() = enable; match (|| async {
service::check_service().await?;
if enable { service::run_core_by_service(&config_path).await
// 服务模式启动失败就直接运行sidecar })()
log::debug!(target: "app", "try to run core in service mode"); .await
{
match (|| async { Ok(_) => return Ok(()),
win_service::check_service().await?; Err(err) => {
win_service::run_core_by_service(&config_path).await // 修改这个值,免得stop出错
})() *self.use_service_mode.lock() = false;
.await log::error!(target: "app", "{err}");
{
Ok(_) => return Ok(()),
Err(err) => {
// 修改这个值,免得stop出错
*self.use_service_mode.lock() = false;
log::error!(target: "app", "{err}");
}
} }
} }
} }
@@ -221,7 +200,6 @@ impl CoreManager {
/// 重启内核 /// 重启内核
pub fn recover_core(&'static self) -> Result<()> { pub fn recover_core(&'static self) -> Result<()> {
// 服务模式不管 // 服务模式不管
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
return Ok(()); return Ok(());
} }
@@ -254,31 +232,14 @@ impl CoreManager {
/// 停止核心运行 /// 停止核心运行
pub fn stop_core(&self) -> Result<()> { pub fn stop_core(&self) -> Result<()> {
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service"); log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
log_err!(super::win_service::stop_core_by_service().await); log_err!(service::stop_core_by_service().await);
}); });
return Ok(()); return Ok(());
} }
#[cfg(target_os = "macos")]
{
let enable_tun = Config::verge().latest().enable_tun_mode.clone();
let enable_tun = enable_tun.unwrap_or(false);
log::debug!(target: "app", "try to unset system dns");
if enable_tun {
let script = include_str!("./script/unset_dns.sh");
match (|| Command::new("bash").args([script]).output())() {
Ok(_) => return Ok(()),
Err(err) => {
log::error!(target: "app", "{err}");
}
}
}
}
let mut sidecar = self.sidecar.lock(); let mut sidecar = self.sidecar.lock();
if let Some(child) = sidecar.take() { if let Some(child) = sidecar.take() {
log::debug!(target: "app", "stop the core by sidecar"); log::debug!(target: "app", "stop the core by sidecar");
+1 -1
View File
@@ -22,7 +22,7 @@ pub fn grant_permission(core: String) -> anyhow::Result<()> {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let output = { let output = {
let path = path.replace(' ', "\\ "); // 避免路径中有空格 let path = path.replace(' ', "\\ "); // 避免路径中有空格
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}"); 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() { let sudo = match Command::new("which").arg("pkexec").output() {
Ok(output) => { Ok(output) => {
+1 -1
View File
@@ -7,7 +7,7 @@ pub mod manager;
pub mod sysopt; pub mod sysopt;
pub mod timer; pub mod timer;
pub mod tray; pub mod tray;
pub mod win_service; pub mod service;
pub mod win_uwp; pub mod win_uwp;
pub use self::core::*; pub use self::core::*;
-5
View File
@@ -1,5 +0,0 @@
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '/Hardware Port/{port=$3} /Device:/{if ($2 == dev) {print port; exit}}')
networksetup -setdnsservers $hardware_port 223.5.5.5
-5
View File
@@ -1,5 +0,0 @@
nic=$(route -n get default | grep "interface" | awk '{print $2}')
hardware_port=$(networksetup -listallhardwareports | awk -v dev="$nic" '/Hardware Port/{port=$3} /Device:/{if ($2 == dev) {print port; exit}}')
networksetup -setdnsservers $hardware_port Empty
@@ -1,18 +1,15 @@
#![cfg(target_os = "windows")]
use crate::config::Config; use crate::config::Config;
use crate::utils::dirs; use crate::utils::dirs;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::os::windows::process::CommandExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand}; use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep; use tokio::time::sleep;
// Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211"; const SERVICE_URL: &str = "http://127.0.0.1:33211";
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
@@ -32,7 +29,13 @@ pub struct JsonResponse {
/// Install the Clash Verge Service /// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
///
#[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> { pub async fn install_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe"); let install_path = binary_path.with_file_name("install-service.exe");
@@ -60,9 +63,69 @@ pub async fn install_service() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(installer_path).status()?,
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(installer_path)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// Uninstall the Clash Verge Service /// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe"); let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
@@ -90,6 +153,63 @@ pub async fn uninstall_service() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstaller_path).status()?,
_ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(uninstaller_path)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// check the windows service status /// check the windows service status
pub async fn check_service() -> Result<JsonResponse> { pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash"); let url = format!("{SERVICE_URL}/get_clash");
@@ -119,7 +239,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into()); let clash_core = clash_core.unwrap_or("clash".into());
let clash_bin = format!("{clash_core}.exe"); let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}");
let bin_path = current_exe()?.with_file_name(clash_bin); let bin_path = current_exe()?.with_file_name(clash_bin);
let bin_path = dirs::path_to_str(&bin_path)?; let bin_path = dirs::path_to_str(&bin_path)?;
+3 -2
View File
@@ -3,9 +3,10 @@ use anyhow::{anyhow, Result};
use auto_launch::{AutoLaunch, AutoLaunchBuilder}; use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::env::current_exe;
use std::sync::Arc; use std::sync::Arc;
use sysproxy::Sysproxy; use sysproxy::Sysproxy;
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe}; use tauri::async_runtime::Mutex as TokioMutex;
pub struct Sysopt { pub struct Sysopt {
/// current system proxy setting /// current system proxy setting
@@ -149,7 +150,7 @@ impl Sysopt {
/// init the auto launch /// init the auto launch
pub fn init_launch(&self) -> Result<()> { pub fn init_launch(&self) -> Result<()> {
let app_exe = current_exe()?; let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?; // let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe let app_name = app_exe
.file_stem() .file_stem()
.and_then(|f| f.to_str()) .and_then(|f| f.to_str())
+99 -13
View File
@@ -130,6 +130,44 @@ impl Tray {
let _ = tray.get_item("global_mode").set_selected(mode == "global"); let _ = tray.get_item("global_mode").set_selected(mode == "global");
let _ = tray.get_item("direct_mode").set_selected(mode == "direct"); let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
#[cfg(target_os = "linux")]
match mode.as_str() {
"rule" => {
let _ = tray
.get_item("rule_mode")
.set_title(t!("Rule Mode ✔", "规则模式 ✔"));
let _ = tray
.get_item("global_mode")
.set_title(t!("Global Mode", "全局模式"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("Direct Mode", "直连模式"));
}
"global" => {
let _ = tray
.get_item("rule_mode")
.set_title(t!("Rule Mode", "规则模式"));
let _ = tray
.get_item("global_mode")
.set_title(t!("Global Mode ✔", "全局模式 ✔"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("Direct Mode", "直连模式"));
}
"direct" => {
let _ = tray
.get_item("rule_mode")
.set_title(t!("Rule Mode", "规则模式"));
let _ = tray
.get_item("global_mode")
.set_title(t!("Global Mode", "全局模式"));
let _ = tray
.get_item("direct_mode")
.set_title(t!("Direct Mode ✔", "直连模式 ✔"));
}
_ => {}
}
let verge = Config::verge(); let verge = Config::verge();
let verge = verge.latest(); let verge = verge.latest();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
@@ -144,9 +182,13 @@ impl Tray {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec(); let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
if *sysproxy_tray_icon { if *sysproxy_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("sysproxy.png"); let icon_dir_path = dirs::app_home_dir()?.join("icons");
if path.exists() { let png_path = icon_dir_path.join("sysproxy.png");
icon = std::fs::read(path).unwrap(); let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
} }
} }
icon icon
@@ -156,9 +198,13 @@ impl Tray {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec(); let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
if *common_tray_icon { if *common_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("common.png"); let icon_dir_path = dirs::app_home_dir()?.join("icons");
if path.exists() { let png_path = icon_dir_path.join("common.png");
icon = std::fs::read(path).unwrap(); let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
} }
} }
icon icon
@@ -170,9 +216,13 @@ impl Tray {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec(); let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
if *tun_tray_icon { if *tun_tray_icon {
let path = dirs::app_home_dir()?.join("icons").join("tun.png"); let icon_dir_path = dirs::app_home_dir()?.join("icons");
if path.exists() { let png_path = icon_dir_path.join("tun.png");
icon = std::fs::read(path).unwrap(); let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
} }
} }
indication_icon = icon indication_icon = icon
@@ -182,6 +232,27 @@ impl Tray {
let _ = tray.get_item("system_proxy").set_selected(*system_proxy); let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
let _ = tray.get_item("tun_mode").set_selected(*tun_mode); let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
#[cfg(target_os = "linux")]
{
if *system_proxy {
let _ = tray
.get_item("system_proxy")
.set_title(t!("System Proxy ✔", "系统代理 ✔"));
} else {
let _ = tray
.get_item("system_proxy")
.set_title(t!("System Proxy", "系统代理"));
}
if *tun_mode {
let _ = tray
.get_item("tun_mode")
.set_title(t!("TUN Mode ✔", "Tun 模式 ✔"));
} else {
let _ = tray
.get_item("tun_mode")
.set_title(t!("TUN Mode", "Tun 模式"));
}
}
let switch_map = { let switch_map = {
let mut map = std::collections::HashMap::new(); let mut map = std::collections::HashMap::new();
@@ -190,18 +261,30 @@ impl Tray {
map map
}; };
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
if let Some(current_profile_uid) = profiles.get_current() {
let current_profile = profiles.get_item(&current_profile_uid);
current_profile_name = match &current_profile.unwrap().name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
};
let _ = tray.set_tooltip(&format!( let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}", "Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"), t!("System Proxy", "系统代理"),
switch_map[system_proxy], switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"), t!("TUN Mode", "Tun 模式"),
switch_map[tun_mode] switch_map[tun_mode],
t!("Curent Profile", "当前订阅"),
current_profile_name
)); ));
Ok(()) Ok(())
} }
pub fn on_left_click(app_handle: &AppHandle) { pub fn on_click(app_handle: &AppHandle) {
let tray_event = { Config::verge().latest().tray_event.clone() }; let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event = tray_event.unwrap_or("main_window".into()); let tray_event = tray_event.unwrap_or("main_window".into());
match tray_event.as_str() { match tray_event.as_str() {
@@ -214,7 +297,10 @@ impl Tray {
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) { pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event { match event {
SystemTrayEvent::LeftClick { .. } => Tray::on_left_click(app_handle), #[cfg(not(target_os = "macos"))]
SystemTrayEvent::LeftClick { .. } => Tray::on_click(app_handle),
#[cfg(target_os = "macos")]
SystemTrayEvent::RightClick { .. } => Tray::on_click(app_handle),
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => { mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5]; let mode = &mode[0..mode.len() - 5];
+4 -4
View File
@@ -1,6 +1,6 @@
function main(params) { function main(config) {
if (params.mode === "script") { if (config.mode === "script") {
params.mode = "rule"; config.mode = "rule";
} }
return params; return config;
} }
@@ -1,10 +1,10 @@
function main(params) { function main(config) {
if (Array.isArray(params.proxies)) { if (Array.isArray(config.proxies)) {
params.proxies.forEach((p, i) => { config.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") { if (p.type === "hysteria" && typeof p.alpn === "string") {
params.proxies[i].alpn = [p.alpn]; config.proxies[i].alpn = [p.alpn];
} }
}); });
} }
return params; return config;
} }
+3 -1
View File
@@ -1,8 +1,10 @@
use serde_yaml::{Mapping, Value}; use serde_yaml::{Mapping, Value};
use std::collections::HashSet; use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 9] = [ pub const HANDLE_FIELDS: [&str; 11] = [
"mode", "mode",
"redir-port",
"tproxy-port",
"mixed-port", "mixed-port",
"socks-port", "socks-port",
"port", "port",
+44 -31
View File
@@ -1,62 +1,75 @@
use super::use_lowercase; use super::use_lowercase;
use anyhow::Result; use anyhow::{Error, Result};
use serde_yaml::Mapping; use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> { pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
use rquickjs::{function::Func, Context, Runtime}; use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
let mut context = Context::default();
let runtime = Runtime::new().unwrap();
let context = Context::full(&runtime).unwrap();
let outputs = Arc::new(Mutex::new(vec![])); let outputs = Arc::new(Mutex::new(vec![]));
let copy_outputs = outputs.clone(); let copy_outputs = outputs.clone();
let result = context.with(|ctx| -> Result<Mapping> { unsafe {
ctx.globals().set( let _ = context.register_global_builtin_callable(
"__verge_log__", "__verge_log__".into(),
Func::from(move |level: String, data: String| { 2,
let mut out = copy_outputs.lock().unwrap(); NativeFunction::from_closure(
out.push((level, data)); move |_: &JsValue, args: &[JsValue], context: &mut Context| {
}), let level = args.get(0).unwrap().to_string(context)?;
)?; let level = level.to_std_string().unwrap();
let data = args.get(1).unwrap().to_string(context)?;
ctx.eval( let data = data.to_std_string().unwrap();
r#"var console = Object.freeze({ let mut out = copy_outputs.lock().unwrap();
out.push((level, data));
Ok(JsValue::undefined())
},
),
);
}
let _ = context.eval(Source::from_bytes(
r#"var console = Object.freeze({
log(data){__verge_log__("log",JSON.stringify(data))}, log(data){__verge_log__("log",JSON.stringify(data))},
info(data){__verge_log__("info",JSON.stringify(data))}, info(data){__verge_log__("info",JSON.stringify(data))},
error(data){__verge_log__("error",JSON.stringify(data))}, error(data){__verge_log__("error",JSON.stringify(data))},
debug(data){__verge_log__("debug",JSON.stringify(data))}, debug(data){__verge_log__("debug",JSON.stringify(data))},
});"#, });"#,
)?; ));
let config = use_lowercase(config.clone()); let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?; let config_str = serde_json::to_string(&config)?;
let code = format!( let code = format!(
r#"try{{ r#"try{{
{script}; {script};
JSON.stringify(main({config_str})||'') JSON.stringify(main({config_str})||'')
}} catch(err) {{ }} catch(err) {{
`__error_flag__ ${{err.toString()}}` `__error_flag__ ${{err.toString()}}`
}}"# }}"#
); );
let result: String = ctx.eval(code.as_str())?; if let Ok(result) = context.eval(Source::from_bytes(code.as_str())) {
if !result.is_string() {
anyhow::bail!("main function should return object");
}
let result = result.to_string(&mut context).unwrap();
let result = result.to_std_string().unwrap();
if result.starts_with("__error_flag__") { if result.starts_with("__error_flag__") {
anyhow::bail!(result[15..].to_owned()); anyhow::bail!(result[15..].to_owned());
} }
if result == "\"\"" { if result == "\"\"" {
anyhow::bail!("main function should return object"); anyhow::bail!("main function should return object");
} }
Ok(serde_json::from_str::<Mapping>(result.as_str())?) let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
}); let mut out = outputs.lock().unwrap();
match res {
let mut out = outputs.lock().unwrap(); Ok(config) => Ok((use_lowercase(config), out.to_vec())),
match result { Err(err) => {
Ok(config) => Ok((use_lowercase(config), out.to_vec())), out.push(("exception".into(), err.to_string()));
Err(err) => { Ok((config, out.to_vec()))
out.push(("exception".into(), err.to_string())); }
Ok((config, out.to_vec()))
} }
} else {
anyhow::bail!("main function should return object");
} }
} }
+52
View File
@@ -34,8 +34,60 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
revise!(config, "tun", tun_val); revise!(config, "tun", tun_val);
if enable { 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}");
}
}
}
use_dns_for_tun(config) use_dns_for_tun(config)
} else { } 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}");
}
}
}
config config
} }
} }
+12 -16
View File
@@ -107,6 +107,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone()); Config::clash().draft().patch_config(patch.clone());
match { match {
let redir_port = patch.get("redir-port");
let tproxy_port = patch.get("tproxy-port");
let mixed_port = patch.get("mixed-port"); let mixed_port = patch.get("mixed-port");
let socks_port = patch.get("socks-port"); let socks_port = patch.get("socks-port");
let port = patch.get("port"); let port = patch.get("port");
@@ -129,7 +131,9 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
}; };
// 激活订阅 // 激活订阅
if mixed_port.is_some() if redir_port.is_some()
|| tproxy_port.is_some()
|| mixed_port.is_some()
|| socks_port.is_some() || socks_port.is_some()
|| port.is_some() || port.is_some()
|| patch.get("secret").is_some() || patch.get("secret").is_some()
@@ -181,22 +185,14 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let tun_tray_icon = patch.tun_tray_icon; let tun_tray_icon = patch.tun_tray_icon;
match { match {
#[cfg(target_os = "windows")] let service_mode = patch.enable_service_mode;
{
let service_mode = patch.enable_service_mode;
if service_mode.is_some() { if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap()); log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
Config::generate()?; Config::generate()?;
CoreManager::global().run_core().await?; CoreManager::global().run_core().await?;
} else if tun_mode.is_some() { } else if tun_mode.is_some() {
update_core_config().await?;
}
}
#[cfg(not(target_os = "windows"))]
if tun_mode.is_some() {
update_core_config().await?; update_core_config().await?;
} }
@@ -303,7 +299,7 @@ pub fn copy_clash_env(app_handle: &AppHandle) {
let sh = let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"); format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}"); let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\""); let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let mut cliboard = app_handle.clipboard_manager(); let mut cliboard = app_handle.clipboard_manager();
+5
View File
@@ -20,6 +20,9 @@ fn main() -> std::io::Result<()> {
return Ok(()); return Ok(());
} }
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
crate::log_err!(init::init_config()); crate::log_err!(init::init_config());
#[allow(unused_mut)] #[allow(unused_mut)]
@@ -57,6 +60,8 @@ fn main() -> std::io::Result<()> {
cmds::test_delay, cmds::test_delay,
cmds::get_app_dir, cmds::get_app_dir,
cmds::copy_icon_file, cmds::copy_icon_file,
cmds::download_icon_cache,
cmds::open_devtools,
cmds::exit_app, cmds::exit_app,
// cmds::update_hotkeys, // cmds::update_hotkeys,
// profile // profile
+5 -1
View File
@@ -92,12 +92,16 @@ pub fn clash_pid_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join("clash.pid")) 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"))
}
#[cfg(windows)] #[cfg(windows)]
pub fn service_path() -> Result<PathBuf> { pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service.exe")) Ok(app_resources_dir()?.join("clash-verge-service.exe"))
} }
#[cfg(windows)]
pub fn service_log_file() -> Result<PathBuf> { pub fn service_log_file() -> Result<PathBuf> {
use chrono::Local; use chrono::Local;
+7 -1
View File
@@ -135,6 +135,12 @@ pub fn delete_log() -> Result<()> {
for file in fs::read_dir(&log_dir)?.flatten() { for file in fs::read_dir(&log_dir)?.flatten() {
let _ = process_file(file); let _ = process_file(file);
} }
let service_log_dir = log_dir.join("service");
for file in fs::read_dir(&service_log_dir)?.flatten() {
let _ = process_file(file);
}
Ok(()) Ok(())
} }
@@ -298,7 +304,7 @@ pub fn startup_script() -> Result<()> {
shell = "powershell"; shell = "powershell";
} }
if path.ends_with(".bat") { if path.ends_with(".bat") {
shell = "cmd"; shell = "powershell";
} }
if shell.is_empty() { if shell.is_empty() {
return Err(anyhow::anyhow!("unsupported script: {path}")); return Err(anyhow::anyhow!("unsupported script: {path}"));
+1
View File
@@ -4,3 +4,4 @@ pub mod init;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;
pub mod tmpl; pub mod tmpl;
pub mod unix_helper;
+3 -5
View File
@@ -141,10 +141,7 @@ pub fn create_window(app_handle: &AppHandle) {
_ => { _ => {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
builder = builder builder = builder.inner_size(800.0, 636.0).center();
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions")
.inner_size(800.0, 636.0)
.center();
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -161,6 +158,7 @@ pub fn create_window(app_handle: &AppHandle) {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let window = builder let window = builder
.decorations(false) .decorations(false)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true) .transparent(true)
.visible(false) .visible(false)
.build(); .build();
@@ -171,7 +169,7 @@ pub fn create_window(app_handle: &AppHandle) {
.title_bar_style(tauri::TitleBarStyle::Overlay) .title_bar_style(tauri::TitleBarStyle::Overlay)
.build(); .build();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let window = builder.decorations(true).transparent(false).build(); let window = builder.decorations(false).transparent(true).build();
match window { match window {
Ok(win) => { Ok(win) => {
+18 -19
View File
@@ -1,44 +1,43 @@
//! Some config file template //! Some config file template
/// template for new a profile item /// template for new a profile item
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge pub const ITEM_LOCAL: &str = "# Profile Template for Clash Verge
proxies: proxies: []
proxy-groups: proxy-groups: []
rules: rules: []
"; ";
/// enhanced profile /// enhanced profile
pub const ITEM_MERGE: &str = "# Merge Template for clash verge pub const ITEM_MERGE: &str = "# Profile Enhancement Merge Template for Clash Verge
# The `Merge` format used to enhance profile
prepend-rules: prepend-rules: []
prepend-rule-providers: prepend-rule-providers: {}
prepend-proxies: prepend-proxies: []
prepend-proxy-providers: prepend-proxy-providers: {}
prepend-proxy-groups: prepend-proxy-groups: []
append-rules: append-rules: []
append-rule-providers: append-rule-providers: {}
append-proxies: append-proxies: []
append-proxy-providers: append-proxy-providers: {}
append-proxy-groups: append-proxy-groups: []
"; ";
/// enhanced profile /// enhanced profile
pub const ITEM_SCRIPT: &str = "// Define the `main` function pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
function main(params) { function main(config) {
return params; return config;
} }
"; ";
+14
View File
@@ -0,0 +1,14 @@
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> &'static str {
use std::process::Command;
match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
}
}
+7 -1
View File
@@ -1,7 +1,8 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": { "package": {
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "1.5.7" "version": "1.6.1"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
@@ -65,6 +66,11 @@
}, },
"path": { "path": {
"all": true "all": true
},
"fs": {
"exists": true,
"readFile": true,
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"]
} }
}, },
"windows": [], "windows": [],
+2
View File
@@ -1,9 +1,11 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": { "tauri": {
"systemTray": { "systemTray": {
"iconPath": "icons/tray-icon.png" "iconPath": "icons/tray-icon.png"
}, },
"bundle": { "bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["deb", "appimage", "updater"], "targets": ["deb", "appimage", "updater"],
"deb": { "deb": {
"depends": ["openssl"], "depends": ["openssl"],
+2
View File
@@ -1,10 +1,12 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": { "tauri": {
"systemTray": { "systemTray": {
"iconPath": "icons/mac-tray-icon.png", "iconPath": "icons/mac-tray-icon.png",
"iconAsTemplate": true "iconAsTemplate": true
}, },
"bundle": { "bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["app", "dmg", "updater"], "targets": ["app", "dmg", "updater"],
"macOS": { "macOS": {
"frameworks": [], "frameworks": [],
+2
View File
@@ -1,9 +1,11 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": { "tauri": {
"systemTray": { "systemTray": {
"iconPath": "icons/tray-icon.png" "iconPath": "icons/tray-icon.png"
}, },
"bundle": { "bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["nsis", "updater"], "targets": ["nsis", "updater"],
"windows": { "windows": {
"certificateThumbprint": null, "certificateThumbprint": null,
+31 -6
View File
@@ -743,6 +743,30 @@ Function un.onInit
!insertmacro MUI_UNGETLANGUAGE !insertmacro MUI_UNGETLANGUAGE
FunctionEnd FunctionEnd
Function un.isDirectoryEmpty
Exch $0
Push $1
Push $2
StrCpy $2 0
FindFirst $1 $2 "$0\*.*"
loop:
StrCmp $2 "" done
StrCmp $2 "." next
StrCmp $2 ".." next
StrCpy $0 0
goto done
next:
FindNext $1 $2
goto loop
done:
FindClose $1
StrCmp $2 "" 0 +2
StrCpy $0 1
Pop $2
Pop $1
Exch $0
FunctionEnd
Section Uninstall Section Uninstall
!insertmacro CheckIfAppIsRunning !insertmacro CheckIfAppIsRunning
@@ -756,7 +780,7 @@ Section Uninstall
{{#each resources}} {{#each resources}}
Delete "$INSTDIR\\{{this.[1]}}" Delete "$INSTDIR\\{{this.[1]}}"
{{/each}} {{/each}}
Delete "$INSTDIR\resources"
; Delete external binaries ; Delete external binaries
{{#each binaries}} {{#each binaries}}
Delete "$INSTDIR\\{{this}}" Delete "$INSTDIR\\{{this}}"
@@ -765,13 +789,14 @@ Section Uninstall
; Delete uninstaller ; Delete uninstaller
Delete "$INSTDIR\uninstall.exe" Delete "$INSTDIR\uninstall.exe"
${If} $DeleteAppDataCheckboxState == 1 ; Remove InstallDir
Push "$INSTDIR"
Call un.isDirectoryEmpty
Pop $0
${If} $0 == 1
RMDir /R /REBOOTOK "$INSTDIR" RMDir /R /REBOOTOK "$INSTDIR"
${Else} ${Else}
{{#each resources_ancestors}} MessageBox MB_OK "Install Directory is not Empty, Please remove it manually."
RMDir /REBOOTOK "$INSTDIR\\{{this}}"
{{/each}}
RMDir "$INSTDIR"
${EndIf} ${EndIf}
; Remove start menu shortcut ; Remove start menu shortcut
+5 -9
View File
@@ -47,11 +47,11 @@ body {
@import "./page.scss"; @import "./page.scss";
@import "./font.scss"; @import "./font.scss";
@media (prefers-color-scheme: dark) { // @media (prefers-color-scheme: dark) {
:root { // :root {
background-color: rgba(18, 18, 18, 1); // background-color: rgba(18, 18, 18, 1);
} // }
} // }
.user-none { .user-none {
user-select: none; user-select: none;
@@ -59,7 +59,3 @@ body {
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
} }
[data-tauri-drag-region] {
app-region: drag;
}
+19 -8
View File
@@ -11,7 +11,8 @@
width: 100%; width: 100%;
// max-width: 225px; // max-width: 225px;
// min-width: 225px; // min-width: 225px;
padding: 16px 0 8px; // padding: 16px 0 8px;
padding: 0px 0px 8px;
// position: relative; // position: relative;
flex-direction: column; flex-direction: column;
align-self: stretch; align-self: stretch;
@@ -37,7 +38,7 @@
justify-content: center; justify-content: center;
align-items: flex-start; align-items: flex-start;
align-self: stretch; align-self: stretch;
border-bottom: 1px solid var(--divider-color); // border-bottom: 1px solid var(--divider-color);
// max-width: $maxLogo + 32px; // max-width: $maxLogo + 32px;
// max-height: $maxLogo; // max-height: $maxLogo;
// margin: 0 auto; // margin: 0 auto;
@@ -60,7 +61,7 @@
.the-newbtn { .the-newbtn {
position: absolute; position: absolute;
right: 10px; right: 10px;
top: 0px; top: 15px;
border-radius: 8px; border-radius: 8px;
padding: 2px 4px; padding: 2px 4px;
transform: scale(0.8); transform: scale(0.8);
@@ -79,7 +80,7 @@
> div { > div {
margin: 0 auto; margin: 0 auto;
padding: 0px 10px; padding: 0px 20px;
} }
} }
} }
@@ -94,12 +95,16 @@
// position: absolute; // position: absolute;
// top: 0px; // top: 0px;
// right: 0px; // right: 0px;
height: 24px; height: 36px;
display: flex; display: flex;
// align-items: center; // align-items: center;
justify-content: end; justify-content: end;
box-sizing: border-box; box-sizing: border-box;
z-index: 2; z-index: 2;
.the-dragbar {
margin-top: 5px;
app-region: drag;
}
} }
.the-content { .the-content {
@@ -116,12 +121,18 @@
.windows, .windows,
.unknown { .unknown {
&.layout { &.layout {
.layout__left { //.layout__left {
padding-top: 24px; // padding-top: 24px;
} //}
.layout__left .the-logo { .layout__left .the-logo {
flex: 1 0 58px; flex: 1 0 58px;
margin-top: 10px;
margin-left: 10px;
padding-top: 30px;
padding-left: 10px;
padding-right: 20px;
padding-bottom: 16px;
} }
.layout__right .the-content { .layout__right .the-content {
@@ -0,0 +1,24 @@
import { TextField, type TextFieldProps, styled } from "@mui/material";
import { useTranslation } from "react-i18next";
export const BaseStyledTextField = styled((props: TextFieldProps) => {
const { t } = useTranslation();
return (
<TextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
sx={{ input: { py: 0.65, px: 1.25 } }}
{...props}
/>
);
})(({ theme }) => ({
"& .MuiInputBase-root": {
background: theme.palette.mode === "light" ? "#fff" : undefined,
},
}));
@@ -3,8 +3,8 @@ import { forwardRef, useImperativeHandle, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, Button, Snackbar } from "@mui/material"; import { Box, Button, Snackbar } from "@mui/material";
import { deleteConnection } from "@/services/api"; import { deleteConnection } from "@/services/api";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { t } from "i18next";
export interface ConnectionDetailRef { export interface ConnectionDetailRef {
open: (detail: IConnectionsItem) => void; open: (detail: IConnectionsItem) => void;
@@ -69,7 +69,9 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
{ label: "Rule", value: rule }, { label: "Rule", value: rule },
{ {
label: "Process", label: "Process",
value: truncateStr(metadata.process || metadata.processPath), value: `${metadata.process}${
metadata.processPath ? `(${metadata.processPath})` : ""
}`,
}, },
{ label: "Time", value: dayjs(data.start).fromNow() }, { label: "Time", value: dayjs(data.start).fromNow() },
{ label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` }, { label: "Source", value: `${metadata.sourceIP}:${metadata.sourcePort}` },
@@ -96,7 +98,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
onClose?.(); onClose?.();
}} }}
> >
Close {t("Close")}
</Button> </Button>
</Box> </Box>
</Box> </Box>
+17 -4
View File
@@ -8,16 +8,29 @@ import {
PushPinOutlined, PushPinOutlined,
PushPinRounded, PushPinRounded,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { useState } from "react"; import { useEffect, useState } from "react";
export const LayoutControl = () => { export const LayoutControl = () => {
const minWidth = 40; const minWidth = 40;
const [isMaximized, setIsMaximized] = useState(false); const [isMaximized, setIsMaximized] = useState(false);
const [isPined, setIsPined] = useState(false); const [isPined, setIsPined] = useState(false);
appWindow.isMaximized().then((isMaximized) => {
setIsMaximized(() => isMaximized); useEffect(() => {
}); const unlistenResized = appWindow.onResized(() => {
appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized);
});
});
appWindow.isMaximized().then((maximized) => {
setIsMaximized(() => maximized);
});
return () => {
unlistenResized.then((fn) => fn());
};
}, []);
return ( return (
<ButtonGroup <ButtonGroup
+13 -11
View File
@@ -29,21 +29,23 @@ export const LayoutTraffic = () => {
// setup log ws during layout // setup log ws during layout
useLogSetup(); useLogSetup();
const { connect, disconnect } = useWebsocket((event) => { const trafficWs = useWebsocket(
const data = JSON.parse(event.data) as ITrafficItem; (event) => {
trafficRef.current?.appendData(data); const data = JSON.parse(event.data) as ITrafficItem;
setTraffic(data); trafficRef.current?.appendData(data);
}); setTraffic(data);
},
{ onError: () => setTraffic({ up: 0, down: 0 }), errorCount: 10 }
);
useEffect(() => { useEffect(() => {
if (!clashInfo || !pageVisible) return; if (!clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
connect(`ws://${server}/traffic?token=${encodeURIComponent(secret)}`); trafficWs.connect(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`
return () => { );
disconnect(); return () => trafficWs.disconnect();
};
}, [clashInfo, pageVisible]); }, [clashInfo, pageVisible]);
/* --------- meta memory information --------- */ /* --------- meta memory information --------- */
@@ -54,7 +56,7 @@ export const LayoutTraffic = () => {
(event) => { (event) => {
setMemory(JSON.parse(event.data)); setMemory(JSON.parse(event.data));
}, },
{ onError: () => setMemory({ inuse: 0 }) } { onError: () => setMemory({ inuse: 0 }), errorCount: 10 }
); );
useEffect(() => { useEffect(() => {
+46
View File
@@ -0,0 +1,46 @@
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
interface Props {
open: boolean;
title: string;
message: string;
onClose: () => void;
onConfirm: () => void;
}
export const ConfirmViewer = (props: Props) => {
const { open, title, message, onClose, onConfirm } = props;
const { t } = useTranslation();
useEffect(() => {
if (!open) return;
}, [open]);
return (
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
<DialogTitle>{t(title)}</DialogTitle>
<DialogContent sx={{ pb: 1, userSelect: "text" }}>
{t(message)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} variant="outlined">
{t("Cancel")}
</Button>
<Button onClick={onConfirm} variant="contained">
{t("Confirm")}
</Button>
</DialogActions>
</Dialog>
);
};
+45 -11
View File
@@ -12,23 +12,45 @@ import {
import { atomThemeMode } from "@/services/states"; import { atomThemeMode } from "@/services/states";
import { readProfileFile, saveProfileFile } from "@/services/cmds"; import { readProfileFile, saveProfileFile } from "@/services/cmds";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { nanoid } from "nanoid";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; import * as monaco from "monaco-editor";
import "monaco-editor/esm/vs/basic-languages/yaml/yaml.contribution.js";
import "monaco-editor/esm/vs/editor/contrib/folding/browser/folding.js";
import { editor } from "monaco-editor/esm/vs/editor/editor.api"; import { editor } from "monaco-editor/esm/vs/editor/editor.api";
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";
interface Props { interface Props {
uid: string; uid: string;
open: boolean; open: boolean;
mode: "yaml" | "javascript"; language: "yaml" | "javascript";
schema?: "clash" | "merge";
onClose: () => void; onClose: () => void;
onChange?: () => void; onChange?: () => void;
} }
export const EditorViewer = (props: Props) => { // yaml worker
const { uid, open, mode, onClose, onChange } = props; configureMonacoYaml(monaco, {
validate: true,
enableSchemaRequest: true,
schemas: [
{
uri: "http://example.com/meta-json-schema.json",
fileMatch: ["**/*.clash.yaml"],
schema: metaSchema as JSONSchema7,
},
{
uri: "http://example.com/clash-verge-merge-json-schema.json",
fileMatch: ["**/*.merge.yaml"],
schema: mergeSchema as JSONSchema7,
},
],
});
export const EditorViewer = (props: Props) => {
const { uid, open, language, schema, onClose, onChange } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const editorRef = useRef<any>(); const editorRef = useRef<any>();
const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null); const instanceRef = useRef<editor.IStandaloneCodeEditor | null>(null);
@@ -41,13 +63,23 @@ export const EditorViewer = (props: Props) => {
const dom = editorRef.current; const dom = editorRef.current;
if (!dom) return; if (!dom) return;
if (instanceRef.current) instanceRef.current.dispose(); if (instanceRef.current) instanceRef.current.dispose();
const uri = monaco.Uri.parse(`${nanoid()}.${schema}.${language}`);
const model = monaco.editor.createModel(data, language, uri);
instanceRef.current = editor.create(editorRef.current, { instanceRef.current = editor.create(editorRef.current, {
value: data, model: model,
language: mode, language: language,
tabSize: ["yaml", "javascript"].includes(language) ? 2 : 4, // 根据语言类型设置缩进
theme: themeMode === "light" ? "vs" : "vs-dark", theme: themeMode === "light" ? "vs" : "vs-dark",
minimap: { enabled: false }, minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
mouseWheelZoom: true, // Ctrl+滚轮调节缩放
quickSuggestions: {
strings: true, // 字符串类型的建议
comments: true, // 注释类型的建议
other: true, // 其他类型的建议
},
}); });
}); });
@@ -77,8 +109,10 @@ export const EditorViewer = (props: Props) => {
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth> <Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
<DialogTitle>{t("Edit File")}</DialogTitle> <DialogTitle>{t("Edit File")}</DialogTitle>
<DialogContent sx={{ width: "95%", pb: 1, userSelect: "text" }}> <DialogContent
<div style={{ width: "100%", height: "500px" }} ref={editorRef} /> sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
>
<div style={{ width: "100%", height: "100%" }} ref={editorRef} />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
+59 -9
View File
@@ -23,7 +23,8 @@ import { Notice } from "@/components/base";
import { EditorViewer } from "./editor-viewer"; import { EditorViewer } from "./editor-viewer";
import { ProfileBox } from "./profile-box"; import { ProfileBox } from "./profile-box";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { ConfirmViewer } from "./confirm-viewer";
import { open } from "@tauri-apps/api/shell";
const round = keyframes` const round = keyframes`
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
@@ -54,6 +55,7 @@ export const ProfileItem = (props: Props) => {
// remote file mode // remote file mode
const hasUrl = !!itemData.url; const hasUrl = !!itemData.url;
const hasExtra = !!extra; // only subscription url has extra info const hasExtra = !!extra; // only subscription url has extra info
const hasHome = !!itemData.home; // only subscription url has home page
const { upload = 0, download = 0, total = 0 } = extra ?? {}; const { upload = 0, download = 0, total = 0 } = extra ?? {};
const from = parseUrl(itemData.url); const from = parseUrl(itemData.url);
@@ -92,6 +94,12 @@ export const ProfileItem = (props: Props) => {
}, [hasUrl, updated]); }, [hasUrl, updated]);
const [fileOpen, setFileOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const onOpenHome = () => {
setAnchorEl(null);
open(itemData.home ?? "");
};
const onEditInfo = () => { const onEditInfo = () => {
setAnchorEl(null); setAnchorEl(null);
@@ -164,21 +172,35 @@ export const ProfileItem = (props: Props) => {
} }
}); });
const urlModeMenu = [ const urlModeMenu = (
hasHome ? [{ label: "Home", handler: onOpenHome }] : []
).concat([
{ label: "Select", handler: onForceSelect }, { label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo }, { label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile }, { label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile }, { label: "Open File", handler: onOpenFile },
{ label: "Update", handler: () => onUpdate(0) }, { label: "Update", handler: () => onUpdate(0) },
{ label: "Update(Proxy)", handler: () => onUpdate(2) }, { label: "Update(Proxy)", handler: () => onUpdate(2) },
{ label: "Delete", handler: onDelete }, {
]; label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
]);
const fileModeMenu = [ const fileModeMenu = [
{ label: "Select", handler: onForceSelect }, { label: "Select", handler: onForceSelect },
{ label: "Edit Info", handler: onEditInfo }, { label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile }, { label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile }, { label: "Open File", handler: onOpenFile },
{ label: "Delete", handler: onDelete }, {
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
]; ];
const boxStyle = { const boxStyle = {
@@ -243,6 +265,7 @@ export const ProfileItem = (props: Props) => {
<Typography <Typography
width="calc(100% - 36px)" width="calc(100% - 36px)"
sx={{ fontSize: "18px", fontWeight: "600", lineHeight: "26px" }}
variant="h6" variant="h6"
component="h2" component="h2"
noWrap noWrap
@@ -279,7 +302,11 @@ export const ProfileItem = (props: Props) => {
{ {
<> <>
{description ? ( {description ? (
<Typography noWrap title={description}> <Typography
noWrap
title={description}
sx={{ fontSize: "14px" }}
>
{description} {description}
</Typography> </Typography>
) : ( ) : (
@@ -312,7 +339,7 @@ export const ProfileItem = (props: Props) => {
<span title="Expire Time">{expire}</span> <span title="Expire Time">{expire}</span>
</Box> </Box>
) : ( ) : (
<Box sx={{ ...boxStyle, fontSize: 14, justifyContent: "flex-end" }}> <Box sx={{ ...boxStyle, fontSize: 12, justifyContent: "flex-end" }}>
<span title="Updated Time">{parseExpire(updated)}</span> <span title="Updated Time">{parseExpire(updated)}</span>
</Box> </Box>
)} )}
@@ -336,7 +363,19 @@ export const ProfileItem = (props: Props) => {
<MenuItem <MenuItem
key={item.label} key={item.label}
onClick={item.handler} onClick={item.handler}
sx={{ minWidth: 120 }} sx={[
{
minWidth: 120,
},
(theme) => {
return {
color:
item.label === "Delete"
? theme.palette.error.main
: undefined,
};
},
]}
dense dense
> >
{t(item.label)} {t(item.label)}
@@ -347,9 +386,20 @@ export const ProfileItem = (props: Props) => {
<EditorViewer <EditorViewer
uid={uid} uid={uid}
open={fileOpen} open={fileOpen}
mode="yaml" language="yaml"
schema="clash"
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
<ConfirmViewer
title="Confirm deletion"
message="This operation is not reversible"
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
onDelete();
setConfirmOpen(false);
}}
/>
</Box> </Box>
); );
}; };
+39 -5
View File
@@ -17,6 +17,7 @@ import { Notice } from "@/components/base";
import { EditorViewer } from "./editor-viewer"; import { EditorViewer } from "./editor-viewer";
import { ProfileBox } from "./profile-box"; import { ProfileBox } from "./profile-box";
import { LogViewer } from "./log-viewer"; import { LogViewer } from "./log-viewer";
import { ConfirmViewer } from "./confirm-viewer";
interface Props { interface Props {
selected: boolean; selected: boolean;
@@ -51,6 +52,7 @@ export const ProfileMore = (props: Props) => {
const [anchorEl, setAnchorEl] = useState<any>(null); const [anchorEl, setAnchorEl] = useState<any>(null);
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const [fileOpen, setFileOpen] = useState(false); const [fileOpen, setFileOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
const [logOpen, setLogOpen] = useState(false); const [logOpen, setLogOpen] = useState(false);
const onEditInfo = () => { const onEditInfo = () => {
@@ -87,7 +89,13 @@ export const ProfileMore = (props: Props) => {
{ label: "Open File", handler: onOpenFile }, { label: "Open File", handler: onOpenFile },
{ label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) }, { label: "To Top", show: showMove, handler: fnWrapper(onMoveTop) },
{ label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) }, { label: "To End", show: showMove, handler: fnWrapper(onMoveEnd) },
{ label: "Delete", handler: fnWrapper(onDelete) }, {
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
]; ];
const disableMenu = [ const disableMenu = [
@@ -95,7 +103,13 @@ export const ProfileMore = (props: Props) => {
{ label: "Edit Info", handler: onEditInfo }, { label: "Edit Info", handler: onEditInfo },
{ label: "Edit File", handler: onEditFile }, { label: "Edit File", handler: onEditFile },
{ label: "Open File", handler: onOpenFile }, { label: "Open File", handler: onOpenFile },
{ label: "Delete", handler: fnWrapper(onDelete) }, {
label: "Delete",
handler: () => {
setAnchorEl(null);
setConfirmOpen(true);
},
},
]; ];
const boxStyle = { const boxStyle = {
@@ -200,7 +214,17 @@ export const ProfileMore = (props: Props) => {
<MenuItem <MenuItem
key={item.label} key={item.label}
onClick={item.handler} onClick={item.handler}
sx={{ minWidth: 120 }} sx={[
{ minWidth: 120 },
(theme) => {
return {
color:
item.label === "Delete"
? theme.palette.error.main
: undefined,
};
},
]}
dense dense
> >
{t(item.label)} {t(item.label)}
@@ -211,10 +235,20 @@ export const ProfileMore = (props: Props) => {
<EditorViewer <EditorViewer
uid={uid} uid={uid}
open={fileOpen} open={fileOpen}
mode={type === "merge" ? "yaml" : "javascript"} language={type === "merge" ? "yaml" : "javascript"}
schema={type === "merge" ? "merge" : undefined}
onClose={() => setFileOpen(false)} onClose={() => setFileOpen(false)}
/> />
<ConfirmViewer
title="Confirm deletion"
message="This operation is not reversible"
open={confirmOpen}
onClose={() => setConfirmOpen(false)}
onConfirm={() => {
onDelete();
setConfirmOpen(false);
}}
/>
{selected && ( {selected && (
<LogViewer <LogViewer
open={logOpen} open={logOpen}
+5 -1
View File
@@ -51,7 +51,6 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
desc: "", desc: "",
url: "", url: "",
option: { option: {
// user_agent: "",
with_proxy: false, with_proxy: false,
self_proxy: false, self_proxy: false,
}, },
@@ -98,6 +97,11 @@ export const ProfileViewer = forwardRef<ProfileViewerRef, Props>(
} }
if (form.option?.update_interval) { if (form.option?.update_interval) {
form.option.update_interval = +form.option.update_interval; form.option.update_interval = +form.option.update_interval;
} else {
delete form.option?.update_interval;
}
if (form.option?.user_agent === "") {
delete form.option.user_agent;
} }
const name = form.name || `${form.type} file`; const name = form.name || `${form.type} file`;
const item = { ...form, name }; const item = { ...form, name };
+7 -3
View File
@@ -6,8 +6,8 @@ import {
providerHealthCheck, providerHealthCheck,
updateProxy, updateProxy,
deleteConnection, deleteConnection,
getGroupProxyDelays,
} from "@/services/api"; } from "@/services/api";
import { Box } from "@mui/material";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BaseEmpty } from "../base"; import { BaseEmpty } from "../base";
@@ -33,7 +33,7 @@ export const ProxyGroups = (props: Props) => {
// 切换分组的节点代理 // 切换分组的节点代理
const handleChangeProxy = useLockFn( const handleChangeProxy = useLockFn(
async (group: IProxyGroupItem, proxy: IProxyItem) => { async (group: IProxyGroupItem, proxy: IProxyItem) => {
if (group.type !== "Selector" && group.type !== "Fallback") return; if (!["Selector", "URLTest", "Fallback"].includes(group.type)) return;
const { name, now } = group; const { name, now } = group;
await updateProxy(name, proxy.name); await updateProxy(name, proxy.name);
@@ -85,7 +85,11 @@ export const ProxyGroups = (props: Props) => {
} }
const names = proxies.filter((p) => !p!.provider).map((p) => p!.name); const names = proxies.filter((p) => !p!.provider).map((p) => p!.name);
await delayManager.checkListDelay(names, groupName, timeout);
await Promise.race([
delayManager.checkListDelay(names, groupName, timeout),
getGroupProxyDelays(groupName, delayManager.getUrl(groupName), timeout), // 查询group delays 将清除fixed(不关注调用结果)
]);
onProxies(); onProxies();
}); });
+1 -1
View File
@@ -109,7 +109,7 @@ export const ProxyHead = (props: Props) => {
<IconButton <IconButton
size="small" size="small"
color="inherit" color="inherit"
title={t("Proxy detail")} title={showType ? t("Proxy basic") : t("Proxy detail")}
onClick={() => onHeadState({ showType: !showType })} onClick={() => onHeadState({ showType: !showType })}
> >
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />} {showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
+25 -11
View File
@@ -7,7 +7,7 @@ import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
interface Props { interface Props {
groupName: string; group: IProxyGroupItem;
proxy: IProxyItem; proxy: IProxyItem;
selected: boolean; selected: boolean;
showType?: boolean; showType?: boolean;
@@ -16,7 +16,7 @@ interface Props {
// 多列布局 // 多列布局
export const ProxyItemMini = (props: Props) => { export const ProxyItemMini = (props: Props) => {
const { groupName, proxy, selected, showType = true, onClick } = props; const { group, proxy, selected, showType = true, onClick } = props;
// -1/<=0 为 不显示 // -1/<=0 为 不显示
// -2 为 loading // -2 为 loading
@@ -25,21 +25,21 @@ export const ProxyItemMini = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000; const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => { useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay); delayManager.setListener(proxy.name, group.name, setDelay);
return () => { return () => {
delayManager.removeListener(proxy.name, groupName); delayManager.removeListener(proxy.name, group.name);
}; };
}, [proxy.name, groupName]); }, [proxy.name, group.name]);
useEffect(() => { useEffect(() => {
if (!proxy) return; if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName)); setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]); }, [proxy]);
const onDelay = useLockFn(async () => { const onDelay = useLockFn(async () => {
setDelay(-2); setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout)); setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
}); });
return ( return (
@@ -65,6 +65,13 @@ export const ProxyItemMini = (props: Props) => {
"&:hover .the-check": { display: !showDelay ? "block" : "none" }, "&:hover .the-check": { display: !showDelay ? "block" : "none" },
"&:hover .the-delay": { display: showDelay ? "block" : "none" }, "&:hover .the-delay": { display: showDelay ? "block" : "none" },
"&:hover .the-icon": { display: "none" }, "&:hover .the-icon": { display: "none" },
"& .the-pin, & .the-unpin": {
position: "absolute",
fontSize: "12px",
top: "-5px",
right: "-5px",
},
"& .the-unpin": { filter: "grayscale(1)" },
"&.Mui-selected": { "&.Mui-selected": {
width: `calc(100% + 3px)`, width: `calc(100% + 3px)`,
marginLeft: `-3px`, marginLeft: `-3px`,
@@ -79,7 +86,10 @@ export const ProxyItemMini = (props: Props) => {
}, },
]} ]}
> >
<Box title={proxy.name} sx={{ overflow: "hidden" }}> <Box
title={`${proxy.name}\n${proxy.now ?? ""}`}
sx={{ overflow: "hidden" }}
>
<Typography <Typography
variant="body2" variant="body2"
component="div" component="div"
@@ -147,14 +157,12 @@ export const ProxyItemMini = (props: Props) => {
</Box> </Box>
)} )}
</Box> </Box>
<Box sx={{ ml: 0.5, color: "primary.main" }}> <Box sx={{ ml: 0.5, color: "primary.main" }}>
{delay === -2 && ( {delay === -2 && (
<Widget> <Widget>
<BaseLoading /> <BaseLoading />
</Widget> </Widget>
)} )}
{!proxy.provider && delay !== -2 && ( {!proxy.provider && delay !== -2 && (
// provider的节点不支持检测 // provider的节点不支持检测
<Widget <Widget
@@ -193,7 +201,6 @@ export const ProxyItemMini = (props: Props) => {
{delayManager.formatDelay(delay, timeout)} {delayManager.formatDelay(delay, timeout)}
</Widget> </Widget>
)} )}
{delay !== -2 && delay <= 0 && selected && ( {delay !== -2 && delay <= 0 && selected && (
// 展示已选择的icon // 展示已选择的icon
<CheckCircleOutlineRounded <CheckCircleOutlineRounded
@@ -202,6 +209,13 @@ export const ProxyItemMini = (props: Props) => {
/> />
)} )}
</Box> </Box>
{group.fixed && group.fixed === proxy.name && (
// 展示fixed状态
<span className={proxy.name === group.now ? "the-pin" : "the-unpin"}>
📌
</span>
)}
</ListItemButton> </ListItemButton>
); );
}; };
+7 -7
View File
@@ -17,7 +17,7 @@ import delayManager from "@/services/delay";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
interface Props { interface Props {
groupName: string; group: IProxyGroupItem;
proxy: IProxyItem; proxy: IProxyItem;
selected: boolean; selected: boolean;
showType?: boolean; showType?: boolean;
@@ -44,7 +44,7 @@ const TypeBox = styled(Box)(({ theme }) => ({
})); }));
export const ProxyItem = (props: Props) => { export const ProxyItem = (props: Props) => {
const { groupName, proxy, selected, showType = true, sx, onClick } = props; const { group, proxy, selected, showType = true, sx, onClick } = props;
// -1/<=0 为 不显示 // -1/<=0 为 不显示
// -2 为 loading // -2 为 loading
@@ -52,21 +52,21 @@ export const ProxyItem = (props: Props) => {
const { verge } = useVerge(); const { verge } = useVerge();
const timeout = verge?.default_latency_timeout || 10000; const timeout = verge?.default_latency_timeout || 10000;
useEffect(() => { useEffect(() => {
delayManager.setListener(proxy.name, groupName, setDelay); delayManager.setListener(proxy.name, group.name, setDelay);
return () => { return () => {
delayManager.removeListener(proxy.name, groupName); delayManager.removeListener(proxy.name, group.name);
}; };
}, [proxy.name, groupName]); }, [proxy.name, group.name]);
useEffect(() => { useEffect(() => {
if (!proxy) return; if (!proxy) return;
setDelay(delayManager.getDelayFix(proxy, groupName)); setDelay(delayManager.getDelayFix(proxy, group.name));
}, [proxy]); }, [proxy]);
const onDelay = useLockFn(async () => { const onDelay = useLockFn(async () => {
setDelay(-2); setDelay(-2);
setDelay(await delayManager.checkDelay(proxy.name, groupName, timeout)); setDelay(await delayManager.checkDelay(proxy.name, group.name, timeout));
}); });
return ( return (
+27 -6
View File
@@ -19,6 +19,9 @@ import type { IRenderItem } from "./use-render-list";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { atomThemeMode } from "@/services/states"; import { atomThemeMode } from "@/services/states";
import { useEffect, useState } from "react";
import { convertFileSrc } from "@tauri-apps/api/tauri";
import { downloadIconCache } from "@/services/cmds";
interface RenderProps { interface RenderProps {
item: IRenderItem; item: IRenderItem;
@@ -38,6 +41,24 @@ export const ProxyRender = (props: RenderProps) => {
const [mode] = useRecoilState(atomThemeMode); const [mode] = useRecoilState(atomThemeMode);
const isDark = mode === "light" ? false : true; const isDark = mode === "light" ? false : true;
const itembackgroundcolor = isDark ? "#282A36" : "#ffffff"; const itembackgroundcolor = isDark ? "#282A36" : "#ffffff";
const [iconCachePath, setIconCachePath] = useState("");
useEffect(() => {
initIconCachePath();
}, [group]);
async function initIconCachePath() {
if (group.icon && group.icon.trim().startsWith("http")) {
const fileName =
group.name.replaceAll(" ", "") + "-" + getFileName(group.icon);
const iconPath = await downloadIconCache(group.icon, fileName);
setIconCachePath(convertFileSrc(iconPath));
}
}
function getFileName(url: string) {
return url.substring(url.lastIndexOf("/") + 1);
}
if (type === 0 && !group.hidden) { if (type === 0 && !group.hidden) {
return ( return (
@@ -55,8 +76,8 @@ export const ProxyRender = (props: RenderProps) => {
group.icon && group.icon &&
group.icon.trim().startsWith("http") && ( group.icon.trim().startsWith("http") && (
<img <img
src={group.icon} src={iconCachePath === "" ? group.icon : iconCachePath}
height="32px" width="32px"
style={{ marginRight: "12px", borderRadius: "6px" }} style={{ marginRight: "12px", borderRadius: "6px" }}
/> />
)} )}
@@ -65,7 +86,7 @@ export const ProxyRender = (props: RenderProps) => {
group.icon.trim().startsWith("data") && ( group.icon.trim().startsWith("data") && (
<img <img
src={group.icon} src={group.icon}
height="32px" width="32px"
style={{ marginRight: "12px", borderRadius: "6px" }} style={{ marginRight: "12px", borderRadius: "6px" }}
/> />
)} )}
@@ -74,7 +95,7 @@ export const ProxyRender = (props: RenderProps) => {
group.icon.trim().startsWith("<svg") && ( group.icon.trim().startsWith("<svg") && (
<img <img
src={`data:image/svg+xml;base64,${btoa(group.icon)}`} src={`data:image/svg+xml;base64,${btoa(group.icon)}`}
height="32px" width="32px"
/> />
)} )}
<ListItemText <ListItemText
@@ -121,7 +142,7 @@ export const ProxyRender = (props: RenderProps) => {
if (type === 2 && !group.hidden) { if (type === 2 && !group.hidden) {
return ( return (
<ProxyItem <ProxyItem
groupName={group.name} group={group}
proxy={proxy!} proxy={proxy!}
selected={group.now === proxy?.name} selected={group.now === proxy?.name}
showType={headState?.showType} showType={headState?.showType}
@@ -165,7 +186,7 @@ export const ProxyRender = (props: RenderProps) => {
{proxyCol?.map((proxy) => ( {proxyCol?.map((proxy) => (
<ProxyItemMini <ProxyItemMini
key={item.key + proxy.name} key={item.key + proxy.name}
groupName={group.name} group={group}
proxy={proxy!} proxy={proxy!}
selected={group.now === proxy.name} selected={group.now === proxy.name}
showType={headState?.showType} showType={headState?.showType}
+1 -1
View File
@@ -20,7 +20,7 @@ interface Props {
} }
const parseColor = (text: string) => { const parseColor = (text: string) => {
if (text === "REJECT") return "error.main"; if (text === "REJECT" || text === "REJECT-DROP") return "error.main";
if (text === "DIRECT") return "text.primary"; if (text === "DIRECT") return "text.primary";
let sum = 0; let sum = 0;
@@ -5,6 +5,8 @@ import { List, ListItem, ListItemText, TextField } from "@mui/material";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import getSystem from "@/utils/get-system";
const OS = getSystem();
export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => { export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -13,6 +15,12 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, patchVerge } = useVerge(); const { verge, patchVerge } = useVerge();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [redirPort, setRedirPort] = useState(
verge?.verge_redir_port ?? clashInfo?.redir_port ?? 7895
);
const [tproxyPort, setTproxyPort] = useState(
verge?.verge_tproxy_port ?? clashInfo?.tproxy_port ?? 7896
);
const [mixedPort, setMixedPort] = useState( const [mixedPort, setMixedPort] = useState(
verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897 verge?.verge_mixed_port ?? clashInfo?.mixed_port ?? 7897
); );
@@ -25,6 +33,8 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
if (verge?.verge_redir_port) setRedirPort(verge?.verge_redir_port);
if (verge?.verge_tproxy_port) setTproxyPort(verge?.verge_tproxy_port);
if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port); if (verge?.verge_mixed_port) setMixedPort(verge?.verge_mixed_port);
if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port); if (verge?.verge_socks_port) setSocksPort(verge?.verge_socks_port);
if (verge?.verge_port) setPort(verge?.verge_port); if (verge?.verge_port) setPort(verge?.verge_port);
@@ -35,6 +45,8 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
const onSave = useLockFn(async () => { const onSave = useLockFn(async () => {
if ( if (
redirPort === verge?.verge_redir_port &&
tproxyPort === verge?.verge_tproxy_port &&
mixedPort === verge?.verge_mixed_port && mixedPort === verge?.verge_mixed_port &&
socksPort === verge?.verge_socks_port && socksPort === verge?.verge_socks_port &&
port === verge?.verge_port port === verge?.verge_port
@@ -42,7 +54,34 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
setOpen(false); setOpen(false);
return; return;
} }
if (
OS === "linux" &&
new Set([redirPort, tproxyPort, mixedPort, socksPort, port]).size !== 5
) {
Notice.error("Port conflict!", 4000);
return;
}
if (
OS === "macos" &&
new Set([redirPort, mixedPort, socksPort, port]).size !== 4
) {
Notice.error("Port conflict!", 4000);
return;
}
if (OS === "windows" && new Set([mixedPort, socksPort, port]).size !== 3) {
Notice.error("Port conflict!", 4000);
return;
}
try { try {
if (OS !== "windows") {
await patchInfo({ "redir-port": redirPort });
await patchVerge({ verge_redir_port: redirPort });
}
if (OS === "linux") {
await patchInfo({ "tproxy-port": tproxyPort });
await patchVerge({ verge_tproxy_port: tproxyPort });
}
await patchInfo({ "mixed-port": mixedPort }); await patchInfo({ "mixed-port": mixedPort });
await patchInfo({ "socks-port": socksPort }); await patchInfo({ "socks-port": socksPort });
await patchInfo({ port }); await patchInfo({ port });
@@ -68,6 +107,35 @@ export const ClashPortViewer = forwardRef<DialogRef>((props, ref) => {
onOk={onSave} onOk={onSave}
> >
<List> <List>
{OS !== "windows" && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Redir Port" />
<TextField
size="small"
autoComplete="off"
sx={{ width: 135 }}
value={redirPort}
onChange={(e) =>
setRedirPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
/>
</ListItem>
)}
{OS === "linux" && (
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Tproxy Port" />
<TextField
size="small"
autoComplete="off"
sx={{ width: 135 }}
value={tproxyPort}
onChange={(e) =>
setTproxyPort(+e.target.value?.replace(/\D+/, "").slice(0, 5))
}
/>
</ListItem>
)}
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Mixed Port" /> <ListItemText primary="Mixed Port" />
<TextField <TextField
+31 -19
View File
@@ -7,10 +7,17 @@ import {
} from "react"; } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRecoilValue } from "recoil"; import { useRecoilValue } from "recoil";
import { Chip } from "@mui/material"; import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Chip,
} from "@mui/material";
import { atomThemeMode } from "@/services/states"; import { atomThemeMode } from "@/services/states";
import { getRuntimeYaml } from "@/services/cmds"; import { getRuntimeYaml } from "@/services/cmds";
import { BaseDialog, DialogRef } from "@/components/base"; import { DialogRef } from "@/components/base";
import { editor } from "monaco-editor/esm/vs/editor/editor.api"; import { editor } from "monaco-editor/esm/vs/editor/editor.api";
import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js"; import "monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution.js";
@@ -47,9 +54,12 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
instanceRef.current = editor.create(editorRef.current, { instanceRef.current = editor.create(editorRef.current, {
value: data ?? "# Error\n", value: data ?? "# Error\n",
language: "yaml", language: "yaml",
tabSize: 2,
theme: themeMode === "light" ? "vs" : "vs-dark", theme: themeMode === "light" ? "vs" : "vs-dark",
minimap: { enabled: false }, minimap: { enabled: dom.clientWidth >= 1000 }, // 超过一定宽度显示minimap滚动条
readOnly: true, mouseWheelZoom: true, // Ctrl+滚轮调节缩放
readOnly: true, // 只读
readOnlyMessage: { value: t("ReadOnlyMessage") },
}); });
}); });
}, },
@@ -57,20 +67,22 @@ export const ConfigViewer = forwardRef<DialogRef>((props, ref) => {
})); }));
return ( return (
<BaseDialog <Dialog open={open} onClose={() => setOpen(false)} maxWidth="xl" fullWidth>
open={open} <DialogTitle>
title={ {t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
<> </DialogTitle>
{t("Runtime Config")} <Chip label={t("ReadOnly")} size="small" />
</> <DialogContent
} sx={{ width: "94%", height: "100vh", pb: 1, userSelect: "text" }}
contentSx={{ width: 520, pb: 1, userSelect: "text" }} >
cancelBtn={t("Back")} <div style={{ width: "100%", height: "100%" }} ref={editorRef} />
disableOk </DialogContent>
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} <DialogActions>
> <Button onClick={() => setOpen(false)} variant="outlined">
<div style={{ width: "100%", height: "420px" }} ref={editorRef} /> {t("Back")}
</BaseDialog> </Button>
</DialogActions>
</Dialog>
); );
}); });
@@ -46,7 +46,7 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
> >
<List> <List>
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="External Controller" /> <ListItemText primary={t("External Controller")} />
<TextField <TextField
size="small" size="small"
autoComplete="off" autoComplete="off"
@@ -58,13 +58,13 @@ export const ControllerViewer = forwardRef<DialogRef>((props, ref) => {
</ListItem> </ListItem>
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary="Core Secret" /> <ListItemText primary={t("Core Secret")} />
<TextField <TextField
size="small" size="small"
autoComplete="off" autoComplete="off"
sx={{ width: 175 }} sx={{ width: 175 }}
value={secret} value={secret}
placeholder="Recommended" placeholder={t("Recommended")}
onChange={(e) => setSecret(e.target.value)} onChange={(e) => setSecret(e.target.value)}
/> />
</ListItem> </ListItem>
+31 -13
View File
@@ -9,6 +9,7 @@ import { open as openDialog } from "@tauri-apps/api/dialog";
import { convertFileSrc } from "@tauri-apps/api/tauri"; import { convertFileSrc } from "@tauri-apps/api/tauri";
import { copyIconFile, getAppDir } from "@/services/cmds"; import { copyIconFile, getAppDir } from "@/services/cmds";
import { join } from "@tauri-apps/api/path"; import { join } from "@tauri-apps/api/path";
import { exists } from "@tauri-apps/api/fs";
export const LayoutViewer = forwardRef<DialogRef>((props, ref) => { export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -19,7 +20,6 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
const [sysproxyIcon, setSysproxyIcon] = useState(""); const [sysproxyIcon, setSysproxyIcon] = useState("");
const [tunIcon, setTunIcon] = useState(""); const [tunIcon, setTunIcon] = useState("");
// const { menu_icon } = verge ?? {};
useEffect(() => { useEffect(() => {
initIconPath(); initIconPath();
}, []); }, []);
@@ -27,12 +27,27 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
async function initIconPath() { async function initIconPath() {
const appDir = await getAppDir(); const appDir = await getAppDir();
const icon_dir = await join(appDir, "icons"); const icon_dir = await join(appDir, "icons");
const common_icon = await join(icon_dir, "common.png"); const common_icon_png = await join(icon_dir, "common.png");
const sysproxy_icon = await join(icon_dir, "sysproxy.png"); const common_icon_ico = await join(icon_dir, "common.ico");
const tun_icon = await join(icon_dir, "tun.png"); const sysproxy_icon_png = await join(icon_dir, "sysproxy.png");
setCommonIcon(common_icon); const sysproxy_icon_ico = await join(icon_dir, "sysproxy.ico");
setSysproxyIcon(sysproxy_icon); const tun_icon_png = await join(icon_dir, "tun.png");
setTunIcon(tun_icon); const tun_icon_ico = await join(icon_dir, "tun.ico");
if (await exists(common_icon_ico)) {
setCommonIcon(common_icon_ico);
} else {
setCommonIcon(common_icon_png);
}
if (await exists(sysproxy_icon_ico)) {
setSysproxyIcon(sysproxy_icon_ico);
} else {
setSysproxyIcon(sysproxy_icon_png);
}
if (await exists(tun_icon_ico)) {
setTunIcon(tun_icon_ico);
} else {
setTunIcon(tun_icon_png);
}
} }
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -141,12 +156,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
filters: [ filters: [
{ {
name: "Tray Icon Image", name: "Tray Icon Image",
extensions: ["png"], extensions: ["png", "ico"],
}, },
], ],
}); });
if (path?.length) { if (path?.length) {
await copyIconFile(`${path}`, "common.png"); await copyIconFile(`${path}`, "common");
await initIconPath();
onChangeData({ common_tray_icon: true }); onChangeData({ common_tray_icon: true });
patchVerge({ common_tray_icon: true }); patchVerge({ common_tray_icon: true });
} }
@@ -185,12 +201,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
filters: [ filters: [
{ {
name: "Tray Icon Image", name: "Tray Icon Image",
extensions: ["png"], extensions: ["png", "ico"],
}, },
], ],
}); });
if (path?.length) { if (path?.length) {
await copyIconFile(`${path}`, "sysproxy.png"); await copyIconFile(`${path}`, "sysproxy");
await initIconPath();
onChangeData({ sysproxy_tray_icon: true }); onChangeData({ sysproxy_tray_icon: true });
patchVerge({ sysproxy_tray_icon: true }); patchVerge({ sysproxy_tray_icon: true });
} }
@@ -227,12 +244,13 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
filters: [ filters: [
{ {
name: "Tray Icon Image", name: "Tray Icon Image",
extensions: ["png"], extensions: ["png", "ico"],
}, },
], ],
}); });
if (path?.length) { if (path?.length) {
await copyIconFile(`${path}`, "tun.png"); await copyIconFile(`${path}`, "tun");
await initIconPath();
onChangeData({ tun_tray_icon: true }); onChangeData({ tun_tray_icon: true });
patchVerge({ tun_tray_icon: true }); patchVerge({ tun_tray_icon: true });
} }
@@ -86,12 +86,15 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
disableFooter disableFooter
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
> >
<Typography>Current State: {state}</Typography> <Typography>
{t("Current State")}: {t(state)}
</Typography>
{(state === "unknown" || state === "uninstall") && ( {(state === "unknown" || state === "uninstall") && (
<Typography> <Typography>
Information: Please make sure that the Clash Verge Service is {t(
installed and enabled "Information: Please make sure that the Clash Verge Service is installed and enabled"
)}
</Typography> </Typography>
)} )}
@@ -102,19 +105,19 @@ export const ServiceViewer = forwardRef<DialogRef, Props>((props, ref) => {
> >
{state === "uninstall" && enable && ( {state === "uninstall" && enable && (
<Button variant="contained" onClick={onDisable}> <Button variant="contained" onClick={onDisable}>
Disable Service Mode {t("Disable Service Mode")}
</Button> </Button>
)} )}
{state === "uninstall" && ( {state === "uninstall" && (
<Button variant="contained" onClick={onInstall}> <Button variant="contained" onClick={onInstall}>
Install {t("Install")}
</Button> </Button>
)} )}
{(state === "active" || state === "installed") && ( {(state === "active" || state === "installed") && (
<Button variant="outlined" onClick={onUninstall}> <Button variant="outlined" onClick={onUninstall}>
Uninstall {t("Uninstall")}
</Button> </Button>
)} )}
</Stack> </Stack>
@@ -128,6 +128,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
rows={3} rows={3}
sx={{ width: 280 }} sx={{ width: 280 }}
value={value.bypass} value={value.bypass}
placeholder={sysproxy?.bypass || `-`}
onChange={(e) => onChange={(e) =>
setValue((v) => ({ ...v, bypass: e.target.value })) setValue((v) => ({ ...v, bypass: e.target.value }))
} }
+15 -8
View File
@@ -13,6 +13,7 @@ import {
import { useClash } from "@/hooks/use-clash"; import { useClash } from "@/hooks/use-clash";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { StackModeSwitch } from "./stack-mode-switch"; import { StackModeSwitch } from "./stack-mode-switch";
import { enhanceProfiles } from "@/services/cmds";
export const TunViewer = forwardRef<DialogRef>((props, ref) => { export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -27,7 +28,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
autoDetectInterface: true, autoDetectInterface: true,
dnsHijack: ["any:53"], dnsHijack: ["any:53"],
strictRoute: false, strictRoute: false,
mtu: 9000, mtu: 1500,
}); });
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -40,7 +41,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true, autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"], dnsHijack: clash?.tun["dns-hijack"] ?? ["any:53"],
strictRoute: clash?.tun["strict-route"] ?? false, strictRoute: clash?.tun["strict-route"] ?? false,
mtu: clash?.tun.mtu ?? 9000, mtu: clash?.tun.mtu ?? 1500,
}); });
}, },
close: () => setOpen(false), close: () => setOpen(false),
@@ -50,12 +51,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
try { try {
let tun = { let tun = {
stack: values.stack, stack: values.stack,
device: values.device, device: values.device === "" ? "Meta" : values.device,
"auto-route": values.autoRoute, "auto-route": values.autoRoute,
"auto-detect-interface": values.autoDetectInterface, "auto-detect-interface": values.autoDetectInterface,
"dns-hijack": values.dnsHijack, "dns-hijack": values.dnsHijack[0] === "" ? [] : values.dnsHijack,
"strict-route": values.strictRoute, "strict-route": values.strictRoute,
mtu: values.mtu, mtu: values.mtu ?? 1500,
}; };
await patchClash({ tun }); await patchClash({ tun });
await mutateClash( await mutateClash(
@@ -65,6 +66,12 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
}), }),
false false
); );
try {
await enhanceProfiles();
Notice.success("Refresh clash config", 1000);
} catch (err: any) {
Notice.error(err.message || err.toString(), 3000);
}
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
@@ -88,7 +95,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
"auto-detect-interface": true, "auto-detect-interface": true,
"dns-hijack": ["any:53"], "dns-hijack": ["any:53"],
"strict-route": false, "strict-route": false,
mtu: 9000, mtu: 1500,
}; };
setValues({ setValues({
stack: "gvisor", stack: "gvisor",
@@ -97,7 +104,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
autoDetectInterface: true, autoDetectInterface: true,
dnsHijack: ["any:53"], dnsHijack: ["any:53"],
strictRoute: false, strictRoute: false,
mtu: 9000, mtu: 1500,
}); });
await patchClash({ tun }); await patchClash({ tun });
await mutateClash( await mutateClash(
@@ -208,7 +215,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
spellCheck="false" spellCheck="false"
sx={{ width: 250 }} sx={{ width: 250 }}
value={values.mtu} value={values.mtu}
placeholder="9000" placeholder="1500"
onChange={(e) => onChange={(e) =>
setValues((v) => ({ setValues((v) => ({
...v, ...v,
+20 -12
View File
@@ -1,8 +1,7 @@
import useSWR from "swr"; import useSWR from "swr";
import snarkdown from "snarkdown";
import { forwardRef, useImperativeHandle, useState, useMemo } from "react"; import { forwardRef, useImperativeHandle, useState, useMemo } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, LinearProgress, styled } from "@mui/material"; import { Box, LinearProgress } from "@mui/material";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { relaunch } from "@tauri-apps/api/process"; import { relaunch } from "@tauri-apps/api/process";
@@ -11,10 +10,8 @@ import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { atomUpdateState } from "@/services/states"; import { atomUpdateState } from "@/services/states";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event"; import { listen, Event, UnlistenFn } from "@tauri-apps/api/event";
import { portableFlag } from "@/pages/_layout"; import { portableFlag } from "@/pages/_layout";
import ReactMarkdown from "react-markdown";
const UpdateLog = styled(Box)(() => ({
"h1,h2,h3,ul,ol,p": { margin: "0.5em 0", color: "inherit" },
}));
let eventListener: UnlistenFn | null = null; let eventListener: UnlistenFn | null = null;
export const UpdateViewer = forwardRef<DialogRef>((props, ref) => { export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
@@ -38,12 +35,11 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
close: () => setOpen(false), close: () => setOpen(false),
})); }));
// markdown parser const markdownContent = useMemo(() => {
const parseContent = useMemo(() => {
if (!updateInfo?.manifest?.body) { if (!updateInfo?.manifest?.body) {
return "New Version is available"; return "New Version is available";
} }
return snarkdown(updateInfo?.manifest?.body); return updateInfo?.manifest?.body;
}, [updateInfo]); }, [updateInfo]);
const onUpdate = useLockFn(async () => { const onUpdate = useLockFn(async () => {
@@ -87,10 +83,22 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onUpdate} onOk={onUpdate}
> >
<UpdateLog <Box sx={{ height: "calc(100% - 10px)", overflow: "auto" }}>
dangerouslySetInnerHTML={{ __html: parseContent }} <ReactMarkdown
sx={{ height: "calc(100% - 10px)", overflow: "auto" }} components={{
/> a: ({ node, ...props }) => {
const { children } = props;
return (
<a {...props} target="_blank">
{children}
</a>
);
},
}}
>
{markdownContent}
</ReactMarkdown>
</Box>
{updateState && ( {updateState && (
<LinearProgress <LinearProgress
variant="buffer" variant="buffer"
@@ -90,6 +90,8 @@ export const WebUIItem = (props: Props) => {
title={value} title={value}
color={value ? "text.primary" : "text.secondary"} color={value ? "text.primary" : "text.secondary"}
sx={({ palette }) => ({ sx={({ palette }) => ({
overflow: "hidden",
textOverflow: "ellipsis",
"> span": { "> span": {
color: palette.primary.main, color: palette.primary.main,
}, },
+36 -51
View File
@@ -17,23 +17,17 @@ interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
} }
const isWIN = getSystem() === "windows";
const SettingSystem = ({ onError }: Props) => { const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge(); const { verge, mutateVerge, patchVerge } = useVerge();
// service mode // service mode
const { data: serviceStatus } = useSWR( const { data: serviceStatus } = useSWR("checkService", checkService, {
isWIN ? "checkService" : null, revalidateIfStale: false,
checkService, shouldRetryOnError: false,
{ focusThrottleInterval: 36e5, // 1 hour
revalidateIfStale: false, });
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const serviceRef = useRef<DialogRef>(null); const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null); const sysproxyRef = useRef<DialogRef>(null);
@@ -56,20 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}> <SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} /> <SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} /> <TunViewer ref={tunRef} />
{isWIN && ( <ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
)}
<SettingItem <SettingItem
label={t("Tun Mode")} label={t("Tun Mode")}
extra={ extra={
<> <>
<Tooltip <Tooltip title={t("Tun Mode Info")} placement="top">
title={
isWIN ? t("Tun Mode Info Windows") : t("Tun Mode Info Unix")
}
placement="top"
>
<IconButton color="inherit" size="small"> <IconButton color="inherit" size="small">
<InfoRounded <InfoRounded
fontSize="inherit" fontSize="inherit"
@@ -102,39 +89,37 @@ const SettingSystem = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingItem> </SettingItem>
{isWIN && ( <SettingItem
<SettingItem label={t("Service Mode")}
label={t("Service Mode")} extra={
extra={ <IconButton
<IconButton color="inherit"
color="inherit" size="small"
size="small" onClick={() => serviceRef.current?.open()}
onClick={() => serviceRef.current?.open()}
>
<PrivacyTipRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
}
>
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
> >
<Switch <PrivacyTipRounded
edge="end" fontSize="inherit"
disabled={ style={{ cursor: "pointer", opacity: 0.75 }}
serviceStatus !== "active" && serviceStatus !== "installed"
}
/> />
</GuardState> </IconButton>
</SettingItem> }
)} >
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
>
<Switch
edge="end"
disabled={
serviceStatus !== "active" && serviceStatus !== "installed"
}
/>
</GuardState>
</SettingItem>
<SettingItem <SettingItem
label={t("System Proxy")} label={t("System Proxy")}
+22 -3
View File
@@ -10,7 +10,13 @@ import {
Input, Input,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { exitApp, openAppDir, openCoreDir, openLogsDir } from "@/services/cmds"; import {
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
} from "@/services/cmds";
import { ArrowForward } from "@mui/icons-material"; import { ArrowForward } from "@mui/icons-material";
import { checkUpdate } from "@tauri-apps/api/updater"; import { checkUpdate } from "@tauri-apps/api/updater";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
@@ -27,6 +33,7 @@ import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer"; import { UpdateViewer } from "./mods/update-viewer";
import getSystem from "@/utils/get-system"; import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers"; import { routers } from "@/pages/_routers";
interface Props { interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
} }
@@ -90,6 +97,7 @@ const SettingVerge = ({ onError }: Props) => {
<MenuItem value="zh">中文</MenuItem> <MenuItem value="zh">中文</MenuItem>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English</MenuItem>
<MenuItem value="ru">Русский</MenuItem> <MenuItem value="ru">Русский</MenuItem>
<MenuItem value="fa">فارسی</MenuItem>
</Select> </Select>
</GuardState> </GuardState>
</SettingItem> </SettingItem>
@@ -149,8 +157,8 @@ const SettingVerge = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ start_page: e })} onGuard={(e) => patchVerge({ start_page: e })}
> >
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}> <Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; link: string }) => { {routers.map((page: { label: string; path: string }) => {
return <MenuItem value={page.link}>{t(page.label)}</MenuItem>; return <MenuItem value={page.path}>{t(page.label)}</MenuItem>;
})} })}
</Select> </Select>
</GuardState> </GuardState>
@@ -304,6 +312,17 @@ const SettingVerge = ({ onError }: Props) => {
</IconButton> </IconButton>
</SettingItem> </SettingItem>
<SettingItem label={t("Open Dev Tools")}>
<IconButton
color="inherit"
size="small"
sx={{ my: "2px" }}
onClick={openDevTools}
>
<ArrowForward />
</IconButton>
</SettingItem>
<SettingItem label={t("Exit")}> <SettingItem label={t("Exit")}>
<IconButton <IconButton
color="inherit" color="inherit"
+24 -4
View File
@@ -17,8 +17,9 @@ import { LanguageTwoTone } from "@mui/icons-material";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { TestBox } from "./test-box"; import { TestBox } from "./test-box";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
import { cmdTestDelay } from "@/services/cmds"; import { cmdTestDelay, downloadIconCache } from "@/services/cmds";
import { listen, Event, UnlistenFn } from "@tauri-apps/api/event"; import { listen, UnlistenFn } from "@tauri-apps/api/event";
import { convertFileSrc } from "@tauri-apps/api/tauri";
interface Props { interface Props {
id: string; id: string;
@@ -39,6 +40,23 @@ export const TestItem = (props: Props) => {
const [position, setPosition] = useState({ left: 0, top: 0 }); const [position, setPosition] = useState({ left: 0, top: 0 });
const [delay, setDelay] = useState(-1); const [delay, setDelay] = useState(-1);
const { uid, name, icon, url } = itemData; const { uid, name, icon, url } = itemData;
const [iconCachePath, setIconCachePath] = useState("");
useEffect(() => {
initIconCachePath();
}, [icon]);
async function initIconCachePath() {
if (icon && icon.trim().startsWith("http")) {
const fileName = uid + "-" + getFileName(icon);
const iconPath = await downloadIconCache(icon, fileName);
setIconCachePath(convertFileSrc(iconPath));
}
}
function getFileName(url: string) {
return url.substring(url.lastIndexOf("/") + 1);
}
const onDelay = async () => { const onDelay = async () => {
setDelay(-2); setDelay(-2);
@@ -86,7 +104,6 @@ export const TestItem = (props: Props) => {
}} }}
> >
<TestBox <TestBox
onClick={onEditTest}
onContextMenu={(event) => { onContextMenu={(event) => {
const { clientX, clientY } = event; const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX }); setPosition({ top: clientY, left: clientX });
@@ -104,7 +121,10 @@ export const TestItem = (props: Props) => {
{icon && icon.trim() !== "" ? ( {icon && icon.trim() !== "" ? (
<Box sx={{ display: "flex", justifyContent: "center" }}> <Box sx={{ display: "flex", justifyContent: "center" }}>
{icon.trim().startsWith("http") && ( {icon.trim().startsWith("http") && (
<img src={icon} height="40px" /> <img
src={iconCachePath === "" ? icon : iconCachePath}
height="40px"
/>
)} )}
{icon.trim().startsWith("data") && ( {icon.trim().startsWith("data") && (
<img src={icon} height="40px" /> <img src={icon} height="40px" />
+29 -1
View File
@@ -49,11 +49,19 @@ export const useClashInfo = () => {
patch: Partial< patch: Partial<
Pick< Pick<
IConfigData, IConfigData,
"port" | "socks-port" | "mixed-port" | "external-controller" | "secret" | "port"
| "socks-port"
| "mixed-port"
| "redir-port"
| "tproxy-port"
| "external-controller"
| "secret"
> >
> >
) => { ) => {
const hasInfo = const hasInfo =
patch["redir-port"] != null ||
patch["tproxy-port"] != null ||
patch["mixed-port"] != null || patch["mixed-port"] != null ||
patch["socks-port"] != null || patch["socks-port"] != null ||
patch["port"] != null || patch["port"] != null ||
@@ -62,6 +70,26 @@ export const useClashInfo = () => {
if (!hasInfo) return; if (!hasInfo) return;
if (patch["redir-port"]) {
const port = patch["redir-port"];
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
}
}
if (patch["tproxy-port"]) {
const port = patch["tproxy-port"];
if (port < 1000) {
throw new Error("The port should not < 1000");
}
if (port > 65536) {
throw new Error("The port should not > 65536");
}
}
if (patch["mixed-port"]) { if (patch["mixed-port"]) {
const port = patch["mixed-port"]; const port = patch["mixed-port"];
if (port < 1000) { if (port < 1000) {
+11 -4
View File
@@ -5,7 +5,8 @@ export type WsMsgFn = (event: MessageEvent<any>) => void;
export interface WsOptions { export interface WsOptions {
errorCount?: number; // default is 5 errorCount?: number; // default is 5
retryInterval?: number; // default is 2500 retryInterval?: number; // default is 2500
onError?: () => void; onError?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
} }
export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => { export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
@@ -33,17 +34,23 @@ export const useWebsocket = (onMessage: WsMsgFn, options?: WsOptions) => {
const ws = new WebSocket(url); const ws = new WebSocket(url);
wsRef.current = ws; wsRef.current = ws;
ws.addEventListener("message", onMessage); ws.addEventListener("message", (event) => {
ws.addEventListener("error", () => { errorCount = 0; // reset counter
onMessage(event);
});
ws.addEventListener("error", (event) => {
errorCount -= 1; errorCount -= 1;
if (errorCount >= 0) { if (errorCount >= 0) {
timerRef.current = setTimeout(connectHelper, 2500); timerRef.current = setTimeout(connectHelper, 2500);
} else { } else {
disconnect(); disconnect();
options?.onError?.(); options?.onError?.(event);
} }
}); });
ws.addEventListener("close", (event) => {
options?.onClose?.(event);
});
}; };
connectHelper(); connectHelper();
+35 -6
View File
@@ -18,6 +18,8 @@
"direct": "direct", "direct": "direct",
"script": "script", "script": "script",
"Create Test": "Create Test",
"Edit Test": "Edit Test",
"Edit": "Edit", "Edit": "Edit",
"Icon": "Icon", "Icon": "Icon",
"Test URL": "Test URL", "Test URL": "Test URL",
@@ -29,7 +31,9 @@
"New": "New", "New": "New",
"Create Profile": "Create Profile", "Create Profile": "Create Profile",
"Choose File": "Choose File", "Choose File": "Choose File",
"Close": "Close",
"Close All": "Close All", "Close All": "Close All",
"Home": "Home",
"Select": "Select", "Select": "Select",
"Edit Info": "Edit Info", "Edit Info": "Edit Info",
"Edit File": "Edit File", "Edit File": "Edit File",
@@ -45,6 +49,8 @@
"Update All Profiles": "Update All Profiles", "Update All Profiles": "Update All Profiles",
"View Runtime Config": "View Runtime Config", "View Runtime Config": "View Runtime Config",
"Reactivate Profiles": "Reactivate Profiles", "Reactivate Profiles": "Reactivate Profiles",
"Confirm deletion": "Confirm deletion",
"This operation is not reversible": "This operation is not reversible",
"Location": "Location", "Location": "Location",
"Delay check": "Delay check", "Delay check": "Delay check",
@@ -52,6 +58,7 @@
"Sort by delay": "Sort by delay", "Sort by delay": "Sort by delay",
"Sort by name": "Sort by name", "Sort by name": "Sort by name",
"Delay check URL": "Delay check URL", "Delay check URL": "Delay check URL",
"Proxy basic": "Proxy basic",
"Proxy detail": "Proxy detail", "Proxy detail": "Proxy detail",
"Filter": "Filter", "Filter": "Filter",
"Filter conditions": "Filter conditions", "Filter conditions": "Filter conditions",
@@ -80,6 +87,9 @@
"Random Port": "Random Port", "Random Port": "Random Port",
"After restart to take effect": "After restart to take effect", "After restart to take effect": "After restart to take effect",
"External": "External", "External": "External",
"External Controller": "External Controller",
"Core Secret": "Core Secret",
"Recommended": "Recommended",
"Clash Core": "Clash Core", "Clash Core": "Clash Core",
"Grant": "Grant", "Grant": "Grant",
"Tun mode requires": "Tun mode requires", "Tun mode requires": "Tun mode requires",
@@ -94,10 +104,10 @@
"Proxy Guard": "Proxy Guard", "Proxy Guard": "Proxy Guard",
"Guard Duration": "Guard Duration", "Guard Duration": "Guard Duration",
"Proxy Bypass": "Proxy Bypass", "Proxy Bypass": "Proxy Bypass",
"Current System Proxy": "Current System Proxy",
"Enable status": "Enable status", "Enable status": "Enable status",
"Server Addr": "Server Addr", "Server Addr": "Server Addr",
"Bypass": "Bypass", "Bypass": "Bypass",
"Current System Proxy": "Current System Proxy",
"Theme Mode": "Theme Mode", "Theme Mode": "Theme Mode",
"Tray Click Event": "Tray Click Event", "Tray Click Event": "Tray Click Event",
"Copy Env Type": "Copy Env Type", "Copy Env Type": "Copy Env Type",
@@ -112,6 +122,9 @@
"Traffic Graph": "Traffic Graph", "Traffic Graph": "Traffic Graph",
"Memory Usage": "Memory Usage", "Memory Usage": "Memory Usage",
"Proxy Group Icon": "Proxy Group Icon", "Proxy Group Icon": "Proxy Group Icon",
"Menu Icon": "Menu Icon",
"Monochrome": "Monochrome",
"Colorful": "Colorful",
"Common Tray Icon": "Common Tray Icon", "Common Tray Icon": "Common Tray Icon",
"System Proxy Tray Icon": "System Proxy Tray Icon", "System Proxy Tray Icon": "System Proxy Tray Icon",
"Tun Tray Icon": "Tun Tray Icon", "Tun Tray Icon": "Tun Tray Icon",
@@ -120,6 +133,7 @@
"Open Core Dir": "Open Core Dir", "Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir", "Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates", "Check for Updates": "Check for Updates",
"Open Dev Tools": "Open Dev Tools",
"Verge Version": "Verge Version", "Verge Version": "Verge Version",
"theme.light": "Light", "theme.light": "Light",
"theme.dark": "Dark", "theme.dark": "Dark",
@@ -127,6 +141,7 @@
"Clash Field": "Clash Field", "Clash Field": "Clash Field",
"Runtime Config": "Runtime Config", "Runtime Config": "Runtime Config",
"ReadOnly": "ReadOnly", "ReadOnly": "ReadOnly",
"ReadOnlyMessage": "Cannot edit in read-only editor",
"Restart": "Restart", "Restart": "Restart",
"Upgrade": "Upgrade", "Upgrade": "Upgrade",
@@ -134,6 +149,7 @@
"Save": "Save", "Save": "Save",
"Cancel": "Cancel", "Cancel": "Cancel",
"Exit": "Exit", "Exit": "Exit",
"Confirm": "Confirm",
"Default": "Default", "Default": "Default",
"Download Speed": "Download Speed", "Download Speed": "Download Speed",
@@ -148,11 +164,11 @@
"App Log Level": "App Log Level", "App Log Level": "App Log Level",
"Auto Close Connections": "Auto Close Connections", "Auto Close Connections": "Auto Close Connections",
"Enable Clash Fields Filter": "Enable Clash Fields Filter", "Auto Check Update": "Auto Check Update",
"Enable Builtin Enhanced": "Enable Builtin Enhanced", "Enable Builtin Enhanced": "Enable Builtin Enhanced",
"Proxy Layout Column": "Proxy Layout Column", "Proxy Layout Column": "Proxy Layout Column",
"Default Latency Test": "Default Latency Test", "Default Latency Test": "Default Latency Test",
"Defaule Latency Timeout": "Defaule Latency Timeout", "Default Latency Timeout": "Default Latency Timeout",
"Auto Log Clean": "Auto Log Clean", "Auto Log Clean": "Auto Log Clean",
"Never Clean": "Never Clean", "Never Clean": "Never Clean",
@@ -167,9 +183,22 @@
"Auto Detect Interface": "Auto Detect Interface", "Auto Detect Interface": "Auto Detect Interface",
"DNS Hijack": "DNS Hijack", "DNS Hijack": "DNS Hijack",
"MTU": "Max Transmission Unit", "MTU": "Max Transmission Unit",
"Reset to Default": "Reset to Default",
"Current State": "Current State",
"pending": "pending",
"installed": "installed",
"uninstall": "uninstalled",
"active": "active",
"unknown": "unknown",
"Disable Service Mode": "Disable Service Mode",
"Install": "Install",
"Uninstall": "Uninstall",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it", "Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
"Tun Mode Info Windows": "The Tun mode requires granting core-related permissions. Please enable service mode before using it", "Tun Mode Info": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings", "System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode" "Information: Please make sure that the Clash Verge Service is installed and enabled": "Information: Please make sure that the Clash Verge Service is installed and enabled",
"Use Regular Expression": "Use Regular Expression"
} }
+1
View File
@@ -0,0 +1 @@
{}
+75 -3
View File
@@ -11,13 +11,15 @@
"Logs": "Логи", "Logs": "Логи",
"Clear": "Очистить", "Clear": "Очистить",
"Proxies": "Прокси", "Proxies": "Прокси",
"Test": "Тест",
"Proxy Groups": "Группы прокси", "Proxy Groups": "Группы прокси",
"Test": "Тест",
"rule": "правила", "rule": "правила",
"global": "глобальный", "global": "глобальный",
"direct": "прямой", "direct": "прямой",
"script": "скриптовый", "script": "скриптовый",
"Create Test": "Создать тест",
"Edit Test": "Редактировать тест",
"Edit": "Редактировать", "Edit": "Редактировать",
"Icon": "Икона", "Icon": "Икона",
"Test URL": "Тестовый URL", "Test URL": "Тестовый URL",
@@ -29,7 +31,9 @@
"New": "Новый", "New": "Новый",
"Create Profile": "Создать профиль", "Create Profile": "Создать профиль",
"Choose File": "Выбрать файл", "Choose File": "Выбрать файл",
"Close": "Закрыть",
"Close All": "Закрыть всё", "Close All": "Закрыть всё",
"Home": "Главная",
"Select": "Выбрать", "Select": "Выбрать",
"Edit Info": "Изменить информацию", "Edit Info": "Изменить информацию",
"Edit File": "Изменить файл", "Edit File": "Изменить файл",
@@ -45,6 +49,8 @@
"Update All Profiles": "Обновить все профили", "Update All Profiles": "Обновить все профили",
"View Runtime Config": "Просмотреть используемый конфиг", "View Runtime Config": "Просмотреть используемый конфиг",
"Reactivate Profiles": "Реактивировать профили", "Reactivate Profiles": "Реактивировать профили",
"Confirm deletion": "Подтвердите удаление",
"This operation is not reversible": "Эта операция необратима",
"Location": "Местоположение", "Location": "Местоположение",
"Delay check": "Проверка задержки", "Delay check": "Проверка задержки",
@@ -52,6 +58,7 @@
"Sort by delay": "Сортировать по задержке", "Sort by delay": "Сортировать по задержке",
"Sort by name": "Сортировать по названию", "Sort by name": "Сортировать по названию",
"Delay check URL": "URL проверки задержки", "Delay check URL": "URL проверки задержки",
"Proxy basic": "Резюме о прокси",
"Proxy detail": "Подробности о прокси", "Proxy detail": "Подробности о прокси",
"Filter": "Фильтр", "Filter": "Фильтр",
"Filter conditions": "Условия фильтрации", "Filter conditions": "Условия фильтрации",
@@ -79,7 +86,13 @@
"Port Config": "Настройка порта", "Port Config": "Настройка порта",
"Random Port": "Случайный порт", "Random Port": "Случайный порт",
"After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение", "After restart to take effect": "Чтобы изменения вступили в силу, необходимо перезапустить приложение",
"External": "Внешний",
"External Controller": "Адрес прослушивания внешнего контроллера",
"Core Secret": "Секрет",
"Recommended": "Рекомендуется",
"Clash Core": "Ядро Clash", "Clash Core": "Ядро Clash",
"Grant": "Предоставить",
"Tun mode requires": "Требуется Режим туннеля",
"Tun Mode": "Режим туннеля", "Tun Mode": "Режим туннеля",
"Service Mode": "Режим сервиса", "Service Mode": "Режим сервиса",
"Auto Launch": "Автозапуск", "Auto Launch": "Автозапуск",
@@ -92,20 +105,35 @@
"Guard Duration": "Период защиты", "Guard Duration": "Период защиты",
"Proxy Bypass": "Игнорирование прокси", "Proxy Bypass": "Игнорирование прокси",
"Current System Proxy": "Текущий системный прокси", "Current System Proxy": "Текущий системный прокси",
"Enable status": "Статус включения",
"Server Addr": "Адрес сервера",
"Bypass": "Игнорирование",
"Theme Mode": "Режим темы", "Theme Mode": "Режим темы",
"Tray Click Event": "Событие щелчка в лотке", "Tray Click Event": "Событие щелчка в лотке",
"Start Page": "Главная страница",
"Copy Env Type": "Скопировать тип Env", "Copy Env Type": "Скопировать тип Env",
"Start Page": "Главная страница",
"Startup Script": "Скрипт запуска", "Startup Script": "Скрипт запуска",
"Browse": "Просмотреть", "Browse": "Просмотреть",
"Show Main Window": "Показать главное окно", "Show Main Window": "Показать главное окно",
"Theme Setting": "Настройка темы", "Theme Setting": "Настройка темы",
"Layout Setting": "Настройка раскладки",
"Miscellaneous": "Настройка различные",
"Hotkey Setting": "Настройка клавиатурных сокращений", "Hotkey Setting": "Настройка клавиатурных сокращений",
"Traffic Graph": "График трафика", "Traffic Graph": "График трафика",
"Memory Usage": "Использование памяти",
"Proxy Group Icon": "Иконка Группы прокси",
"Menu Icon": "Иконка меню",
"Monochrome": "Монохромный",
"Colorful": "Полноцветный",
"Common Tray Icon": "Общий значок в лотке",
"System Proxy Tray Icon": "Значок системного прокси в лотке",
"Tun Tray Icon": "Значок туннеля в лотке",
"Language": "Язык", "Language": "Язык",
"Open App Dir": "Открыть папку приложения", "Open App Dir": "Открыть папку приложения",
"Open Core Dir": "Открыть папку ядра", "Open Core Dir": "Открыть папку ядра",
"Open Logs Dir": "Открыть папку логов", "Open Logs Dir": "Открыть папку логов",
"Check for Updates": "Проверить обновления",
"Open Dev Tools": "Открыть инструменты разработчика",
"Verge Version": "Версия Verge", "Verge Version": "Версия Verge",
"theme.light": "Светлая", "theme.light": "Светлая",
"theme.dark": "Тёмная", "theme.dark": "Тёмная",
@@ -113,6 +141,7 @@
"Clash Field": "Используемые настройки Clash", "Clash Field": "Используемые настройки Clash",
"Runtime Config": "Используемый конфиг", "Runtime Config": "Используемый конфиг",
"ReadOnly": "Только для чтения", "ReadOnly": "Только для чтения",
"ReadOnlyMessage": "Невозможно редактировать в режиме только для чтения",
"Restart": "Перезапуск", "Restart": "Перезапуск",
"Upgrade": "Обновлять", "Upgrade": "Обновлять",
@@ -120,6 +149,11 @@
"Save": "Сохранить", "Save": "Сохранить",
"Cancel": "Отмена", "Cancel": "Отмена",
"Exit": "Выход", "Exit": "Выход",
"Confirm": "Подтвердить",
"Default": "По умолчанию",
"Download Speed": "Скорость загрузки",
"Upload Speed": "Скорость загрузки",
"open_or_close_dashboard": "Open/Close Dashboard", "open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Режим правил", "clash_mode_rule": "Режим правил",
@@ -128,5 +162,43 @@
"toggle_system_proxy": "Включить/Отключить системный прокси", "toggle_system_proxy": "Включить/Отключить системный прокси",
"toggle_tun_mode": "Включить/Отключить режим туннеля", "toggle_tun_mode": "Включить/Отключить режим туннеля",
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную" "App Log Level": "Уровень журнала приложения",
"Auto Close Connections": "Автоматическое закрытие соединений",
"Auto Check Update": "Автоматическая проверка обновлений",
"Enable Builtin Enhanced": "Включить встроенные улучшения",
"Proxy Layout Column": "Количество столбцов в макете прокси",
"Default Latency Test": "Ссылка на тестирование задержки по умолчанию",
"Default Latency Timeout": "Таймаут задержки по умолчанию",
"Auto Log Clean": "Автоматическая очистка журналов",
"Never Clean": "Никогда не очищать",
"Retain 7 Days": "Сохранять 7 дней",
"Retain 30 Days": "Сохранять 30 дней",
"Retain 90 Days": "Сохранять 90 дней",
"Stack": "Стек",
"Device": "Имя устройства",
"Auto Route": "Автоматическая маршрутизация",
"Strict Route": "Строгий маршрут",
"Auto Detect Interface": "Автоопределение интерфейса",
"DNS Hijack": "DNS-перехват",
"MTU": "Максимальная единица передачи",
"Reset to Default": "Сбросить настройки по умолчанию",
"Current State": "Текущее состояние",
"pending": "Ожидающий",
"installed": "Установленный",
"uninstall": "Не установленный",
"active": "Активированный",
"unknown": "неизвестный",
"Disable Service Mode": "Отключить режим обслуживания",
"Install": "Установить",
"Uninstall": "Удалить",
"Portable Updater Error": "Портативная версия не поддерживает обновление внутри приложения, пожалуйста, скачайте и замените вручную",
"Tun Mode Info": "Режим туннеля требует предоставления разрешений, связанных с ядром. Пожалуйста, включите сервисный режим перед его использованием",
"System and Mixed Can Only be Used in Service Mode": "Система и смешанные могут использоваться только в сервисном режиме",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Информация: Пожалуйста, убедитесь, что сервис Clash Verge Service установлен и включен",
"Use Regular Expression": "Использование регулярных выражений"
} }
+28 -3
View File
@@ -18,6 +18,8 @@
"direct": "直连", "direct": "直连",
"script": "脚本", "script": "脚本",
"Create Test": "新建测试",
"Edit Test": "编辑测试",
"Edit": "编辑", "Edit": "编辑",
"Icon": "图标", "Icon": "图标",
"Test URL": "测试地址", "Test URL": "测试地址",
@@ -29,7 +31,9 @@
"New": "新建", "New": "新建",
"Create Profile": "新建订阅", "Create Profile": "新建订阅",
"Choose File": "选择文件", "Choose File": "选择文件",
"Close": "关闭",
"Close All": "关闭全部", "Close All": "关闭全部",
"Home": "首页",
"Select": "使用", "Select": "使用",
"Edit Info": "编辑信息", "Edit Info": "编辑信息",
"Edit File": "编辑文件", "Edit File": "编辑文件",
@@ -45,6 +49,8 @@
"Update All Profiles": "更新所有订阅", "Update All Profiles": "更新所有订阅",
"View Runtime Config": "查看运行时订阅", "View Runtime Config": "查看运行时订阅",
"Reactivate Profiles": "重新激活订阅", "Reactivate Profiles": "重新激活订阅",
"Confirm deletion": "确认删除",
"This operation is not reversible": "此操作不可逆",
"Location": "当前节点", "Location": "当前节点",
"Delay check": "延迟测试", "Delay check": "延迟测试",
@@ -52,6 +58,7 @@
"Sort by delay": "按延迟排序", "Sort by delay": "按延迟排序",
"Sort by name": "按名称排序", "Sort by name": "按名称排序",
"Delay check URL": "延迟测试链接", "Delay check URL": "延迟测试链接",
"Proxy basic": "隐藏节点细节",
"Proxy detail": "展示节点细节", "Proxy detail": "展示节点细节",
"Filter": "过滤节点", "Filter": "过滤节点",
"Filter conditions": "过滤条件", "Filter conditions": "过滤条件",
@@ -80,6 +87,9 @@
"Random Port": "随机端口", "Random Port": "随机端口",
"After restart to take effect": "重启后生效", "After restart to take effect": "重启后生效",
"External": "外部控制", "External": "外部控制",
"External Controller": "外部控制器监听地址",
"Core Secret": "API访问密钥",
"Recommended": "建议设置",
"Clash Core": "Clash 内核", "Clash Core": "Clash 内核",
"Grant": "授权", "Grant": "授权",
"Tun mode requires": "如需启用TUN模式需要授权", "Tun mode requires": "如需启用TUN模式需要授权",
@@ -123,6 +133,7 @@
"Open Core Dir": "内核目录", "Open Core Dir": "内核目录",
"Open Logs Dir": "日志目录", "Open Logs Dir": "日志目录",
"Check for Updates": "检查更新", "Check for Updates": "检查更新",
"Open Dev Tools": "打开开发者工具",
"Verge Version": "Verge 版本", "Verge Version": "Verge 版本",
"theme.light": "浅色", "theme.light": "浅色",
"theme.dark": "深色", "theme.dark": "深色",
@@ -130,6 +141,7 @@
"Clash Field": "Clash 字段", "Clash Field": "Clash 字段",
"Runtime Config": "当前配置", "Runtime Config": "当前配置",
"ReadOnly": "只读", "ReadOnly": "只读",
"ReadOnlyMessage": "无法在只读模式下编辑",
"Restart": "重启内核", "Restart": "重启内核",
"Upgrade": "升级内核", "Upgrade": "升级内核",
@@ -137,6 +149,7 @@
"Save": "保存", "Save": "保存",
"Cancel": "取消", "Cancel": "取消",
"Exit": "退出", "Exit": "退出",
"Confirm": "确认",
"Default": "默认", "Default": "默认",
"Download Speed": "下载速度", "Download Speed": "下载速度",
@@ -172,8 +185,20 @@
"MTU": "最大传输单元", "MTU": "最大传输单元",
"Reset to Default": "重置为默认值", "Reset to Default": "重置为默认值",
"Current State": "当前状态",
"pending": "等待中",
"installed": "已安装",
"uninstall": "未安装",
"active": "已激活",
"unknown": "未知",
"Disable Service Mode": "禁用服务模式",
"Install": "安装",
"Uninstall": "卸载",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换", "Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限,使用前请先开启服务模式", "Tun Mode Info": "Tun模式需要授予内核相关权限,使用前请先开启服务模式",
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限,使用前请先在内核设置中给内核授权", "System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用",
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用" "Information: Please make sure that the Clash Verge Service is installed and enabled": "提示信息: 请确保Clash Verge Service已安装并启用",
"Use Regular Expression": "使用正则表达式"
} }
+13
View File
@@ -24,6 +24,19 @@ 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", "R", "U"].includes(event.key.toUpperCase())
) {
event.preventDefault();
}
});
createRoot(container).render( createRoot(container).render(
<React.StrictMode> <React.StrictMode>
<RecoilRoot> <RecoilRoot>
+27 -23
View File
@@ -4,9 +4,8 @@ import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr"; import { SWRConfig, mutate } from "swr";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Route, Routes, useLocation } from "react-router-dom"; import { useLocation, useRoutes } from "react-router-dom";
import { CSSTransition, TransitionGroup } from "react-transition-group"; import { List, Paper, ThemeProvider } from "@mui/material";
import { alpha, List, Paper, ThemeProvider } from "@mui/material";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
import { routers } from "./_routers"; import { routers } from "./_routers";
@@ -16,7 +15,7 @@ import LogoSvg from "@/assets/image/logo.svg?react";
import LogoSvg_dark from "@/assets/image/logo_dark.svg?react"; import LogoSvg_dark from "@/assets/image/logo_dark.svg?react";
import { atomThemeMode } from "@/services/states"; import { atomThemeMode } from "@/services/states";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { BaseErrorBoundary, Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { LayoutItem } from "@/components/layout/layout-item"; import { LayoutItem } from "@/components/layout/layout-item";
import { LayoutControl } from "@/components/layout/layout-control"; import { LayoutControl } from "@/components/layout/layout-control";
import { LayoutTraffic } from "@/components/layout/layout-traffic"; import { LayoutTraffic } from "@/components/layout/layout-traffic";
@@ -27,6 +26,8 @@ import "dayjs/locale/ru";
import "dayjs/locale/zh-cn"; import "dayjs/locale/zh-cn";
import { getPortableFlag } from "@/services/cmds"; import { getPortableFlag } from "@/services/cmds";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
export let portableFlag = false; export let portableFlag = false;
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
@@ -43,6 +44,8 @@ const Layout = () => {
const { language, start_page } = verge || {}; const { language, start_page } = verge || {};
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const routersEles = useRoutes(routers);
if (!routersEles) return null;
useEffect(() => { useEffect(() => {
window.addEventListener("keydown", (e) => { window.addEventListener("keydown", (e) => {
@@ -122,9 +125,17 @@ const Layout = () => {
({ palette }) => ({ ({ palette }) => ({
bgcolor: palette.background.paper, bgcolor: palette.background.paper,
}), }),
OS === "linux"
? {
borderRadius: "8px",
border: "2px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
}
: {},
]} ]}
> >
<div className="layout__left" data-tauri-drag-region="true"> <div className="layout__left">
<div className="the-logo" data-tauri-drag-region="true"> <div className="the-logo" data-tauri-drag-region="true">
{!isDark ? <LogoSvg /> : <LogoSvg_dark />} {!isDark ? <LogoSvg /> : <LogoSvg_dark />}
{<UpdateButton className="the-newbtn" />} {<UpdateButton className="the-newbtn" />}
@@ -134,7 +145,7 @@ const Layout = () => {
{routers.map((router) => ( {routers.map((router) => (
<LayoutItem <LayoutItem
key={router.label} key={router.label}
to={router.link} to={router.path}
icon={router.icon} icon={router.icon}
> >
{t(router.label)} {t(router.label)}
@@ -148,11 +159,16 @@ const Layout = () => {
</div> </div>
<div className="layout__right"> <div className="layout__right">
{OS === "windows" && ( {
<div className="the-bar" data-tauri-drag-region="true"> <div className="the-bar">
<LayoutControl /> <div
className="the-dragbar"
data-tauri-drag-region="true"
style={{ width: "100%" }}
></div>
{OS !== "macos" && <LayoutControl />}
</div> </div>
)} }
<TransitionGroup className="the-content"> <TransitionGroup className="the-content">
<CSSTransition <CSSTransition
@@ -160,19 +176,7 @@ const Layout = () => {
timeout={300} timeout={300}
classNames="page" classNames="page"
> >
<Routes> {React.cloneElement(routersEles, { key: location.pathname })}
{routers.map(({ label, link, ele: Ele }) => (
<Route
key={label}
path={link}
element={
<BaseErrorBoundary key={label}>
<Ele />
</BaseErrorBoundary>
}
/>
))}
</Routes>
</CSSTransition> </CSSTransition>
</TransitionGroup> </TransitionGroup>
</div> </div>
+21 -15
View File
@@ -5,6 +5,7 @@ import ProfilesPage from "./profiles";
import SettingsPage from "./settings"; import SettingsPage from "./settings";
import ConnectionsPage from "./connections"; import ConnectionsPage from "./connections";
import RulesPage from "./rules"; import RulesPage from "./rules";
import { BaseErrorBoundary } from "@/components/base";
import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react"; import ProxiesSvg from "@/assets/image/itemicon/proxies.svg?react";
import ProfilesSvg from "@/assets/image/itemicon/profiles.svg?react"; import ProfilesSvg from "@/assets/image/itemicon/profiles.svg?react";
@@ -25,44 +26,49 @@ import SettingsRoundedIcon from "@mui/icons-material/SettingsRounded";
export const routers = [ export const routers = [
{ {
label: "Label-Proxies", label: "Label-Proxies",
link: "/", path: "/",
icon: [<WifiRoundedIcon />, <ProxiesSvg />], icon: [<WifiRoundedIcon />, <ProxiesSvg />],
ele: ProxiesPage, element: <ProxiesPage />,
}, },
{ {
label: "Label-Profiles", label: "Label-Profiles",
link: "/profile", path: "/profile",
icon: [<DnsRoundedIcon />, <ProfilesSvg />], icon: [<DnsRoundedIcon />, <ProfilesSvg />],
ele: ProfilesPage, element: <ProfilesPage />,
}, },
{ {
label: "Label-Connections", label: "Label-Connections",
link: "/connections", path: "/connections",
icon: [<LanguageRoundedIcon />, <ConnectionsSvg />], icon: [<LanguageRoundedIcon />, <ConnectionsSvg />],
ele: ConnectionsPage, element: <ConnectionsPage />,
}, },
{ {
label: "Label-Rules", label: "Label-Rules",
link: "/rules", path: "/rules",
icon: [<ForkRightRoundedIcon />, <RulesSvg />], icon: [<ForkRightRoundedIcon />, <RulesSvg />],
ele: RulesPage, element: <RulesPage />,
}, },
{ {
label: "Label-Logs", label: "Label-Logs",
link: "/logs", path: "/logs",
icon: [<SubjectRoundedIcon />, <LogsSvg />], icon: [<SubjectRoundedIcon />, <LogsSvg />],
ele: LogsPage, element: <LogsPage />,
}, },
{ {
label: "Label-Test", label: "Label-Test",
link: "/test", path: "/test",
icon: [<WifiTetheringRoundedIcon />, <TestSvg />], icon: [<WifiTetheringRoundedIcon />, <TestSvg />],
ele: TestPage, element: <TestPage />,
}, },
{ {
label: "Label-Settings", label: "Label-Settings",
link: "/settings", path: "/settings",
icon: [<SettingsRoundedIcon />, <SettingsSvg />], icon: [<SettingsRoundedIcon />, <SettingsSvg />],
ele: SettingsPage, element: <SettingsPage />,
}, },
]; ].map((router) => ({
...router,
element: (
<BaseErrorBoundary key={router.label}>{router.element}</BaseErrorBoundary>
),
}));
+3 -3
View File
@@ -4,7 +4,7 @@ const OS = getSystem();
// default theme setting // default theme setting
export const defaultTheme = { export const defaultTheme = {
primary_color: "#007AFF", primary_color: "#007AFF",
secondary_color: "#FFCC00", secondary_color: "#fc9b76",
primary_text: "#000000", primary_text: "#000000",
secondary_text: "#3c3c4399", secondary_text: "#3c3c4399",
info_color: "#007AFF", info_color: "#007AFF",
@@ -12,8 +12,8 @@ export const defaultTheme = {
warning_color: "#FF9500", warning_color: "#FF9500",
success_color: "#06943D", success_color: "#06943D",
background_color: "#f5f5f5", background_color: "#f5f5f5",
font_family: `-apple-system, BlinkMacSystemFont,"Microsoft YaHei UI", "Microsoft YaHei", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", ${ font_family: `-apple-system, BlinkMacSystemFont,"Microsoft YaHei UI", "Microsoft YaHei", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji"${
OS === "windows" ? "twemoji mozilla" : "" OS === "windows" ? ", twemoji mozilla" : ""
}`, }`,
}; };
+11 -21
View File
@@ -1,14 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { import { Box, Button, IconButton, MenuItem, Select } from "@mui/material";
Box,
Button,
IconButton,
MenuItem,
Paper,
Select,
TextField,
} from "@mui/material";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -26,6 +18,7 @@ import {
} from "@/components/connection/connection-detail"; } from "@/components/connection/connection-detail";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] }; const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
@@ -45,7 +38,12 @@ const ConnectionsPage = () => {
const isTableLayout = setting.layout === "table"; const isTableLayout = setting.layout === "table";
const orderOpts: Record<string, OrderFunc> = { const orderOpts: Record<string, OrderFunc> = {
Default: (list) => list, Default: (list) =>
list.sort(
(a, b) =>
new Date(b.start || "0").getTime()! -
new Date(a.start || "0").getTime()!
),
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!), "Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
"Download Speed": (list) => "Download Speed": (list) =>
list.sort((a, b) => b.curDownload! - a.curDownload!), list.sort((a, b) => b.curDownload! - a.curDownload!),
@@ -185,25 +183,17 @@ const ConnectionsPage = () => {
</Select> </Select>
)} )}
<TextField <BaseStyledTextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
spellCheck="false"
variant="outlined"
placeholder={t("Filter conditions")}
value={filterText} value={filterText}
onChange={(e) => setFilterText(e.target.value)} onChange={(e) => setFilterText(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
/> />
</Box> </Box>
<Box <Box
height="calc(100% - 70px)" height="calc(100% - 65px)"
sx={{ sx={{
userSelect: "text", userSelect: "text",
margin: "12px", margin: "10px",
borderRadius: "8px", borderRadius: "8px",
bgcolor: isDark ? "#282a36" : "#ffffff", bgcolor: isDark ? "#282a36" : "#ffffff",
}} }}
+62 -24
View File
@@ -5,9 +5,9 @@ import {
Button, Button,
IconButton, IconButton,
MenuItem, MenuItem,
Paper,
Select, Select,
TextField, SelectProps,
styled,
} from "@mui/material"; } from "@mui/material";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -19,6 +19,25 @@ import { atomEnableLog, atomLogData } from "@/services/states";
import { BaseEmpty, BasePage } from "@/components/base"; import { BaseEmpty, BasePage } from "@/components/base";
import LogItem from "@/components/log/log-item"; import LogItem from "@/components/log/log-item";
import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
const StyledSelect = styled((props: SelectProps<string>) => {
return (
<Select
size="small"
autoComplete="off"
sx={{
width: 120,
height: 33.375,
mr: 1,
'[role="button"]': { py: 0.65 },
}}
{...props}
/>
);
})(({ theme }) => ({
background: theme.palette.mode === "light" ? "#fff" : undefined,
}));
const LogPage = () => { const LogPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -28,8 +47,27 @@ const LogPage = () => {
const isDark = theme.palette.mode === "dark"; const isDark = theme.palette.mode === "dark";
const [logState, setLogState] = useState("all"); const [logState, setLogState] = useState("all");
const [filterText, setFilterText] = useState(""); const [filterText, setFilterText] = useState("");
const [useRegexSearch, setUseRegexSearch] = useState(true);
const [hasInputError, setInputError] = useState(false);
const [inputHelperText, setInputHelperText] = useState("");
const filterLogs = useMemo(() => { const filterLogs = useMemo(() => {
setInputHelperText("");
setInputError(false);
if (useRegexSearch) {
try {
const regex = new RegExp(filterText);
return logData.filter((data) => {
return (
regex.test(data.payload) &&
(logState === "all" ? true : data.type.includes(logState))
);
});
} catch (err: any) {
setInputHelperText(err.message.substring(0, 60));
setInputError(true);
return logData;
}
}
return logData.filter((data) => { return logData.filter((data) => {
return ( return (
data.payload.includes(filterText) && data.payload.includes(filterText) &&
@@ -77,42 +115,42 @@ const LogPage = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<Select <StyledSelect
size="small"
autoComplete="off"
value={logState} value={logState}
onChange={(e) => setLogState(e.target.value)} onChange={(e) => setLogState(e.target.value)}
sx={{
width: 120,
height: 33.375,
mr: 1,
'[role="button"]': { py: 0.65 },
}}
> >
<MenuItem value="all">ALL</MenuItem> <MenuItem value="all">ALL</MenuItem>
<MenuItem value="inf">INFO</MenuItem> <MenuItem value="inf">INFO</MenuItem>
<MenuItem value="warn">WARN</MenuItem> <MenuItem value="warn">WARN</MenuItem>
<MenuItem value="err">ERROR</MenuItem> <MenuItem value="err">ERROR</MenuItem>
</Select> </StyledSelect>
<TextField <BaseStyledTextField
hiddenLabel error={hasInputError}
fullWidth
size="small"
autoComplete="off"
spellCheck="false"
variant="outlined"
placeholder={t("Filter conditions")}
value={filterText} value={filterText}
onChange={(e) => setFilterText(e.target.value)} onChange={(e) => setFilterText(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }} helperText={inputHelperText}
placeholder={t("Filter conditions")}
InputProps={{
sx: { pr: 1 },
endAdornment: (
<IconButton
sx={{ fontWeight: "800", height: "100%", padding: "0" }}
color={useRegexSearch ? "primary" : "default"}
title={t("Use Regular Expression")}
onClick={() => setUseRegexSearch(!useRegexSearch)}
>
.*
</IconButton>
),
}}
/> />
</Box> </Box>
<Box <Box
height="calc(100% - 70px)" height="calc(100% - 65px)"
sx={{ sx={{
margin: "12px", margin: "10px",
borderRadius: "8px", borderRadius: "8px",
bgcolor: isDark ? "#282a36" : "#ffffff", bgcolor: isDark ? "#282a36" : "#ffffff",
}} }}
+75 -29
View File
@@ -1,8 +1,8 @@
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { useSetRecoilState } from "recoil"; import { useSetRecoilState } from "recoil";
import { Box, Button, Grid, IconButton, Stack, TextField } from "@mui/material"; import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material";
import { import {
DndContext, DndContext,
closestCenter, closestCenter,
@@ -33,6 +33,7 @@ import {
deleteProfile, deleteProfile,
updateProfile, updateProfile,
reorderProfile, reorderProfile,
createProfile,
} from "@/services/cmds"; } from "@/services/cmds";
import { atomLoadingCache } from "@/services/states"; import { atomLoadingCache } from "@/services/states";
import { closeAllConnections } from "@/services/api"; import { closeAllConnections } from "@/services/api";
@@ -46,6 +47,11 @@ import { ProfileMore } from "@/components/profile/profile-more";
import { useProfiles } from "@/hooks/use-profiles"; import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer"; import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { useRecoilState } from "recoil";
import { atomThemeMode } from "@/services/states";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
import { listen } from "@tauri-apps/api/event";
import { readTextFile } from "@tauri-apps/api/fs";
const ProfilePage = () => { const ProfilePage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -60,6 +66,35 @@ const ProfilePage = () => {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}) })
); );
useEffect(() => {
const unlisten = listen("tauri://file-drop", async (event) => {
const fileList = event.payload as string[];
for (let file of fileList) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
Notice.error("Only support YAML files.");
continue;
}
const item = {
type: "local",
name: file.split(/\/|\\/).pop() ?? "New Profile",
desc: "",
url: "",
option: {
with_proxy: false,
self_proxy: false,
},
} as IProfileItem;
let data = await readTextFile(file);
await createProfile(item, data);
await mutateProfiles();
}
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
const { const {
profiles = {}, profiles = {},
activateSelected, activateSelected,
@@ -235,6 +270,11 @@ const ProfilePage = () => {
const text = await navigator.clipboard.readText(); const text = await navigator.clipboard.readText();
if (text) setUrl(text); if (text) setUrl(text);
}; };
const [mode] = useRecoilState(atomThemeMode);
const islight = mode === "light" ? true : false;
const dividercolor = islight
? "rgba(0, 0, 0, 0.06)"
: "rgba(255, 255, 255, 0.06)";
return ( return (
<BasePage <BasePage
@@ -284,16 +324,10 @@ const ProfilePage = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<TextField <BaseStyledTextField
hiddenLabel
fullWidth
size="small"
value={url} value={url}
variant="outlined" variant="outlined"
autoComplete="off"
spellCheck="false"
onChange={(e) => setUrl(e.target.value)} onChange={(e) => setUrl(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
placeholder={t("Profile URL")} placeholder={t("Profile URL")}
InputProps={{ InputProps={{
sx: { pr: 1 }, sx: { pr: 1 },
@@ -323,6 +357,7 @@ const ProfilePage = () => {
loading={loading} loading={loading}
variant="contained" variant="contained"
size="small" size="small"
sx={{ borderRadius: "6px" }}
onClick={onImport} onClick={onImport}
> >
{t("Import")} {t("Import")}
@@ -330,6 +365,7 @@ const ProfilePage = () => {
<Button <Button
variant="contained" variant="contained"
size="small" size="small"
sx={{ borderRadius: "6px" }}
onClick={() => viewerRef.current?.create()} onClick={() => viewerRef.current?.create()}
> >
{t("New")} {t("New")}
@@ -341,7 +377,7 @@ const ProfilePage = () => {
mb: 0.5, mb: 0.5,
pl: "10px", pl: "10px",
mr: "10px", mr: "10px",
height: "calc(100% - 20px)", height: "calc(100% - 68px)",
overflowY: "auto", overflowY: "auto",
}} }}
> >
@@ -350,7 +386,7 @@ const ProfilePage = () => {
collisionDetection={closestCenter} collisionDetection={closestCenter}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
> >
<Box sx={{ mb: 4.5 }}> <Box sx={{ mb: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}> <Grid container spacing={{ xs: 1, lg: 1 }}>
<SortableContext <SortableContext
items={regularItems.map((x) => { items={regularItems.map((x) => {
@@ -375,24 +411,34 @@ const ProfilePage = () => {
</DndContext> </DndContext>
{enhanceItems.length > 0 && ( {enhanceItems.length > 0 && (
<Grid container spacing={{ xs: 2, lg: 2 }}> <Divider
{enhanceItems.map((item) => ( variant="middle"
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}> flexItem
<ProfileMore sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
selected={!!chain.includes(item.uid)} ></Divider>
itemData={item} )}
enableNum={chain.length || 0}
logInfo={chainLogs[item.uid]} {enhanceItems.length > 0 && (
onEnable={() => onEnable(item.uid)} <Box sx={{ mt: 1.5 }}>
onDisable={() => onDisable(item.uid)} <Grid container spacing={{ xs: 1, lg: 1 }}>
onDelete={() => onDelete(item.uid)} {enhanceItems.map((item) => (
onMoveTop={() => onMoveTop(item.uid)} <Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
onMoveEnd={() => onMoveEnd(item.uid)} <ProfileMore
onEdit={() => viewerRef.current?.edit(item)} selected={!!chain.includes(item.uid)}
/> itemData={item}
</Grid> enableNum={chain.length || 0}
))} logInfo={chainLogs[item.uid]}
</Grid> 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>
))}
</Grid>
</Box>
)} )}
</Box> </Box>
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} /> <ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
+5 -12
View File
@@ -2,12 +2,13 @@ import useSWR from "swr";
import { useState, useMemo } from "react"; import { useState, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { Box, TextField } from "@mui/material"; import { Box } from "@mui/material";
import { getRules } from "@/services/api"; import { getRules } from "@/services/api";
import { BaseEmpty, BasePage } from "@/components/base"; import { BaseEmpty, BasePage } from "@/components/base";
import RuleItem from "@/components/rule/rule-item"; import RuleItem from "@/components/rule/rule-item";
import { ProviderButton } from "@/components/rule/provider-button"; import { ProviderButton } from "@/components/rule/provider-button";
import { useCustomTheme } from "@/components/layout/use-custom-theme"; import { useCustomTheme } from "@/components/layout/use-custom-theme";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
const RulesPage = () => { const RulesPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -41,24 +42,16 @@ const RulesPage = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
<TextField <BaseStyledTextField
hiddenLabel
fullWidth
size="small"
autoComplete="off"
variant="outlined"
spellCheck="false"
placeholder={t("Filter conditions")}
value={filterText} value={filterText}
onChange={(e) => setFilterText(e.target.value)} onChange={(e) => setFilterText(e.target.value)}
sx={{ input: { py: 0.65, px: 1.25 } }}
/> />
</Box> </Box>
<Box <Box
height="calc(100% - 70px)" height="calc(100% - 65px)"
sx={{ sx={{
margin: "12px", margin: "10px",
borderRadius: "8px", borderRadius: "8px",
bgcolor: isDark ? "#282a36" : "#ffffff", bgcolor: isDark ? "#282a36" : "#ffffff",
}} }}
+1 -1
View File
@@ -134,7 +134,7 @@ const TestPage = () => {
> >
<Box <Box
sx={{ sx={{
pt: 1, pt: 1.25,
mb: 0.5, mb: 0.5,
px: "10px", px: "10px",
}} }}
+24 -2
View File
@@ -75,9 +75,13 @@ export const getRules = async () => {
}; };
/// Get Proxy delay /// Get Proxy delay
export const getProxyDelay = async (name: string, url?: string) => { export const getProxyDelay = async (
name: string,
url?: string,
timeout?: number
) => {
const params = { const params = {
timeout: 10000, timeout: timeout || 10000,
url: url || "http://1.1.1.1", url: url || "http://1.1.1.1",
}; };
const instance = await getAxios(); const instance = await getAxios();
@@ -237,3 +241,21 @@ export const closeAllConnections = async () => {
const instance = await getAxios(); const instance = await getAxios();
await instance.delete<any, any>(`/connections`); await instance.delete<any, any>(`/connections`);
}; };
// Get Group Proxy Delays
export const getGroupProxyDelays = async (
groupName: string,
url?: string,
timeout?: number
) => {
const params = {
timeout: timeout || 10000,
url: url || "http://1.1.1.1",
};
const instance = await getAxios();
const result = await instance.get(
`/group/${encodeURIComponent(groupName)}/delay`,
{ params }
);
return result as any as Record<string, number>;
};
+9 -1
View File
@@ -213,13 +213,21 @@ export async function getPortableFlag() {
return invoke<boolean>("get_portable_flag"); return invoke<boolean>("get_portable_flag");
} }
export async function openDevTools() {
return invoke("open_devtools");
}
export async function exitApp() { export async function exitApp() {
return invoke("exit_app"); return invoke("exit_app");
} }
export async function copyIconFile( export async function copyIconFile(
path: string, path: string,
name: "common.png" | "sysproxy.png" | "tun.png" name: "common" | "sysproxy" | "tun"
) { ) {
return invoke<void>("copy_icon_file", { path, name }); return invoke<void>("copy_icon_file", { path, name });
} }
export async function downloadIconCache(url: string, name: string) {
return invoke<string>("download_icon_cache", { url, name });
}
+3
View File
@@ -3,16 +3,19 @@ import { initReactI18next } from "react-i18next";
import en from "@/locales/en.json"; import en from "@/locales/en.json";
import ru from "@/locales/ru.json"; import ru from "@/locales/ru.json";
import zh from "@/locales/zh.json"; import zh from "@/locales/zh.json";
import fa from "@/locales/fa.json";
const resources = { const resources = {
en: { translation: en }, en: { translation: en },
ru: { translation: ru }, ru: { translation: ru },
zh: { translation: zh }, zh: { translation: zh },
fa: { translation: fa },
}; };
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
lng: "en", lng: "en",
fallbackLng: "en",
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
+6
View File
@@ -64,6 +64,7 @@ interface IProxyItem {
hidden?: boolean; hidden?: boolean;
icon?: string; icon?: string;
provider?: string; // 记录是否来自provider provider?: string; // 记录是否来自provider
fixed?: string; // 记录固定(优先)的节点
} }
type IProxyGroupItem = Omit<IProxyItem, "all"> & { type IProxyGroupItem = Omit<IProxyItem, "all"> & {
@@ -142,6 +143,8 @@ interface IClashInfo {
// status: string; // status: string;
mixed_port?: number; // clash mixed port mixed_port?: number; // clash mixed port
socks_port?: number; // clash socks port socks_port?: number; // clash socks port
redir_port?: number; // clash redir port
tproxy_port?: number; // clash tproxy port
port?: number; // clash http port port?: number; // clash http port
server?: string; // external-controller server?: string; // external-controller
secret?: string; secret?: string;
@@ -166,6 +169,7 @@ interface IProfileItem {
expire: number; expire: number;
}; };
option?: IProfileOption; option?: IProfileOption;
home?: string;
} }
interface IProfileOption { interface IProfileOption {
@@ -214,6 +218,8 @@ interface IVergeConfig {
enable_random_port?: boolean; enable_random_port?: boolean;
verge_mixed_port?: number; verge_mixed_port?: number;
verge_socks_port?: number; verge_socks_port?: number;
verge_redir_port?: number;
verge_tproxy_port?: number;
verge_port?: number; verge_port?: number;
enable_proxy_guard?: boolean; enable_proxy_guard?: boolean;
proxy_guard_duration?: number; proxy_guard_duration?: number;
+10 -2
View File
@@ -2,7 +2,7 @@ import { defineConfig } from "vite";
import path from "path"; import path from "path";
import svgr from "vite-plugin-svgr"; import svgr from "vite-plugin-svgr";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import monaco from "vite-plugin-monaco-editor"; import monacoEditor from "vite-plugin-monaco-editor";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
@@ -11,7 +11,15 @@ export default defineConfig({
plugins: [ plugins: [
svgr(), svgr(),
react(), react(),
monaco({ languageWorkers: ["editorWorkerService", "typescript"] }), monacoEditor({
languageWorkers: ["editorWorkerService", "typescript"],
customWorkers: [
{
label: "yaml",
entry: "monaco-yaml/yaml.worker",
},
],
}),
], ],
build: { build: {
outDir: "../dist", outDir: "../dist",