1081 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
LiuTianYu
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 e6a5a2b4961dba4e891b1b62d6f35db4ca9ee5ce.
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
MystiPanda
50ade92238 Release 1.5.7 2024-03-11 17:09:01 +08:00
MystiPanda
b598d00aef fix: display error 2024-03-11 16:18:39 +08:00
MystiPanda
97bee17e4e chore: Change IP 2024-03-11 16:05:09 +08:00
MystiPanda
6aa5c79332 fix: a little 2024-03-11 15:02:41 +08:00
MystiPanda
7c5ce756f9 fix: Change DNS for MacOS Tun Mode
#568
2024-03-11 14:55:00 +08:00
MystiPanda
43d53a9ffa fix: default value 2024-03-11 13:17:45 +08:00
MystiPanda
e6589bee5b fix: Try to fix touch drag
#456
2024-03-11 12:36:20 +08:00
MystiPanda
9f94cad615 feat: Allow to control whether auto check update 2024-03-11 12:17:46 +08:00
MystiPanda
7778d9f773 style: fix a little 2024-03-10 23:56:04 +08:00
MystiPanda
564bd55147 style: fix styles 2024-03-10 23:47:12 +08:00
MystiPanda
a7ff806522 fix styles 2024-03-10 22:13:25 +08:00
MystiPanda
ac3b074d66 chore: proxy group header height 2024-03-10 21:56:49 +08:00
MystiPanda
da949c2604 chore: adjust proxy group font style 2024-03-10 21:52:40 +08:00
MystiPanda
20a9775dc6 chore: Adjust Profile Style 2024-03-10 21:37:52 +08:00
MystiPanda
8ec19e058f chore: Add Default value 2024-03-10 21:15:22 +08:00
MystiPanda
a5c4549722 style: Adjust icon color 2024-03-10 20:48:13 +08:00
Amnesiash
c7d302aa16 style: Adjust colorful icon (#558)
Co-authored-by: MystiPanda <mystipanda@proton.me>
2024-03-10 20:37:00 +08:00
MystiPanda
77ac565ff3 chore: i18n 2024-03-10 20:33:27 +08:00
MystiPanda
2e6f834a50 chore: Update Icon 2024-03-10 20:14:51 +08:00
MystiPanda
5b044cca9e feat: Add option to control menu icon 2024-03-10 19:54:47 +08:00
MystiPanda
2ab5623809 chore: Adjust styles 2024-03-10 13:24:38 +08:00
MystiPanda
2d07f0d77c Revert "chore: Adjust secondary text style (#545)"
This reverts commit 8ba7f9f702.
2024-03-10 13:08:09 +08:00
Amnesiash
8ba7f9f702 chore: Adjust secondary text style (#545) 2024-03-10 12:54:34 +08:00
MystiPanda
460ac7a86b style: Adjust delay fontSize
#544
2024-03-10 12:51:53 +08:00
MystiPanda
f93a1ab3eb docs: update preview 2024-03-10 12:32:08 +08:00
MystiPanda
6b02696aa8 chore: Change Default Font 2024-03-10 12:23:11 +08:00
MystiPanda
25f20d6a85 chore: delete debug output 2024-03-10 11:47:55 +08:00
MystiPanda
8387964bae Release 1.5.6 2024-03-10 11:43:37 +08:00
MystiPanda
926278617f feat: Add default webui
#530
2024-03-10 11:41:22 +08:00
MystiPanda
1933737a0c Adjust styles 2024-03-10 11:21:17 +08:00
MystiPanda
b3e5288bde Adjust styles 2024-03-10 11:12:54 +08:00
wonfen
ed6e966b2f Sytle: UI improvement & Update Readme 2024-03-10 07:00:24 +08:00
MystiPanda
9315fe36b6 fix: fix a little 2024-03-10 00:41:18 +08:00
black23eep
41fbf5ba36 Update layout-traffic.tsx (#528) 2024-03-10 00:35:26 +08:00
MystiPanda
40db481453 chore: update 2024-03-10 00:34:22 +08:00
MystiPanda
1b5e295744 fix: fontSize and some styles 2024-03-10 00:22:22 +08:00
Amnesiash
b42a52f7f6 fix settings.svg (#526) 2024-03-10 00:04:06 +08:00
MystiPanda
8268df84d0 chore: center 2024-03-09 23:38:03 +08:00
Charles
eb6fa5f1f1 tweak(ui): menu icon use svg component (#524) 2024-03-09 23:13:08 +08:00
MystiPanda
025c8856ed fix: img path error 2024-03-09 22:43:53 +08:00
MystiPanda
ed421445e9 Release 1.5.5 2024-03-09 22:02:36 +08:00
Amnesiash
0cda07106b refactor: Upgrade to the new UI (#521)
Co-authored-by: MystiPanda <mystipanda@proton.me>
2024-03-09 21:37:21 +08:00
wonfen
f335941b62 UI: change paste icon, delete default profile name 2024-03-09 02:34:33 +08:00
Pylogmon
f0719f8bde feat: Merge Providers (#508) 2024-03-08 11:37:52 +08:00
MystiPanda
27fe28661c feat: Add animation for provider update 2024-03-02 13:52:48 +08:00
MystiPanda
bb13b12de6 chore: fix style 2024-02-28 18:25:50 +08:00
MystiPanda
1117e28b2e feat: Try to support touch drag 2024-02-27 23:41:52 +08:00
MystiPanda
0f48873c25 chore: Remove unnecessary hotkey 2024-02-27 11:18:52 +08:00
MystiPanda
faf61b4af3 fix: Image display failed on Linux 2024-02-26 13:31:51 +08:00
Cyenoch
e29ce93d32 Feat: Provide a switch for allowing invalid certificates (#450) 2024-02-25 16:07:06 +08:00
MystiPanda
38ab68492e chore: change style 2024-02-24 15:39:29 +08:00
MystiPanda
53b1576e5f Release 1.5.4 2024-02-24 14:00:01 +08:00
MystiPanda
2835e79973 feat: Support Open/Close Dashboard Hotkey
#439
2024-02-24 13:39:30 +08:00
MystiPanda
8e4d7e989b feat: Show current proxy for group node
#444
2024-02-24 13:09:53 +08:00
MystiPanda
603c6b826c feat: allow disable group icon 2024-02-24 12:38:17 +08:00
MystiPanda
51bcc77cb3 refactor: Optimize implementation of Custom tray icon 2024-02-24 11:25:22 +08:00
MystiPanda
a30d07b924 feat: Support Custom Tray Icon 2024-02-24 00:52:21 +08:00
MystiPanda
11faa5fe39 Release 1.5.3 2024-02-22 00:23:56 +08:00
MystiPanda
ce8383b26f feat: add reset button 2024-02-22 00:19:45 +08:00
MystiPanda
bd8f07c90a fix: Do not set autolaunch at init
#423 #424
2024-02-21 23:50:50 +08:00
MystiPanda
fee077bebd chore: rm -m arg 2024-02-21 23:38:01 +08:00
MystiPanda
558f4d93ba chore: change default value of dns hijack 2024-02-21 16:40:47 +08:00
MystiPanda
e77d126349 chore: change default value of strict route 2024-02-21 11:32:51 +08:00
MystiPanda
cd8667d6de chore: fix placeholder 2024-02-21 11:13:28 +08:00
MystiPanda
8a2d06d010 chore: Add Translation 2024-02-21 11:06:32 +08:00
MystiPanda
92741fc733 feat: Disable system stack when service mode is turned off 2024-02-21 10:52:03 +08:00
MystiPanda
1cff162649 fix: Config data display error
#417
2024-02-21 10:02:28 +08:00
MystiPanda
f59be465ea Release 1.5.2 2024-02-21 00:04:11 +08:00
MystiPanda
27305317ce chore: Auto Update config.yml 2024-02-20 23:54:02 +08:00
MystiPanda
8378320e50 feat: Support Tun Config (#416) 2024-02-20 23:27:03 +08:00
MystiPanda
86a69952db chore: Update patch file 2024-02-20 18:39:36 +08:00
MystiPanda
e212ebfdb9 fix: Allow program run with administrator to start at startup 2024-02-20 18:35:39 +08:00
MystiPanda
f235125d48 chore: add tooltip for tun mode 2024-02-19 18:10:10 +08:00
MystiPanda
8efa815eb9 feat: Support custom delay timeout (#397) 2024-02-18 11:11:22 +08:00
MystiPanda
e54d42576b build: Try restart windows service after install (#395) 2024-02-18 10:19:54 +08:00
MystiPanda
1e18539862 fix: Merge profile unexpect behavior 2024-02-17 15:58:52 +08:00
MystiPanda
673fdf4721 ci: Update Release Note 2024-02-15 20:05:00 +08:00
MystiPanda
57d6bfba51 ci: change tag 2024-02-15 19:45:37 +08:00
MystiPanda
8fa23aecda chore: update ci script 2024-02-15 19:14:35 +08:00
MystiPanda
f9c10533e8 chore: Change CI Name 2024-02-15 18:45:13 +08:00
MystiPanda
c891c3723e ci: Support alpha package 2024-02-15 18:43:20 +08:00
MystiPanda
a0625ef2e7 chore: remove 32bit package 2024-02-15 18:22:15 +08:00
MystiPanda
b2774abb54 chore: default show proxy detials 2024-02-15 18:17:21 +08:00
wonfen
fa089bbc40 doc: remove 32bit package 2024-02-12 02:25:18 +08:00
MystiPanda
8437d1763f fix: build error 2024-02-11 20:51:00 +08:00
MystiPanda
0532b0f9df Release v1.5.1 2024-02-11 20:32:25 +08:00
MystiPanda
5f8eb853f0 style: clear script 2024-02-11 20:03:14 +08:00
MystiPanda
9125a85382 fix: Custom GLOBAL group display error 2024-02-11 18:27:27 +08:00
MystiPanda
5b3dc44c72 feat: Show proxies count for provider 2024-02-10 13:13:27 +08:00
MystiPanda
ee5147f111 feat: Save window maximize state 2024-02-10 12:51:30 +08:00
MystiPanda
f67137fd5e style: change config name
#350
2024-02-08 10:23:54 +08:00
MystiPanda
b67db4a896 fix: Script profile invalid
#347
2024-02-06 08:55:44 +08:00
MystiPanda
067018828c Release 1.5.0 2024-02-06 00:41:12 +08:00
MystiPanda
e990530ada refactor: Remove clash field filter 2024-02-05 17:28:36 +08:00
wonfen
45f863a72a chore: enable clash field TLS fingerprint 2024-02-04 13:07:25 +08:00
MystiPanda
519febbeb2 fix: Try to fix traffic parse error
#337
2024-02-04 10:24:37 +08:00
wonfen
854b1a4caf chore: update changelog 2024-02-03 16:14:26 +08:00
MystiPanda
08eb95e73a fix: Copy Env Type Select Error 2024-02-02 17:57:56 +08:00
MystiPanda
66f8fbf08c chore: typo 2024-02-02 17:47:28 +08:00
MystiPanda
86a30bbb29 chore: Update Changelog 2024-02-02 17:34:15 +08:00
MystiPanda
966c453c27 fix: prevent_exit 2024-02-02 17:26:31 +08:00
MystiPanda
c7bcaf0193 Release 1.4.11 2024-02-02 16:56:39 +08:00
MystiPanda
0ac91e36a0 fix: exit_app event 2024-02-02 16:32:19 +08:00
MystiPanda
6f1299ce9e chore: fix styles 2024-02-02 16:15:23 +08:00
MystiPanda
83ca29d649 chore: Remove prevent_close 2024-02-02 15:57:03 +08:00
MystiPanda
d0206044dc fix: Fix Nisi Error 2024-02-02 15:53:28 +08:00
ycjcl868
d4623e4175 chore: unused clash core (#284) 2024-01-23 11:22:15 +08:00
MystiPanda
68ef03377d chore: fix style 2024-01-21 13:49:07 +08:00
MystiPanda
d3f9d3d033 revert: some style 2024-01-21 13:49:07 +08:00
wonfen
a6ccd04471 Sytle: UI improvement 2024-01-20 16:05:01 +08:00
Lai Zn
0fd8174e77 fix: Solve the confliction issues on auto updater failure (#273) 2024-01-20 13:03:48 +08:00
MystiPanda
ebc63f479a revert: Support both registry and api for windows sysproxy 2024-01-20 12:16:46 +08:00
MystiPanda
570c39c20a build: Update lock file 2024-01-18 22:34:14 +08:00
MystiPanda
1d57257cf5 Release v1.4.10 2024-01-18 22:26:23 +08:00
MystiPanda
82d7baee0b feat: Add exit button on setting page 2024-01-18 22:19:14 +08:00
MystiPanda
1d91e1f1f7 build: Update Depends 2024-01-18 16:26:38 +08:00
MystiPanda
15e8894614 feat: Support Custom Start Page 2024-01-18 15:37:02 +08:00
Lai Zn
0ae96918e9 feat: Use url path name as fallback subscription name (#255) 2024-01-18 14:36:37 +08:00
MystiPanda
e970880059 feat: Show SubInfo for Proxy Provider
#211
2024-01-18 14:26:57 +08:00
Lai Zn
69ae86aba8 fix: make port change set to system proxy immediately (#256) 2024-01-18 09:37:46 +08:00
MystiPanda
8142e1be49 fix: use nanoid to compatible with old devices 2024-01-18 01:16:39 +08:00
MystiPanda
cbc7612451 feat: Optimizing Provider Support 2024-01-18 01:02:30 +08:00
Kiri
09e6a7b7cc feat: Add support for loong64 (#246) 2024-01-17 22:30:51 +08:00
Lai Zn
082e35668a docs: Add guidelines for windows development (#250) 2024-01-17 19:02:39 +08:00
MystiPanda
c9e78c837b fix: Do not use proxy when test on tun mode 2024-01-17 18:08:09 +08:00
MystiPanda
2d171db672 Release v1.4.9 2024-01-17 15:43:15 +08:00
MystiPanda
a8b11abec8 feat: Support Startup Script 2024-01-17 15:06:16 +08:00
MystiPanda
b08333dccd feat: Support proxy group icon 2024-01-17 13:32:56 +08:00
MystiPanda
98bad48971 fix: Fix connection table sort error
#108
2024-01-17 12:54:54 +08:00
MystiPanda
53375bb536 chore: Update I18n 2024-01-17 11:30:19 +08:00
MystiPanda
88798093e1 fix: csp error 2024-01-17 11:08:14 +08:00
MystiPanda
45a28751af feat: Add Test Page 2024-01-17 11:02:17 +08:00
MystiPanda
1670c44464 fix: fix column width 2024-01-16 19:02:38 +08:00
MystiPanda
a286ac85dc Release v1.4.8 2024-01-16 15:36:04 +08:00
MystiPanda
1606adc0ab feat: Support both registry and api for windows sysproxy 2024-01-16 15:11:53 +08:00
MystiPanda
b4ce557d92 fix: Can not use specify update time when create profile 2024-01-16 10:29:04 +08:00
MystiPanda
2ba7fff8d4 refactor: rm theme blur 2024-01-15 17:13:55 +08:00
MystiPanda
98efc93610 Revert Use Tauri Http Api 2024-01-15 10:18:04 +08:00
MystiPanda
2680507eae Revert Use Tauri Websocket 2024-01-15 10:17:00 +08:00
MystiPanda
df72392ad2 fix: Fix connections sort issue and add total traffic info 2024-01-15 00:46:19 +08:00
MystiPanda
a6acb15a00 refactor: Use Tauri Http API 2024-01-14 19:35:03 +08:00
MystiPanda
ba7242a815 refactor: Use Tauri WebSocket 2024-01-14 17:30:18 +08:00
MystiPanda
99740c1324 fix patch error 2024-01-11 14:47:25 +08:00
MystiPanda
c04b20d1fa Release v1.4.7 2024-01-11 13:19:05 +08:00
MystiPanda
e127067878 feat: Disable updater for portable 2024-01-11 12:44:30 +08:00
MystiPanda
6f03b72368 feat: Support hide group
#214
2024-01-11 12:34:05 +08:00
Morris Li
7bd3ee9340 Fix expired Tauri domain (#222) 2024-01-11 10:17:56 +08:00
MystiPanda
c94db606e5 build: Use old sysproxy 2024-01-10 19:24:14 +08:00
MystiPanda
4d2474226b refactor: cargo clippy 2024-01-10 17:36:35 +08:00
MystiPanda
1ffc4f538b refactor: Optimizing the implementation of Linux URL Scheme registration 2024-01-10 16:34:35 +08:00
MystiPanda
6702ac957b fix: resolve scheme error 2024-01-10 15:37:40 +08:00
MystiPanda
ba42b2e77d fix: linux build error 2024-01-10 15:22:08 +08:00
MystiPanda
fba18ca40a feat: Support URL Scheme for MacOS 2024-01-10 14:04:06 +08:00
MystiPanda
3a2a7a1476 feat: Support URL Scheme for Linux 2024-01-10 13:03:34 +08:00
MystiPanda
3c6d6b90bc build: fix macos build script 2024-01-09 22:49:11 +08:00
MystiPanda
47dc9ee304 fix patch error 2024-01-09 22:25:25 +08:00
MystiPanda
0045bf206c Release v1.4.6 2024-01-09 22:17:29 +08:00
MystiPanda
669a1a6953 feat: Support URL Scheme for Windows
#165
2024-01-09 21:57:06 +08:00
MystiPanda
e28452cc7b feat: Optimize control button style 2024-01-09 16:04:56 +08:00
MystiPanda
5ceac03db1 feat: Add pin button 2024-01-09 15:13:59 +08:00
MystiPanda
96215e5950 fix: Fix some compile error 2024-01-09 14:52:43 +08:00
wonfen
7a030b9224 Style: UI improvement & 1.4.6 ready 2024-01-09 13:57:53 +08:00
wonfen
e743478a4d chore: UI adjustment 2024-01-08 19:32:21 +08:00
MystiPanda
bc29c80d44 chore: Use Latest and compatible core 2023-12-23 12:09:19 +08:00
MystiPanda
82b8a474d7 fix: Cargo clippy 2023-12-21 16:49:21 +08:00
MystiPanda
99851b297d fix: Get filename error
#165
2023-12-19 19:52:13 +08:00
MystiPanda
e3a500e12c fix: portable flag 2023-12-15 21:39:34 +08:00
MystiPanda
378666e3cc chore: update service url 2023-12-15 20:38:17 +08:00
MystiPanda
b65ad1ebd7 fix: user-agent version error 2023-12-15 15:18:01 +08:00
MystiPanda
933a821b5f fix: Subinfo parse error 2023-12-15 11:35:10 +08:00
MystiPanda
159337ddbd Release 1.4.5 2023-12-14 17:40:48 +08:00
MystiPanda
2e25a4333a fix: Window control button icon issue
#136
2023-12-14 14:59:55 +08:00
MystiPanda
64fafb0795 chore: Remove unsafe function 2023-12-14 13:56:51 +08:00
MystiPanda
5927e0bb3b fix: icon size 2023-12-14 13:20:01 +08:00
MystiPanda
e21596db83 feat: Update MacOS tray icon 2023-12-14 13:11:46 +08:00
MystiPanda
8a608c3c3e chore: Optimize service path 2023-12-14 13:03:52 +08:00
MystiPanda
76f9db8516 chore: Optimize upgrade process 2023-12-14 11:24:26 +08:00
MystiPanda
90f4809b7c chore: default enable clash field filter 2023-12-14 10:49:58 +08:00
MystiPanda
3ff1af9fd6 chore: Delete unnecessary drag area 2023-12-13 11:09:19 +08:00
MystiPanda
711dd520f7 chore: Change service path 2023-12-13 10:59:07 +08:00
MystiPanda
99503c836a fix: Save wrong window size 2023-12-12 16:42:19 +08:00
MystiPanda
2c8bc51862 Revert "fix: Change PID file path"
This reverts commit d64bdf02de.
2023-12-12 15:59:00 +08:00
MystiPanda
5e7aa15232 fix: Can't switch env type 2023-12-11 10:51:59 +08:00
MystiPanda
d0761869b6 build: fix update issue 2023-12-10 22:24:46 +08:00
MystiPanda
bb99b89228 build: fix patch 2023-12-10 21:40:57 +08:00
MystiPanda
7b88beb0b5 v1.4.4 2023-12-10 21:36:22 +08:00
MystiPanda
82c630bd0e feat: Support windows aarch64 (#112) 2023-12-10 20:45:27 +08:00
MystiPanda
bb4f11e1b8 feat: Patch for windows aarch64 2023-12-10 19:50:01 +08:00
MystiPanda
3182d20b7c build: Update Depends 2023-12-10 17:14:48 +08:00
MystiPanda
a6f15e2474 chore: Hide script mode for alpha core 2023-12-10 16:31:55 +08:00
MystiPanda
0c3f03680c Support upgrade alpha core 2023-12-10 15:57:10 +08:00
MystiPanda
a66e8dfc9f feat: Support update geodata
#37
2023-12-10 15:22:04 +08:00
zclkkk
556232aac4 chore: Allow user to choose where to install (#110) 2023-12-10 09:50:43 +08:00
MystiPanda
7fcfaadd91 chore: Remove script mode 2023-12-09 17:04:03 +08:00
MystiPanda
18d388e1e2 docs: Update ISSUE_TEMPLATE 2023-12-09 12:00:01 +08:00
MystiPanda
7de6622d74 feat: Support different tray icon for macos 2023-12-09 11:22:02 +08:00
MystiPanda
9af1f90990 feat: Support different tray icon for linux 2023-12-09 11:03:19 +08:00
Pylogmon
15ab43963d feat: Optimize copy environment variable logic (#106) 2023-12-08 22:16:42 +08:00
MystiPanda
6b7465a4b0 perf: Improves window creation speed 2023-12-08 13:10:35 +08:00
MystiPanda
d64bdf02de fix: Change PID file path
#99
2023-12-08 11:59:40 +08:00
MystiPanda
4a2c94993a docs: Update README 2023-12-07 16:49:59 +08:00
MystiPanda
86c318d86b feat: Add AppImage for x86 linux 2023-12-07 16:38:39 +08:00
MystiPanda
bffc1ad41d ci: fix build script 2023-12-07 16:17:34 +08:00
MystiPanda
2a685c116f chore: Fix build error 2023-12-07 16:02:29 +08:00
MystiPanda
0e271d2924 ci: Fix Linux CI Script 2023-12-07 15:57:53 +08:00
MystiPanda
7127c4d590 ci: Fix Linux Build Script 2023-12-07 15:34:49 +08:00
MystiPanda
a82929996b chore: change default port to 7897 2023-12-07 14:52:14 +08:00
MystiPanda
3f01f52c7b refactor: fix depends 2023-12-07 14:44:44 +08:00
MystiPanda
9c94494622 refactor: Remove unnecessary changes 2023-12-07 13:43:53 +08:00
MystiPanda
c8a508f35c Revert "chore: change default port to 7897"
This reverts commit 4ec73ef1c3.
2023-12-07 13:32:49 +08:00
wonfen
4ec73ef1c3 chore: change default port to 7897 2023-12-06 03:37:16 +08:00
MystiPanda
3465b79e5b docs: Update README 2023-12-05 17:02:54 +08:00
MystiPanda
726a0a33ae docs: UPDATELOG 2023-12-05 16:47:42 +08:00
MystiPanda
ba05810d80 Release 1.4.3 2023-12-05 16:28:50 +08:00
MystiPanda
10bb53e7de chore: fix appid 2023-12-05 15:41:13 +08:00
MystiPanda
dd8a083546 refactor: Change app_home to standard locations of app_data 2023-12-05 15:39:44 +08:00
MystiPanda
84131a71c9 fix: Stop core before install update 2023-12-05 15:01:15 +08:00
MystiPanda
4909a11896 docs: Update README 2023-12-05 13:33:23 +08:00
MystiPanda
4069357b81 refactor: Remove page animation 2023-12-05 13:30:55 +08:00
MystiPanda
e6e87bcc40 refactor: Change Tun Icon Color 2023-12-05 13:23:54 +08:00
MystiPanda
7ba22045e8 build: Dependency downgrade 2023-12-05 12:55:20 +08:00
Pylogmon
d4040b61c4 refactor: Change Portable Config Path (#66) 2023-12-05 12:52:26 +08:00
Pylogmon
7534f8fc37 refactor: Hide Clash Field Option when Disable Filter (#63) 2023-12-05 08:34:57 +08:00
MystiPanda
7ced009e92 Update README.md 2023-12-04 14:51:46 +08:00
MystiPanda
cc13c71e40 ci: fix updater 2023-12-04 13:51:46 +08:00
MystiPanda
d2f09209a7 chore: Use Nsis 2023-12-04 13:45:28 +08:00
Pylogmon
a241b04d99 fix: Open File (#56) 2023-12-04 12:15:12 +08:00
Pylogmon
4f35d907cb fix: Get Profile Filename (#54) 2023-12-04 12:15:01 +08:00
MystiPanda
9e50d144a3 chore: change linux meta core to compatible 2023-12-04 11:38:54 +08:00
wonfen
8514bb48c4 chore: change windows meta core to compatible 2023-12-04 06:00:31 +08:00
WhizPanda
3ae633a2a1 fix: alpha core can't display memory 2023-12-03 18:55:38 +08:00
WhizPanda
2799cb9118 style: fix icon size 2023-12-03 18:28:41 +08:00
WhizPanda
d20346b6ac docs: Update README.md 2023-12-03 16:42:49 +08:00
Pylogmon
2f00666a68 style: improve drag icon style (#51) 2023-12-03 16:27:13 +08:00
WhizPanda
80f4570093 ci: Fix Portable Script 2023-12-03 15:36:39 +08:00
wonfen
3f0a2ba48f docs: update readme 2023-12-03 15:25:33 +08:00
WhizPanda
fc90094e8a feat: Support Both Stable and Alpha Version (#47) 2023-12-03 14:26:03 +08:00
wonfen
20d580ade8 release: 1.4.2, tweak UI, fix emoji on mac 2023-12-03 14:01:53 +08:00
WhizPanda
290a024a9e chore: Adjust style 2023-12-02 23:20:04 +08:00
WhizPanda
64a283c3a6 chore: Replace Repo Name 2023-12-02 23:04:04 +08:00
WhizPanda
3e93f0ecbc build: Update Depends 2023-12-02 22:36:04 +08:00
WhizPanda
b12bcc1c49 style: Improve Style 2023-12-02 16:23:53 +08:00
WhizPanda
05d7313dcc feat: Support New Clash Field
#46
2023-12-02 15:18:54 +08:00
WhizPanda
c0b6e549f0 build: Update Cargo Depends 2023-12-01 15:52:49 +08:00
WhizPanda
31f9bf219e build: Update Depends 2023-12-01 14:49:32 +08:00
WhizPanda
4906ca7059 feat: support random mixed port 2023-12-01 12:56:18 +08:00
WhizPanda
b963a7a0e5 fix: Fix Updater Script 2023-12-01 11:19:49 +08:00
WhizPanda
5d6dadda76 Remove unnecessary conditions 2023-12-01 11:11:20 +08:00
Pylogmon
77f69fd223 feat: Add Windows x86 and Linux armv7 Support (#44)
* feat: Add Windows x86 and Linux armv7 Support

* ci: Remove Linux armv7 Support
2023-12-01 11:03:18 +08:00
Pylogmon
7f5052bc87 feat: Use Latest Meta Core mihomo (#41) 2023-12-01 02:44:18 +08:00
Pylogmon
3009c1f4f6 feat: Support cross-compiling to aarch64 (#40)
#19
2023-11-30 22:46:09 +08:00
Pylogmon
5425872bba feat: Support Disable Tray Click Event (#38)
#21
2023-11-30 22:45:02 +08:00
Pylogmon
0759e17295 feat: Set different tray icon on tun mode (#33)
好看!
2023-11-30 01:29:11 +08:00
Pylogmon
56a59d25df feat: Add Download Progress for Updater (#34) 2023-11-30 01:28:46 +08:00
Pylogmon
887f92babe feat: Support Drag to Reorder the Profile (#29)
* feat: Support Drag to Reorder the Profile

* style: Remove unnecessary styles
2023-11-29 08:54:02 +08:00
wonfen
197f942b3f Merge pull request #27 from Pylogmon/emoji-font
feat: Embed Emoji fonts
2023-11-28 10:41:35 +08:00
Pylogmon
a5be7a35e9 feat: Embed Emoji fonts 2023-11-28 10:36:32 +08:00
wonfen
e347675265 fix: Adjust font order 2023-11-28 10:03:01 +08:00
wonfen
02a084d571 chore: update preview gif 2023-11-28 08:49:22 +08:00
wonfen
7fbcc23d94 Merge branch 'main' of github.com:wonfen/clash-verge-rev 2023-11-28 07:53:11 +08:00
wonfen
bda87167a3 update clashmeta core, Imporve UI, merge PR, reset icons, fix CI 2023-11-28 07:49:44 +08:00
wonfen
34f53c287e Merge pull request #24 from Pylogmon/tray
fix: Tray Icon Tooltip is Empty
2023-11-28 01:46:41 +08:00
wonfen
9ab07f37d7 Merge pull request #21 from Pylogmon/tray-event
feat: Config Tray Click Event
2023-11-28 01:46:16 +08:00
Pylogmon
36399b39fb chore: fix style 2023-11-27 20:10:31 +08:00
Pylogmon
1e015287a1 chore: Add Translation 2023-11-27 20:04:47 +08:00
Pylogmon
a590aa8485 fix: Tray Icon Tooltip is Empty 2023-11-27 19:55:42 +08:00
Pylogmon
c0582fde66 chore: Remove Debug Info 2023-11-26 18:49:48 +08:00
Pylogmon
d8d75b4afa feat: Config Tray Click Event 2023-11-26 18:35:21 +08:00
wonfen
9d4942723c fix: portable CI again 2023-11-23 15:01:30 +08:00
wonfen
e5b1ece5c0 chore: change preview gif and fix portable CI 2023-11-23 14:34:11 +08:00
wonfen
1b25bfbf5a fix: CI portable 2023-11-23 13:42:16 +08:00
wonfen
7c64d9f42a Merge pull request #3 from Kuingsmile/main
fix: Downgrade runas to fix service install bug
2023-11-23 11:11:06 +08:00
wonfen
61f9f4ef58 chore: update clash.meta core to 2023.11.23, change win icons. 2023-11-23 11:10:32 +08:00
Kuingsmile
b8eac56213 fix: Downgrade runas to fix service install bug 2023-11-22 17:17:12 -08:00
wonfen
ab5fb5749b Merge pull request #2 from Kuingsmile/main
feat: Update DialogContent width in EditorViewer
2023-11-23 07:46:12 +08:00
Kuingsmile
e0fb9a1b25 feat: Update DialogContent width in EditorViewer
component
2023-11-22 06:49:47 -08:00
wonfen
97e717f646 Merge pull request #1 from Kuingsmile/main
feat: add UWP loopback tools
2023-11-22 16:23:38 +08:00
Kuingsmile
caa82ad1e6 feat: add UWP loopback tools 2023-11-22 00:15:41 -08:00
wonfen
7ec251ea6d chore: UI adjustment, add translation, fix CI 2023-11-22 14:52:14 +08:00
wonfen
675b0c3cca chore: update readme & fix updater issue 2023-11-22 07:31:38 +08:00
wonfen
05e54d4b7f chore: remove old updater CI 2023-11-22 07:06:08 +08:00
wonfen
b4527c90e5 Merge remote-tracking branch 'nyanpasu/main' 2023-11-22 06:48:30 +08:00
wonfen
dbc626734d chore: delete clash core, update CI, change profile name, change URL test link 2023-11-22 02:56:47 +08:00
keiko233
3f2ce3cb80 chore: fix typos 2023-11-16 11:31:06 +08:00
keiko233
0820d6d6fb chore: add updater workflow 2023-11-16 10:59:27 +08:00
keiko233
a8c74e39c2 chore: remove un supported platform
* I don't have the relevant equipment to test with
* May support in future
2023-11-16 10:56:03 +08:00
keiko233
d58c29907d chore: no need for second build 2023-11-16 10:41:28 +08:00
keiko233
2322733427 chore: fix missing assets type 2023-11-16 10:37:26 +08:00
keiko233
d76e8fe85a chore: remove un supported platform 2023-11-16 10:30:25 +08:00
keiko233
0c2e4fe34a chore: fix typos 2023-11-16 10:30:01 +08:00
keiko233
30b6c87a49 docs: add preview gif 2023-11-15 15:17:51 +08:00
keiko233
540221467a chore: clean up workflows 2023-11-15 14:37:10 +08:00
keiko233
d44d331a78 Bump Version 1.4.0 2023-11-15 14:06:29 +08:00
keiko233
595554f18a chore: add release build 2023-11-15 13:52:56 +08:00
keiko233
84d3c3f7eb fix: rust lint 2023-11-15 13:49:06 +08:00
keiko233
890f55c9dc chore: test: use pnpm 2023-11-13 14:49:28 +08:00
keiko233
575a14e1f3 feat: minor tweaks 2023-11-12 00:10:23 +08:00
keiko233
91074bebd6 feat: Nyanpasu Misc 2023-11-12 00:09:32 +08:00
keiko233
3459a16b48 chore: switch updater endpoints 2023-11-12 00:08:23 +08:00
keiko233
88c9b6849f fix: valid with unified-delay & tcp-concurrent 2023-11-11 22:34:30 +08:00
keiko233
d87bb25baf chore: delete current release assets 2023-11-11 22:27:28 +08:00
keiko233
34cb796505 feat: add baseContentIn animation 2023-11-11 19:34:03 +08:00
keiko233
1eb34e0662 feat: add route transition 2023-11-11 18:32:11 +08:00
keiko233
01d631033f feat: Material You! 2023-11-11 17:12:57 +08:00
keiko233
84b2c07340 fix: touchpad scrolling causes blank area to appear 2023-11-11 15:03:41 +08:00
keiko233
a86eeb636d chore: drop upload-artifact & use prerelease 2023-11-11 15:00:54 +08:00
keiko233
417a5a8214 chore: fix: typos
* ↑ baka
2023-11-10 11:50:01 +08:00
keiko233
bcf40dde8c fix: typos 2023-11-10 11:35:38 +08:00
keiko233
cad87484c7 fix: download clash core from backup repo 2023-11-10 11:26:05 +08:00
keiko233
b8ad641ed6 chore: add dev workflow 2023-11-10 10:38:41 +08:00
keiko233
ae8197be8b feat: default disable ipv6 2023-11-10 09:10:15 +08:00
keiko233
eafb0274a7 feat: default enable unified-delay & tcp-concurrent with use meta core 2023-11-10 09:08:17 +08:00
keiko233
71a23a4e02 refactor: copy_clash_env 2023-11-09 10:52:52 +08:00
keiko233
26fd90dfa3 feat: support copy CMD & PowerShell proxy env 2023-11-09 09:58:17 +08:00
keiko233
e393ebede2 fix: use meta Country.mmdb 2023-11-09 09:35:41 +08:00
keiko233
2981bb3f19 feat: default use meta core
* :)
2023-11-09 09:34:03 +08:00
keiko233
6e9f05abb1 feat: update Clash Default bypass addrs
*Enabling TUN will cause the local address to go through the proxy
2023-11-09 09:33:38 +08:00
GyDi
9df1115380 chore: fix check 2023-11-03 16:00:34 +08:00
GyDi
f22e360cbb chore: fix check script 2023-11-03 15:52:36 +08:00
MZhao
67769af6f4 feat: new windows tray icons (#899) 2023-11-03 15:01:46 +08:00
Aromia
b1a9a1d6d9 chore: update download links in README.md (#896) 2023-11-03 11:21:20 +08:00
Goooler
cf0606ecb7 chore: bump github actions (#868)
https://github.com/actions/checkout/releases/tag/v4.0.0
https://github.com/actions/setup-node/releases/tag/v4.0.0
2023-11-02 20:14:05 +08:00
neovali
7287edcd6f chore: add fedora linux install description (#874) 2023-11-02 20:13:24 +08:00
GyDi
e0d26203dd feat: support auto clean log files 2023-11-02 20:12:46 +08:00
GyDi
7e3a85e9da fix: latency url empty 2023-11-01 23:22:30 +08:00
GyDi
5a0fed9c93 feat: increase the concurrency of latency test 2023-11-01 20:52:38 +08:00
GyDi
1f1e743912 fix: change default port 2023-10-31 14:56:19 +08:00
GyDi
b4301ed0d5 fix: csp 2023-10-31 12:00:57 +08:00
GyDi
b5391560fc v1.3.8 2023-10-31 01:29:20 +08:00
GyDi
718989cbcf chore: update log 2023-10-31 01:28:57 +08:00
GyDi
d0aee76962 fix: add default valid key 2023-10-30 17:10:51 +08:00
GyDi
fb08af96bd fix: page null exception, close #821 2023-10-30 00:53:24 +08:00
GyDi
510a0c5e70 feat: adjust the delay display interval and color, close #836 2023-10-29 23:01:05 +08:00
keiko233
89bdc8ec75 chore: update dependencies 2023-10-22 00:44:01 +08:00
keiko233
ae25ade318 chore: tsconfig: skip Lib Check 2023-10-21 23:53:57 +08:00
keiko233
dd5e46a8a7 feat: theme: change color 2023-10-21 17:30:50 +08:00
keiko233
f7218aaa9e chore: update README.md 2023-10-21 17:02:13 +08:00
keiko233
ee79bcfc44 feat: profiles: import btn with loading state 2023-10-21 16:50:46 +08:00
keiko233
2c2c174874 feat: profile-viewer: handleOk with loading state 2023-10-21 16:47:39 +08:00
keiko233
f57c49ce3a feat: base-dialog: okBtn use LoadingButton 2023-10-21 16:47:10 +08:00
keiko233
06121acfac chore: import MUI Lab 2023-10-21 16:43:30 +08:00
keiko233
2078ce7446 chore: Update dependencies 2023-10-21 16:43:16 +08:00
keiko233
49f41abfdb feat: Nyanpasu Misc 2023-10-20 11:52:40 +08:00
keiko233
70fcfe6d6c feat: Theme support modify --background-color 2023-10-20 11:12:49 +08:00
keiko233
b060b4b9bf feat: settings use Grid layout 2023-10-20 10:05:03 +08:00
keiko233
ee2135bfb3 chore: Use your own update endpoints and public keys 2023-10-19 09:37:31 +08:00
keiko233
7daa322441 chore: readme remove ad 2023-10-18 13:38:58 +08:00
GyDi
74252cb66b chore: fix rust lint 2023-10-18 13:38:03 +08:00
GyDi
5fe2be031f chore: fix rust lint 2023-10-11 14:55:20 +08:00
GyDi
2ba3aaba47 chore: fix alpha ci 2023-10-11 14:31:44 +08:00
GyDi
ad8903991c fix: try fix csp 2023-10-11 14:21:56 +08:00
GyDi
3e5624c570 chore: update readme 2023-10-11 00:04:56 +08:00
keiko233
88d3bba300 feat: add Connections Info to ConnectionsPage
*Add Upload Traffic, Download Traffic and Active Connections to ConnectionsPage.
*IConnections uploadTotal and downloadTotal data missing not displayed, add it to ConnectionsPage interface here.
2023-10-10 17:05:31 +08:00
Majokeiko
f5ee6f3537 feat: ClashFieldViewer BaseDialog maxHeight usage percentage (#813)
*The overall interface will be more intuitive when the content is longer.
2023-10-10 14:29:27 +08:00
GyDi
afc77e7adc chore: update readme 2023-10-10 10:25:57 +08:00
GyDi
024f42fce6 chore: update readme 2023-10-10 10:25:01 +08:00
GyDi
8a5f12b97c chore: update clash meta 2023-10-08 21:45:12 +08:00
GyDi
954b21cf39 chore: update readme 2023-09-11 10:24:44 +08:00
GyDi
74d095774d chore: fix updater 2023-09-11 10:16:54 +08:00
GyDi
17a2722e6d chore: fix updater 2023-09-11 10:12:40 +08:00
GyDi
c843bddbfe chore: fix clash map, close #736 2023-09-10 19:06:02 +08:00
GyDi
3f22a49755 v1.3.7 2023-09-10 19:02:10 +08:00
GyDi
7af2ffcebf chore: update log 2023-09-10 19:01:59 +08:00
GyDi
de90c959e0 chore: update auto launch 2023-09-10 18:45:17 +08:00
GyDi
9987dc1eb4 fix: i18n 2023-09-10 15:03:29 +08:00
GyDi
3efd575dd2 feat: add Open Dashboard to the hotkey, close #723 2023-09-10 14:46:03 +08:00
GyDi
f4c7b17a87 feat: add check for updates button, close #766 2023-09-10 14:30:31 +08:00
GyDi
16d80718cb fix: fix page undefined exception, close #770 2023-09-10 13:45:18 +08:00
GyDi
ad228d53b7 feat: add paste and clear icon 2023-09-09 16:52:00 +08:00
Majokeiko
15ee1e531b feat: Subscription URL TextField use multiline (#761)
*Subscription link that are too long can make reading difficult, so use multiline TextField.
2023-09-07 16:14:42 +08:00
GyDi
1c8fb3392a chore: update mmdb 2023-08-28 15:06:13 +08:00
GyDi
8647866a32 fix: set min window size, close #734 2023-08-28 15:00:27 +08:00
GyDi
23351c4f1c chore: change ubuntu ci version 2023-08-28 14:44:09 +08:00
GyDi
1367c304cf chore: update clash 2023-08-28 14:22:50 +08:00
GyDi
26d6bcb074 chore: update release link 2023-08-14 11:11:15 +08:00
GyDi
b0d651ece1 chore: update clash meta 2023-08-14 11:09:19 +08:00
GyDi
b6d50ba6a4 fix: rm debug code 2023-08-12 19:16:20 +08:00
GyDi
b3ab6a9166 v1.3.6 2023-08-12 16:12:03 +08:00
GyDi
f39a5ac9c2 chore: update log 2023-08-12 16:11:50 +08:00
GyDi
38a9a9240d fix: use sudo when pkexec not found 2023-08-12 15:58:37 +08:00
GyDi
241b22a465 chore: update ci 2023-08-12 15:41:26 +08:00
GyDi
741abc0366 feat: show loading when change profile 2023-08-05 22:07:30 +08:00
GyDi
7854775de5 fix: remove div 2023-08-05 21:43:58 +08:00
GyDi
e62eaa6b4b fix: list key 2023-08-05 21:43:05 +08:00
GyDi
b4cce23ef4 feat: support proxy provider update 2023-08-05 21:38:44 +08:00
GyDi
2bcaf90fc8 feat: add repo link 2023-08-05 19:54:59 +08:00
GyDi
96ffbe2f84 feat: support clash meta memory usage display 2023-08-05 19:40:23 +08:00
GyDi
6f5acee1c3 fix: websocket disconnect when window focus 2023-08-05 17:21:15 +08:00
GyDi
54e491d8bf feat: supports show connection detail 2023-08-05 16:52:14 +08:00
GyDi
ab6374e278 chore: update geo data to meta, close #707 2023-08-05 13:42:37 +08:00
GyDi
2fda4c9f67 chore: fix faq 2023-08-05 13:41:30 +08:00
GyDi
5138a45b0f chore: add faq and download link 2023-08-05 13:38:19 +08:00
GyDi
b224d4fa8a chore: add promotion 2023-08-05 12:57:48 +08:00
whitemirror33
a552e44483 feat: update connection table with wider process column and click to show full detail (#696) 2023-08-04 14:36:28 +08:00
GyDi
0cf3bba118 feat: more trace logs 2023-08-04 14:15:15 +08:00
Andrei Shevchuk
2c48ea3508 feat: Add Russian Language (#697)
* Add Russian Language

* Add Russian support

* Minor update

* Update Russian translation
2023-08-03 11:07:58 +08:00
GyDi
b9b6212b75 fix: try fix undefined error 2023-07-28 09:26:06 +08:00
GyDi
b978aaec21 chore: alpha ci 2023-07-26 17:07:20 +08:00
GyDi
af704681d9 feat: center window when out of monitor 2023-07-24 20:55:26 +08:00
GyDi
1443ddfe6c chore: fix test ci 2023-07-24 09:59:13 +08:00
GyDi
54457a3e1b chore: fix test ci 2023-07-24 09:47:05 +08:00
GyDi
bf180e6a2c chore: test ci 2023-07-24 09:28:54 +08:00
GyDi
864a5820c9 chore: test ci 2023-07-24 09:13:19 +08:00
GyDi
4d3ca49c3f v1.3.5 2023-07-23 13:31:51 +08:00
GyDi
c49c3cf7f0 chore: update log 2023-07-23 13:31:34 +08:00
GyDi
5d5ab57469 fix: blurry tray icon in Windows 2023-07-23 13:25:54 +08:00
GyDi
31978d8de0 chore: update clash core 2023-07-23 13:11:35 +08:00
GyDi
e8eb68bf24 chore: fix check script 2023-07-23 13:11:17 +08:00
GyDi
9ea08f4fed chore: update issue templates 2023-07-22 23:04:13 +08:00
GyDi
fe078a5c5b v1.3.4 2023-07-22 20:38:08 +08:00
GyDi
61933954f3 chore: update log 2023-07-22 20:37:22 +08:00
GyDi
4c243638cb fix: enable context menu in editable element 2023-07-22 17:21:04 +08:00
GyDi
02ba04b5d8 feat: support copy environment variable 2023-07-22 15:35:32 +08:00
GyDi
4f158a4829 fix: save window size and pos in Windows 2023-07-22 13:13:16 +08:00
GyDi
177a22df59 feat: save window size and position 2023-07-22 10:58:16 +08:00
GyDi
6b0ca2966e feat: app log level add silent 2023-07-22 09:25:54 +08:00
GyDi
aadfaf7150 feat: overwrite resource file according to file modified 2023-07-22 09:18:54 +08:00
GyDi
b307b9a66b feat: support app log level settings 2023-07-22 08:53:37 +08:00
Kimiblock Moe
6c1ab6002d feat: Use polkit to elevate permission instaed of sudo (#678) 2023-07-21 23:05:33 +08:00
GyDi
9638eefc91 chore: update check script 2023-07-11 13:25:55 +08:00
GyDi
9e9c4ad587 fix: optimize traffic graph high CPU usage when hidden 2023-07-10 23:44:09 +08:00
GyDi
ce231431b9 fix: remove fallback group select status, close #659 2023-07-10 23:16:19 +08:00
GyDi
06e1e14e02 chore: update clash 2023-07-10 13:36:17 +08:00
GyDi
416e7884f5 feat: add unified-delay field 2023-06-30 13:58:51 +08:00
GyDi
d579222007 v1.3.3 2023-06-30 09:38:33 +08:00
GyDi
30243c84cd chore: update log 2023-06-30 09:35:49 +08:00
GyDi
3557a77645 fix: error boundary with key 2023-06-30 09:17:59 +08:00
GyDi
97be28638b chore: update clash meta 2023-06-30 09:02:26 +08:00
GyDi
aba0826c38 fix: connections is null 2023-06-30 09:02:17 +08:00
GyDi
f032228d0e fix: font family not works in some interfaces, close #639 2023-06-29 14:21:14 +08:00
GyDi
6cf174c5ed fix: encodeURIComponent secret 2023-06-29 14:15:57 +08:00
GyDi
c2109d245f chore: update clash & clash meta 2023-06-08 13:52:40 +08:00
GyDi
6a9745171e feat: add error boundary to the app root 2023-06-08 13:50:45 +08:00
GyDi
f9a68e8b23 fix: encode controller secret, close #601 2023-06-08 13:48:58 +08:00
GyDi
6e391df5ee fix: linux not change icon 2023-05-28 18:14:11 +08:00
GyDi
f5edca94d3 fix: try fix blank error 2023-05-28 17:35:00 +08:00
GyDi
60046abec3 fix: close all connections when change mode 2023-05-28 17:07:39 +08:00
GyDi
cafc2060b8 fix: macos not change icon 2023-05-28 16:46:17 +08:00
GyDi
b1f45752cf chore: update deps 2023-05-28 16:45:43 +08:00
GyDi
ed17551170 chore: update clash 2023-05-28 16:44:07 +08:00
w568w
ef5adab638 feat: show tray icon variants in different status (#537) 2023-05-28 10:55:39 +08:00
GyDi
fb653ff99d fix: error message null 2023-05-19 10:53:11 +08:00
GyDi
78fc47a9c4 chore: update gh proxy 2023-05-19 09:44:31 +08:00
GyDi
2a124cea61 v1.3.2 2023-05-19 00:57:22 +08:00
GyDi
4c7cc563dc chore: update log 2023-05-19 00:57:10 +08:00
GyDi
6114af4f93 fix: profile data undefined error, close #566 2023-05-18 20:02:46 +08:00
GyDi
8c31629655 chore: update clash core 2023-05-05 20:45:52 +08:00
GyDi
03c8a8edb2 chore: update deps 2023-05-05 20:27:46 +08:00
yettera765
3eeaee154f fix: import url error (#543)
use rustls instead of depending user's system tls
2023-05-05 12:08:08 +08:00
GyDi
8cf8fa7c80 v1.3.1 2023-04-11 10:04:55 +08:00
GyDi
6b4f6fc71e chore: update log 2023-04-11 10:04:28 +08:00
Tatius Titus
30c2680b6f chore: Upgrade to React 18 (#495)
* chore: Upgrade to React 18

* runfix: Add children type to FC components

* chore: Remove @types/react
2023-04-07 12:59:44 +08:00
Mr-Spade
fb7b1800cc fix: linux DEFAULT_BYPASS (#503) 2023-04-07 12:47:13 +08:00
GyDi
ff573bf377 chore: update deps 2023-04-02 11:20:18 +08:00
GyDi
0a33bb861e fix: open file with vscode 2023-04-02 11:19:48 +08:00
GyDi
728756289b chore: update deps 2023-04-02 10:09:14 +08:00
GyDi
56ccd3a0ac chore: update clash meta 2023-04-02 09:43:32 +08:00
Tatius Titus
66f3f0ba07 fix: Do not render div as a descendant of p (#494) 2023-04-02 09:37:09 +08:00
Srinivas Gowda
af5e0d589e chore: update clash core version (#476) 2023-03-27 10:59:27 +08:00
GyDi
533dc99e7d fix: use replace instead 2023-03-17 08:37:45 +08:00
GyDi
fc5ca965ba fix: escape path space 2023-03-17 08:36:19 +08:00
John Smith
9c4a46bcdb fix: escape the space in path (#451) 2023-03-17 08:20:35 +08:00
GyDi
52658886e7 fix: add target os linux 2023-03-16 23:57:34 +08:00
GyDi
8174ab7616 chore: fix ci 2023-03-16 23:54:14 +08:00
GyDi
2b6acedae1 fix: appimage path unwrap panic 2023-03-16 23:45:48 +08:00
GyDi
d00fe9c5f4 v1.3.0 2023-03-16 21:33:51 +08:00
GyDi
88aa270728 chore: update log 2023-03-16 21:33:14 +08:00
GyDi
4ae409c7f4 feat: auto restart core after grand permission 2023-03-16 21:32:39 +08:00
GyDi
9a29c9abdd feat: add restart core button 2023-03-16 20:56:37 +08:00
GyDi
66d93ea037 fix: remove esc key listener in macOS 2023-03-16 20:42:45 +08:00
GyDi
6c0066dbfb feat: support update all profiles 2023-03-16 20:32:41 +08:00
GyDi
4fde644733 chore: rm file 2023-03-16 20:31:48 +08:00
GyDi
e7841c60df fix: adjust style 2023-03-16 19:32:59 +08:00
hybo
94f647b24a chore: evaluate error context lazily (#447) 2023-03-16 17:06:52 +08:00
GyDi
630249d22a fix: adjust swr option 2023-03-16 17:03:12 +08:00
GyDi
db99b4cb54 fix: infinite retry when websocket error 2023-03-16 17:03:00 +08:00
GyDi
c77db23586 fix: type error 2023-03-16 13:51:46 +08:00
GyDi
daf66bcec4 chore: update deps 2023-03-16 13:42:27 +08:00
GyDi
8caf36349f chore: fix ci 2023-03-16 13:18:05 +08:00
GyDi
6934de58e5 chore: fix ci 2023-03-16 13:16:30 +08:00
GyDi
54a5007c01 feat: support to grant permission to clash core 2023-03-16 11:16:54 +08:00
GyDi
e25a455698 fix: do not parse log except the clash core 2023-03-16 10:34:28 +08:00
GyDi
ab429dfeb6 feat: support clash fields filter in ui 2023-03-15 08:43:24 +08:00
GyDi
c5289dc0e8 fix: field sort for filter 2023-03-15 08:43:03 +08:00
boatrainlsz
d191877002 chore: update README (#435) 2023-03-07 22:55:38 +08:00
GyDi
4d979160c2 chore: update deps 2023-03-07 00:10:49 +08:00
GyDi
d00e8f6e19 chore: update clash 2023-03-06 23:35:30 +08:00
GyDi
91b77e5237 fix: add meta fields 2023-02-18 00:46:03 +08:00
GyDi
5b8c246d53 chore: update clash 2023-02-18 00:09:40 +08:00
GyDi
b374b9b91c feat: open dir on the tray 2023-02-17 00:15:21 +08:00
GyDi
403717117e fix: runtime config user select 2023-02-17 00:00:23 +08:00
GyDi
027295d995 feat: support to disable clash fields filter 2023-02-16 23:52:55 +08:00
GyDi
c9b7eccbc1 fix: app_handle as_ref 2023-02-15 17:25:35 +08:00
GyDi
2b6d9348cd fix: use crate 2023-02-11 23:31:55 +08:00
GyDi
692f8c8454 fix: appimage auto launch, close #403 2023-02-11 23:19:08 +08:00
inRm3D
6783355c4d chore: add openssl depend (#395) 2023-02-02 10:54:15 +08:00
GyDi
fb9cca1e99 v1.2.3 2023-02-01 22:11:34 +08:00
GyDi
eb770ede1a chore: update log 2023-02-01 22:11:06 +08:00
GyDi
2643e853af chore: aarch script 2023-02-01 21:41:15 +08:00
GyDi
b79456e91b chore: update clash 2023-01-30 20:50:08 +08:00
GyDi
ac66c086f8 chore: meta ci 2023-01-30 20:40:57 +08:00
GyDi
ebccf401dd chore: fix ci 2023-01-22 13:31:34 +08:00
GyDi
66494845b7 chore: fix ci 2023-01-19 18:02:08 +08:00
GyDi
e6c36ad602 chore: alpha ci 2023-01-18 23:39:27 +08:00
inRm3D
26379182db chore: try fix missing libssl3 (#378)
* Update ci.yml

* use package from source
2023-01-18 23:36:18 +08:00
GyDi
bba03d14d4 fix: compatible with UTF8 BOM, close #283 2023-01-17 19:51:02 +08:00
GyDi
23b728a762 feat: adjust macOS window style 2023-01-16 22:57:53 +08:00
GyDi
819c5207d2 fix: use selected proxy after profile changed 2023-01-15 21:33:03 +08:00
GyDi
311358544e fix: error log 2023-01-15 19:11:51 +08:00
GyDi
4480ecc96d chore: ci 2023-01-14 22:22:18 +08:00
GyDi
6c5f70a205 v1.2.2 2023-01-14 22:01:28 +08:00
GyDi
99adfb4a9e chore: update log 2023-01-14 21:56:27 +08:00
GyDi
7909cf4067 fix: adjust fields order 2023-01-14 14:57:55 +08:00
GyDi
780ab20aeb fix: add meta fields 2023-01-14 14:51:29 +08:00
GyDi
73119bb7c5 chore: ci 2023-01-14 12:20:17 +08:00
GyDi
f20f0f064e chore: update clash meta 2023-01-14 12:16:09 +08:00
GyDi
1b44ae098c fix: add os platform value 2023-01-14 12:07:31 +08:00
GyDi
453c230716 feat: recover core after panic, close #353 2023-01-14 11:45:47 +08:00
GyDi
439d885ee1 chore: allow unused 2023-01-13 23:11:48 +08:00
GyDi
43dee3ef76 fix: reconnect traffic websocket 2023-01-13 23:02:45 +08:00
GyDi
c71ba6ff8d chore: update deps 2023-01-13 21:29:21 +08:00
GyDi
fb7a36eb73 feat: use decorations in Linux, close #354 2023-01-12 00:42:33 +08:00
GyDi
e7f294a065 fix: parse bytes precision, close #334 2023-01-11 14:05:49 +08:00
GyDi
d5037f180e fix: trigger new profile dialog, close #356 2023-01-11 13:45:16 +08:00
GyDi
e90158809a fix: parse log cause panic 2023-01-11 13:30:14 +08:00
GyDi
0cb802ed9a chore: update clash meta 2023-01-09 21:55:03 +08:00
GyDi
d0b47204f4 v1.2.1 2022-12-23 22:44:27 +08:00
GyDi
351cb391e5 chore: update log 2022-12-23 22:44:00 +08:00
GyDi
051be927cd fix: avoid setting login item repeatedly, close #326 2022-12-23 22:39:27 +08:00
GyDi
8bad2c2113 fix: adjust code 2022-12-15 21:54:48 +08:00
GyDi
2bcf6fb3eb fix: adjust delay check concurrency 2022-12-15 12:23:57 +08:00
GyDi
d1ba0ed2b2 chore: adjust code 2022-12-15 12:22:20 +08:00
GyDi
6e421e60c5 fix: change default column to auto 2022-12-14 16:56:33 +08:00
GyDi
8385050804 fix: change default app version 2022-12-14 16:15:53 +08:00
GyDi
bfe4f08232 fix: adjust rule ui 2022-12-14 15:59:41 +08:00
GyDi
132f914b0d fix: adjust log ui 2022-12-14 15:49:05 +08:00
GyDi
97d82b03ab fix: keep delay data 2022-12-14 15:29:02 +08:00
GyDi
f06fa3f9b7 fix: use list item button 2022-12-14 15:16:49 +08:00
GyDi
6337788a22 fix: proxy item style 2022-12-14 15:15:44 +08:00
GyDi
024db4358b feat: auto proxy layout column 2022-12-14 15:07:51 +08:00
GyDi
173f35487e chore: fix ci 2022-12-13 18:13:28 +08:00
GyDi
74cbe82dd1 chore: fix ci 2022-12-13 18:11:43 +08:00
GyDi
a8c30d30a9 chore: fix ci 2022-12-13 18:02:23 +08:00
GyDi
1b7a52d5af chore: alpha ci 2022-12-13 17:44:46 +08:00
GyDi
e5109789bf chore: alpha ci 2022-12-13 17:41:53 +08:00
GyDi
4d2b35e09d feat: support to change proxy layout column 2022-12-13 17:34:39 +08:00
GyDi
5c5177ec57 feat: support to open core dir 2022-12-13 00:44:24 +08:00
GyDi
d93b00cd15 chore: update deps 2022-12-12 00:00:45 +08:00
MoeShin
39ade59174 fix: Virtuoso no work in legacy browsers (#318) 2022-12-08 10:47:42 +08:00
GyDi
0309c815b9 chore: update deps 2022-12-04 20:54:43 +08:00
GyDi
ce613098db fix: adjust ui 2022-12-04 00:45:39 +08:00
GyDi
ab34044196 feat: profile page ui 2022-11-28 22:29:58 +08:00
GyDi
17f724748f chore: deps 2022-11-28 22:27:23 +08:00
GyDi
f3a917b5e7 chore: update clash version 2022-11-26 13:25:52 +08:00
GyDi
376011ea08 fix: refresh websocket 2022-11-25 22:22:57 +08:00
GyDi
2ce7624c14 fix: adjust ui 2022-11-24 18:24:34 +08:00
GyDi
c62dddd5b9 feat: save some fields in the runtime config, close #292 2022-11-24 17:52:25 +08:00
GyDi
490ba9f140 chore: alpha ci 2022-11-24 11:14:03 +08:00
GyDi
f76890cc56 fix: parse bytes base 1024 2022-11-24 11:11:31 +08:00
GyDi
e1c8f1fed9 fix: add clash fields 2022-11-24 10:46:21 +08:00
GyDi
6e19a4ab8b chore: ci 2022-11-24 10:41:03 +08:00
GyDi
b156523a7f chore: ci 2022-11-24 10:35:22 +08:00
GyDi
d20b745ae5 fix: direct mode hide proxies 2022-11-24 10:35:09 +08:00
GyDi
c51e9e6b2c feat: add meta feature 2022-11-24 10:26:41 +08:00
GyDi
1a31fa9067 fix: profile can not edit 2022-11-24 10:26:25 +08:00
GyDi
558b8499af chore: ci 2022-11-23 23:46:32 +08:00
GyDi
986c162988 v1.2.0 2022-11-23 23:15:39 +08:00
GyDi
770d5cd11c chore: ci 2022-11-23 23:15:20 +08:00
GyDi
446d2ab3af chore: update log 2022-11-23 23:13:44 +08:00
GyDi
2f9bf7f063 feat: display proxy group type 2022-11-23 18:27:57 +08:00
GyDi
72ff9c0964 chore: rm dead code 2022-11-23 17:45:49 +08:00
GyDi
33b1a11d85 fix: parse logger time 2022-11-23 17:45:43 +08:00
GyDi
06dabf1e4e fix: adjust service mode ui 2022-11-23 17:45:22 +08:00
GyDi
28d3691e0b feat: add use clash hook 2022-11-23 17:44:40 +08:00
GyDi
ffa21fbfd2 fix: adjust style 2022-11-23 17:42:01 +08:00
GyDi
db028665fd fix: check hotkey and optimize hotkey input, close #287 2022-11-23 17:30:19 +08:00
GyDi
6bc83d9f27 fix: mutex dead lock 2022-11-23 16:04:25 +08:00
GyDi
790d832155 fix: adjust item ui 2022-11-23 15:29:42 +08:00
GyDi
f477cecdeb fix: regenerate config before change core 2022-11-23 09:57:21 +08:00
GyDi
e031389021 fix: close connections when profile change 2022-11-23 00:20:57 +08:00
GyDi
e00f826eb8 fix: lint 2022-11-22 23:02:18 +08:00
GyDi
24f4e8ab99 chore: fix check script 2022-11-22 23:01:04 +08:00
GyDi
1550d528bd fix: windows service mode 2022-11-22 22:01:34 +08:00
GyDi
40c041031e fix: init config file 2022-11-22 22:01:19 +08:00
GyDi
3e555ec9f1 chore: fix mmdb url 2022-11-22 21:15:45 +08:00
GyDi
5098f14aab fix: service mode error and fallback to sidecar 2022-11-22 20:47:21 +08:00
GyDi
a355a9c85e fix: service mode viewer ui 2022-11-22 20:45:17 +08:00
GyDi
e7db2a8573 chore: update check script 2022-11-22 20:44:44 +08:00
GyDi
9b18bd0b48 fix: create theme error, close #294 2022-11-22 16:01:33 +08:00
GyDi
f95ddd594e feat: guard the mixed-port and external-controller 2022-11-22 15:45:17 +08:00
GyDi
fe8168784f feat: adjust builtin script and support meta guard script 2022-11-22 12:00:48 +08:00
MoeShin
4046f143f6 fix: matchMedia().addEventListener #258 (#296) 2022-11-22 09:25:39 +08:00
GyDi
e4e16999c8 chore: alpha release add portable 2022-11-22 00:16:30 +08:00
GyDi
10f3ba4ff4 chore: alpha ci 2022-11-21 23:19:25 +08:00
GyDi
3cd2be5081 fix: check config 2022-11-21 23:11:56 +08:00
GyDi
c9359978f9 fix: show global when no rule groups 2022-11-21 23:06:32 +08:00
GyDi
781c67b31a fix: service viewer ref 2022-11-21 23:02:48 +08:00
GyDi
020bd129fb fix: service ref error 2022-11-21 22:33:06 +08:00
GyDi
8086b6d78c feat: disable script mode when use clash meta 2022-11-21 22:28:57 +08:00
GyDi
48e14b36b8 feat: check config when change core 2022-11-21 22:27:55 +08:00
GyDi
b3c1c56579 fix: group proxies render list is null 2022-11-21 22:10:24 +08:00
GyDi
bd0e932910 feat: support builtin script for enhanced mode 2022-11-21 21:05:00 +08:00
GyDi
525e5f88ae chore: update ci 2022-11-20 23:17:05 +08:00
GyDi
005eeb0e0b chore: update ci node version 2022-11-20 23:14:43 +08:00
GyDi
d21bb015e8 fix: pretty bytes 2022-11-20 23:08:30 +08:00
GyDi
7338838b0e feat: adjust profiles page ui 2022-11-20 22:37:34 +08:00
GyDi
8bb4803ff9 refactor: adjust base components export 2022-11-20 22:03:55 +08:00
GyDi
892b919cf3 refactor: adjust setting dialog component 2022-11-20 21:48:39 +08:00
GyDi
572d81ecef fix: use verge hook 2022-11-20 20:12:58 +08:00
GyDi
a4ce7a4037 feat: optimize proxy page ui 2022-11-20 19:46:16 +08:00
GyDi
6eafb15cf9 chore: adjust type 2022-11-19 17:22:29 +08:00
GyDi
e19fe5ce1c fix: adjust notice 2022-11-19 01:24:07 +08:00
GyDi
9d2017e598 feat: add error boundary 2022-11-19 01:22:19 +08:00
GyDi
f33c419ed9 chore: rm polyfill 2022-11-19 01:22:00 +08:00
GyDi
f425fbaf9d feat: adjust clash log 2022-11-18 22:08:06 +08:00
GyDi
bcc5ec897a fix: windows issue 2022-11-18 20:15:34 +08:00
GyDi
f5f2fe3472 fix: change dev log level 2022-11-18 18:37:29 +08:00
GyDi
b7c3863882 fix: patch clash config 2022-11-18 18:37:17 +08:00
GyDi
d759f48ee8 fix: cmds params 2022-11-18 18:26:55 +08:00
GyDi
3a37075e71 Merge branch 'refactor' 2022-11-18 18:21:49 +08:00
GyDi
58366c0b87 chore: rm code 2022-11-18 18:19:26 +08:00
GyDi
2667ed13f1 refactor: done 2022-11-18 18:18:41 +08:00
GyDi
34daffbc96 refactor: adjust all path methods and reduce unwrap 2022-11-18 10:26:39 +08:00
GyDi
be81cd72af fix: adjust singleton detect 2022-11-18 08:24:27 +08:00
GyDi
4ae00714d2 fix: change template 2022-11-18 08:04:26 +08:00
GyDi
f24cbb6692 refactor: rm code 2022-11-17 23:21:13 +08:00
GyDi
5a35c5b928 refactor: fix 2022-11-17 22:53:41 +08:00
GyDi
1880da6351 refactor: rm dead code 2022-11-17 22:52:22 +08:00
GyDi
df93cb103c refactor: for windows 2022-11-17 20:19:40 +08:00
GyDi
63b474a32c refactor: wip 2022-11-17 17:07:13 +08:00
GyDi
abdbf158d1 refactor: wip 2022-11-16 01:26:41 +08:00
GyDi
ee68d80d0a refactor: wip 2022-11-15 01:33:50 +08:00
GyDi
c8e6f3a627 refactor: rm update item block_on 2022-11-14 23:07:51 +08:00
GyDi
dc941575fe refactor: fix 2022-11-14 22:50:47 +08:00
GyDi
e64103e5f2 fix: copy resource file 2022-11-14 21:11:42 +08:00
GyDi
f0ab03a9fb chore: tmpl add clash core 2022-11-14 21:10:29 +08:00
GyDi
09965f1cc6 feat: add draft 2022-11-14 19:31:22 +08:00
GyDi
d2852bb34a refactor: fix 2022-11-14 01:45:58 +08:00
GyDi
b03c52a501 refactor: wip 2022-11-14 01:26:33 +08:00
GyDi
fd6633f536 fix: MediaQueryList addEventListener polyfill 2022-11-13 10:27:26 +08:00
GyDi
320ac81f48 chore: fix ci 2022-11-12 17:07:47 +08:00
GyDi
70bcd2428f chore: alpha ci 2022-11-12 17:06:26 +08:00
GyDi
71f5ada0a3 chore: lock version 2022-11-12 17:06:18 +08:00
GyDi
aab5141404 chore: fix ci 2022-11-12 16:54:57 +08:00
GyDi
ff6b119f27 chore: alpha ci 2022-11-12 16:47:27 +08:00
GyDi
7ef4b7eeb8 fix: change default tun dns-hijack 2022-11-12 11:48:02 +08:00
GyDi
4668be6e24 chore: format rust code 2022-11-12 11:37:23 +08:00
GyDi
a211fc7c97 fix: something 2022-11-11 22:45:32 +08:00
GyDi
54af0b675d chore: update deps 2022-11-11 21:28:34 +08:00
GyDi
8c8171e774 feat: change default latency test url 2022-11-11 01:24:18 +08:00
GyDi
0bb1790206 fix: provider proxy sort by delay 2022-11-11 01:21:23 +08:00
GyDi
45fc84d8be chore: rm dead code 2022-11-10 22:58:46 +08:00
GyDi
f7500f4cad chore: clash meta compatible and geosite.dat 2022-11-10 22:58:34 +08:00
GyDi
0cfd718d8a feat: auto close connection when proxy changed 2022-11-10 01:27:05 +08:00
GyDi
5f486d0f51 fix: profile item menu ui dense 2022-11-09 15:53:42 +08:00
GyDi
6e5a2f85a1 fix: disable auto scroll to proxy 2022-11-09 11:44:09 +08:00
GyDi
e66a89208d feat: support to change external controller 2022-11-06 23:23:26 +08:00
GyDi
7f65c501c6 feat: add sub-rules 2022-11-05 15:50:06 +08:00
GyDi
22c2382765 feat(macOS): support cmd+w and cmd+q 2022-11-04 00:51:46 +08:00
GyDi
38e1a4febf chore: update clash meta 2022-11-04 00:36:32 +08:00
GyDi
8db554b377 chore: fix ci 2022-11-03 01:34:23 +08:00
GyDi
5f5cc55331 chore: compatible ci 2022-11-03 01:32:16 +08:00
GyDi
2f8b39186f chore: update ci 2022-11-03 01:15:50 +08:00
GyDi
a3a724e2e6 chore: test ci 2022-11-03 00:51:49 +08:00
GyDi
73235c8699 chore: test ci 2022-11-03 00:48:27 +08:00
GyDi
7e5999e862 chore: add x 2022-11-02 00:57:27 +08:00
GyDi
afa244dcb0 v1.1.2 2022-11-02 00:54:51 +08:00
GyDi
4ea5bb2390 fix: check remote profile 2022-11-02 00:51:25 +08:00
GyDi
e545d552f6 chore: update log 2022-11-02 00:44:45 +08:00
GyDi
56fe7b3596 feat: add version on tray 2022-11-01 23:29:59 +08:00
GyDi
eb28ec866a feat: add animation 2022-10-29 23:36:10 +08:00
GyDi
4649454282 fix: remove smoother 2022-10-29 20:05:55 +08:00
angrylid
a45dc6efda feat: add animation to ProfileNew component (#252)
* chore: add .vscode to .gitignore

* feat: add animation to ProfileNew component
2022-10-29 20:02:51 +08:00
GyDi
9e56b9fbb5 fix: icon button color 2022-10-29 19:22:15 +08:00
GyDi
5504994cb9 feat: check remote profile field 2022-10-28 13:00:52 +08:00
GyDi
acff6d0432 fix: init system proxy correctly 2022-10-28 01:33:21 +08:00
GyDi
eea9cb7c5b fix: open file 2022-10-28 01:26:45 +08:00
GyDi
c0ddddfb1f fix: reset proxy 2022-10-28 01:06:54 +08:00
GyDi
f7dab3ca56 fix: init config error 2022-10-28 01:02:47 +08:00
GyDi
e11b4038a3 feat: system tray support zh language 2022-10-28 00:40:29 +08:00
GyDi
b635e64803 fix: adjust reset proxy 2022-10-27 21:14:14 +08:00
GyDi
20a194b49a fix: adjust code 2022-10-27 21:13:27 +08:00
GyDi
33a5fb8837 fix: add https proxy 2022-10-26 17:11:54 +08:00
GyDi
59dae640db fix: auto scroll into view when sorted proxies changed 2022-10-26 01:24:06 +08:00
GyDi
a6ac75e97b feat: display delay check result timely 2022-10-26 01:11:02 +08:00
GyDi
cc5b33a8ec fix: refresh proxies interval, close #235 2022-10-26 01:08:34 +08:00
GyDi
6e1a627b84 fix: style 2022-10-25 23:54:21 +08:00
GyDi
62d4c65e1c chore: update deps 2022-10-25 23:54:10 +08:00
GyDi
600134a3ac fix: fetch profile with system proxy, close #249 2022-10-25 23:45:24 +08:00
LooSheng
df14af7337 fix: The profile is replaced when the request fails. (#246) 2022-10-23 17:22:26 +08:00
GyDi
2f740b570d fix: default dns config 2022-10-22 22:26:40 +08:00
GyDi
294d980b52 fix: kill clash when exit in service mode, close #241 2022-10-22 13:55:06 +08:00
GyDi
c9d9909d74 chore: update mmdb 2022-10-22 13:30:08 +08:00
GyDi
90eeabae7b feat: update profile with system proxy/clash proxy 2022-10-18 23:19:21 +08:00
GyDi
a32c77c5f1 chore: adjust code 2022-10-17 23:26:25 +08:00
GyDi
910846f2ce fix: icon button color inherit 2022-10-16 14:40:45 +08:00
GyDi
35d0438261 fix: app version to string 2022-10-16 14:38:57 +08:00
GyDi
515af472ce fix: break loop when core terminated 2022-10-14 14:46:15 +08:00
GyDi
f062f7f9fe feat: change global mode ui, close #226 2022-10-13 11:54:52 +08:00
GyDi
f68378041f feat: default user agent same with app version 2022-10-12 11:28:47 +08:00
GyDi
30ef3057ac v1.1.1 2022-10-11 23:03:12 +08:00
GyDi
48f3a934c9 chore: update log 2022-10-11 23:02:49 +08:00
GyDi
38d1fde84f fix: api error handle 2022-10-11 22:51:18 +08:00
GyDi
dd78670a4b fix: clash meta not load geoip, close #212 2022-10-11 22:24:54 +08:00
GyDi
b8a8190a43 fix: sort proxy during loading, close #221 2022-10-11 22:06:55 +08:00
GyDi
2d0989342f fix: not create windows when enable slient start 2022-10-11 21:50:00 +08:00
GyDi
15ff9b06a1 fix: root background color 2022-10-11 20:49:03 +08:00
GyDi
41b19f69de fix: create window correctly 2022-10-11 00:57:34 +08:00
GyDi
f2bd6f1fce fix: set_activation_policy 2022-10-10 22:15:21 +08:00
GyDi
ec94218a4b chore: rm aarch64 ci 2022-09-28 18:50:33 +08:00
GyDi
c2e5c7cf38 chore: test ci 2022-09-28 17:26:32 +08:00
GyDi
bac5734527 chore: test ci 2022-09-28 17:12:27 +08:00
GyDi
bef4033d94 chore: fix ci 2022-09-28 16:04:05 +08:00
GyDi
64216cba67 chore: fix test ci 2022-09-28 15:19:50 +08:00
Particle_G
a9d6167a9f chore: add support for windows arm64, close #216 (#209) 2022-09-28 15:13:24 +08:00
GyDi
6c6b40548f fix: disable spell check 2022-09-28 14:15:22 +08:00
苏业钦
a916d88e85 chore: add support linux arm64 (#215) 2022-09-28 10:40:41 +08:00
GyDi
ab2f0548a3 chore: clash meta linux use compatible version 2022-09-27 00:16:22 +08:00
GyDi
e30ba07285 feat: optimize config feedback 2022-09-26 20:46:29 +08:00
GyDi
1b336d973d fix: adjust init launch on dev 2022-09-26 01:35:19 +08:00
GyDi
7b1866737f v1.1.0 2022-09-26 01:27:56 +08:00
GyDi
eedc4ab648 chore: update log 2022-09-26 01:27:32 +08:00
GyDi
5e429c7a94 fix: ignore disable auto launch error 2022-09-26 01:09:05 +08:00
GyDi
57d23eb043 fix(macos): set auto launch path to application 2022-09-25 23:36:55 +08:00
GyDi
5ebd9be89a fix: i18n 2022-09-25 01:41:46 +08:00
GyDi
5a743779e2 feat: show connections with table layout 2022-09-25 01:35:21 +08:00
GyDi
0495062110 fix: style 2022-09-24 20:55:40 +08:00
GyDi
d522191f69 fix: save enable log on localstorage 2022-09-24 19:03:14 +08:00
Priestch
bbbdc8b7a6 fix: typo in api.ts (#207) 2022-09-24 18:48:11 +08:00
GyDi
c00ed8aa5a chore: use ubuntu latest 2022-09-24 16:08:39 +08:00
GyDi
0beaa94068 feat: show loading on proxy group delay check 2022-09-24 15:31:09 +08:00
GyDi
96e76665d6 fix: refresh clash ui await patch 2022-09-24 14:01:28 +08:00
Shun Li
6423a29600 feat: add chains[0] and process to connections display (#205)
* add chains[0] display

* add metadata.process to connections
2022-09-24 13:06:32 +08:00
GyDi
8bddf30dcf refactor(hotkey): use tauri global shortcut 2022-09-23 15:31:01 +08:00
GyDi
a6b2db182d feat: adjust connection page ui 2022-09-23 00:08:52 +08:00
GyDi
b9f3f9d859 Merge pull request #197 from FoundTheWOUT/main
Support ordering connection
2022-09-22 22:55:46 +08:00
GyDi
6331447dcd feat: yaml merge key 2022-09-21 22:15:24 +08:00
GyDi
4213ee660f feat: toggle log ws 2022-09-20 22:15:28 +08:00
GyDi
3bd9287c5d chore: update deps 2022-09-20 21:26:38 +08:00
GyDi
b23d3f7c8b feat: add rule page 2022-09-18 23:19:02 +08:00
GyDi
f8d9e5e027 feat: hotkey viewer 2022-09-18 15:52:53 +08:00
GyDi
8fa7fb3b1f feat: refresh ui when hotkey clicked 2022-09-18 15:50:03 +08:00
FoundTheWOUT
63d92a0872 Support ordering connection 2022-09-15 15:52:12 +08:00
GyDi
509d83365e feat: support hotkey (wip) 2022-09-14 01:19:02 +08:00
GyDi
d6ab73c905 fix: remove dead code 2022-09-12 13:42:21 +08:00
GyDi
5a93ba05d5 fix: style 2022-09-12 13:42:13 +08:00
GyDi
2d2fdf0b1e fix: handle is none 2022-09-12 00:45:19 +08:00
GyDi
cf96622261 fix: unused 2022-09-12 00:02:25 +08:00
GyDi
47c8ccb0e5 refactor: optimize 2022-09-11 20:58:55 +08:00
GyDi
02fdb8778b fix: style 2022-09-11 18:38:51 +08:00
GyDi
6bed7f0e66 fix: windows logo size 2022-09-11 17:50:58 +08:00
GyDi
7597d335b9 feat: hide window on macos 2022-09-09 16:42:35 +08:00
GyDi
f32c5ba244 fix: do not kill sidecar during updating 2022-09-09 16:33:04 +08:00
GyDi
ab53ab21e2 fix: delay update config 2022-09-09 16:30:27 +08:00
GyDi
acfe5dbb49 fix: reduce logo size 2022-09-09 16:19:13 +08:00
GyDi
f9b91fa189 feat: system proxy setting 2022-09-07 01:51:43 +08:00
GyDi
2462e68ba1 fix: window center 2022-09-06 22:18:06 +08:00
GyDi
019b2a1681 chore: update resource 2022-09-06 15:32:22 +08:00
GyDi
e94a07b677 fix: log level warn value 2022-09-06 14:42:58 +08:00
GyDi
c058c29755 feat: change default singleton port and support to change the port 2022-09-06 00:45:01 +08:00
GyDi
9e7c7ac163 feat: log info 2022-09-05 20:30:39 +08:00
GyDi
fcee41f00d feat: kill clash by pid 2022-09-05 16:30:29 +08:00
GyDi
ff2c1bf8ed feat: change clash port in dialog 2022-09-05 02:12:25 +08:00
GyDi
a2cf26e7ed feat: add proxy item check loading 2022-09-04 23:53:48 +08:00
GyDi
71e6900375 feat: compatible with proxy providers health check 2022-09-04 22:55:54 +08:00
GyDi
3bdc98bd12 v1.0.6 2022-09-03 00:40:50 +08:00
GyDi
68d2a6e951 chore: update log 2022-09-03 00:39:39 +08:00
GyDi
acf47ac947 chore: update deps 2022-09-02 23:06:00 +08:00
GyDi
fda24e5f5a chore: update deps 2022-09-02 01:35:06 +08:00
GyDi
05eca8e4d8 chore: use ubuntu 18.04 2022-09-02 01:27:06 +08:00
GyDi
b915f3b1a9 feat: add empty ui 2022-09-02 01:09:38 +08:00
GyDi
ab58968f4d feat: complete i18n 2022-09-02 01:05:45 +08:00
GyDi
820d1e7570 fix: increase delay checker concurrency 2022-09-02 00:41:43 +08:00
GyDi
a120c8cf98 fix: external controller allow lan 2022-09-02 00:24:19 +08:00
GyDi
5a35ea116f chore: update clash meta version 2022-09-02 00:11:52 +08:00
GyDi
e72ad1f030 fix: remove useless optimizations 2022-08-31 21:44:23 +08:00
GyDi
a17362437a chore: test ci 2022-08-29 17:26:09 +08:00
GyDi
8643dc43b1 chore: update clash 2022-08-29 11:11:55 +08:00
GyDi
e1c869a358 chore: add test ci os version 2022-08-29 11:02:14 +08:00
GyDi
2f5b8d9abe Merge branch 'main' of github.com:zzzgydi/clash-verge 2022-08-24 22:41:36 +08:00
GyDi
db324f54eb chore: update auto launch 2022-08-24 22:41:12 +08:00
GyDi
3242efb1a2 fix: reduce unsafe unwrap 2022-08-23 15:19:04 +08:00
FoundTheWOUT
30f9f1a021 fix: timer restore at app launch 2022-08-23 10:35:36 +08:00
GyDi
3f58d05aa7 fix: adjust log text 2022-08-19 17:11:59 +08:00
GyDi
c611a51575 fix: only script profile can display console 2022-08-17 01:45:37 +08:00
GyDi
73458dcd28 v1.0.5 2022-08-16 23:56:29 +08:00
GyDi
23ebeb1cc0 chore: update log 2022-08-16 23:56:04 +08:00
GyDi
a7ba9f1886 fix: fill button title attr 2022-08-16 23:46:33 +08:00
GyDi
d5192e2244 feat: windows portable version do not check update 2022-08-16 01:53:40 +08:00
GyDi
7eb595170f fix: do not reset system proxy when consistent 2022-08-16 01:27:32 +08:00
GyDi
bd576ca808 fix: adjust web ui item style 2022-08-16 01:08:54 +08:00
GyDi
2f8146b11f fix: clash field state error 2022-08-16 00:48:06 +08:00
GyDi
7f321c89cb feat: adjust clash info parsing logs 2022-08-15 20:21:43 +08:00
GyDi
fa65f606b8 fix: badge color error 2022-08-15 20:15:44 +08:00
GyDi
7a3285adaf fix: web ui port value error 2022-08-15 20:14:33 +08:00
GyDi
8bf78fef10 fix: delay show window 2022-08-15 01:37:26 +08:00
GyDi
78f97ce4df feat: adjust runtime config 2022-08-15 01:30:37 +08:00
GyDi
66ccbf70f8 feat: support restart app on tray 2022-08-15 01:22:39 +08:00
GyDi
cb48600b40 fix: adjust dialog action button variant 2022-08-15 00:55:35 +08:00
GyDi
33ce235713 feat: optimize profile page 2022-08-14 23:10:19 +08:00
GyDi
f1a68ece01 fix: script code error 2022-08-12 23:41:25 +08:00
GyDi
dd563360af fix: script exception handle 2022-08-12 11:14:34 +08:00
GyDi
7f6dac4271 feat: refactor 2022-08-12 03:20:55 +08:00
GyDi
178fd8e828 feat: adjust tun mode config 2022-08-11 03:26:08 +08:00
GyDi
1641e02a7d feat: reimplement enhanced mode 2022-08-11 02:55:10 +08:00
GyDi
cfd04e9bb4 feat: use rquickjs crate 2022-08-10 13:05:48 +08:00
GyDi
38effaf740 feat: reimplement enhanced mode 2022-08-09 21:15:55 +08:00
GyDi
a68eb4a73e chore: format 2022-08-09 14:33:47 +08:00
GyDi
c02990ef98 fix: change fields 2022-08-09 14:31:59 +08:00
FoundTheWOUT
5aa7d5ffe9 fix: silent start (#150) 2022-08-09 14:14:06 +08:00
GyDi
4942b0fca5 fix: save profile when update 2022-08-08 23:17:22 +08:00
GyDi
aed1bdff5a fix: list compare wrong 2022-08-08 23:16:28 +08:00
GyDi
929c840006 fix: button color 2022-08-08 23:15:05 +08:00
GyDi
57aef1d3c2 chore: rm code 2022-08-08 22:30:13 +08:00
GyDi
c1734a094c fix: limit theme mode value 2022-08-08 22:28:44 +08:00
GyDi
99c46685ac feat: finish clash field control 2022-08-08 22:14:03 +08:00
GyDi
066b08040a feat: clash field viewer wip 2022-08-08 01:51:30 +08:00
GyDi
35de2334fb fix: add valid clash field 2022-08-07 20:52:50 +08:00
GyDi
5564c966a5 feat: support web ui 2022-08-06 21:56:54 +08:00
GyDi
0891b5e7b7 feat: adjust setting page style 2022-08-06 03:48:03 +08:00
GyDi
f3341f201f refactor: ts path alias 2022-08-06 02:35:11 +08:00
GyDi
bf0dafabe2 fix: icon style 2022-08-06 01:44:33 +08:00
GyDi
60f6587169 fix: reduce unwrap 2022-07-30 23:32:52 +08:00
GyDi
a9bf32919e feat: runtime config viewer 2022-07-25 01:20:13 +08:00
GyDi
7633f9f88b chore: fix updater script 2022-07-18 12:55:59 +08:00
GyDi
9b56233938 v1.0.4 2022-07-17 17:56:07 +08:00
GyDi
65074264b8 chore: update log 2022-07-17 17:52:41 +08:00
GyDi
4f6fceb87f feat: improve log rule 2022-07-17 17:39:44 +08:00
GyDi
659fdd1d37 fix: import mod 2022-07-17 16:51:54 +08:00
GyDi
8bce2ce040 feat: theme mode support follows system 2022-07-17 16:02:17 +08:00
GyDi
55cc83a5d4 chore: update deps 2022-07-17 14:54:09 +08:00
GyDi
6a51e93ded refactor: mode manage on tray 2022-07-13 02:26:54 +08:00
GyDi
fcf570e96e chore: rm code 2022-07-13 01:07:29 +08:00
GyDi
cbc184e953 fix: add tray separator 2022-07-13 01:05:53 +08:00
GyDi
98f9063352 Merge pull request #128 from Limsanity/main
feat: support switch proxy mode
2022-07-13 01:02:14 +08:00
limsanity
c278f1af00 style: resolve formatting problem 2022-07-13 00:54:47 +08:00
limsanity
fbb17a0ba5 feat(system tray): support switch rule/global/direct/script mode in system tray 2022-07-13 00:43:27 +08:00
GyDi
8637a9823e chore: update deps 2022-07-11 00:41:21 +08:00
GyDi
d717fe7e8c chore: update clash version 2022-07-08 01:42:14 +08:00
GyDi
5e8dfe7267 chore: update aarch rule 2022-07-07 02:54:20 +08:00
GyDi
aaa4fbcdbd chore: aarch upload script 2022-07-07 02:53:53 +08:00
GyDi
150f0cf486 fix: instantiate core after init app, close #122 2022-07-06 15:14:33 +08:00
GyDi
711b220a05 chore: update clash version 2022-07-06 00:54:01 +08:00
GyDi
b615c485f7 chore: update deps 2022-07-06 00:49:09 +08:00
GyDi
2f3b6b29ae fix: rm macOS transition props 2022-07-05 01:24:23 +08:00
GyDi
7aecd83c4a chore: update deps 2022-07-05 00:52:22 +08:00
GyDi
3b460ab91f chore: change tray icon 2022-06-22 01:26:25 +08:00
GyDi
661c0eb970 v1.0.3 2022-06-20 02:09:12 +08:00
GyDi
f4a1f1fdc8 chore: update log 2022-06-20 02:06:46 +08:00
GyDi
b5432c3728 chore: update clash version 2022-06-20 01:47:15 +08:00
GyDi
6d0625c409 fix: improve external-controller parse and log 2022-06-20 01:36:56 +08:00
GyDi
2f1ea08b8a chore: update macos icon 2022-06-18 18:41:53 +08:00
GyDi
a092da1943 fix: show windows on click 2022-06-18 17:28:37 +08:00
GyDi
43ef3cc562 chore: new logo for mac 2022-06-18 17:28:05 +08:00
GyDi
047774475c chore: update alpha ci 2022-06-17 01:18:19 +08:00
GyDi
91b8504df5 chore: update tauri 2022-06-17 01:16:46 +08:00
GyDi
a4fb2dfcf8 fix: adjust update profile notice error 2022-06-15 02:42:04 +08:00
GyDi
ba16ec02e5 fix: style issue on mac 2022-06-15 02:41:37 +08:00
GyDi
55b7af2623 chore: update version 2022-06-15 01:15:05 +08:00
GyDi
b428eff10e feat: improve yaml file error log 2022-06-14 01:40:02 +08:00
GyDi
35aee15b6d chore: update deps 2022-06-14 01:23:58 +08:00
GyDi
6f53c1bfde Merge pull request #104 from FoundTheWOUT/main
fix: check script run on all OS
2022-06-12 22:25:05 +08:00
FoundTheWOUT
db0230ed75 fix: check script run on all OS 2022-06-05 23:55:41 +08:00
GyDi
7fa3c1e12a feat: save proxy page state 2022-06-04 18:55:39 +08:00
GyDi
3b5993652f chore: portable script 2022-06-02 00:56:05 +08:00
GyDi
be4ad8947f chore: updater script 2022-06-01 10:06:08 +08:00
GyDi
2ead15e78e v1.0.2 2022-06-01 01:26:05 +08:00
GyDi
aa0740ff94 chore: update log 2022-06-01 01:25:39 +08:00
GyDi
6d3f837820 fix: macOS disable transparent 2022-06-01 01:04:46 +08:00
GyDi
359b82c29c chore: rm some files 2022-06-01 00:49:52 +08:00
GyDi
b54171bc2c chore: rust cache 2022-06-01 00:47:04 +08:00
GyDi
936b2131e0 chore: adjust ci and script 2022-06-01 00:43:57 +08:00
GyDi
aaef6a9e9c chore: enable meta by default 2022-06-01 00:31:12 +08:00
GyDi
9327006e61 chore: rm file 2022-05-30 00:58:48 +08:00
GyDi
5225c841ae chore: adjust code 2022-05-30 00:57:31 +08:00
GyDi
8464e319fd chore: update deps 2022-05-29 23:35:22 +08:00
GyDi
98b8bd90ea fix: window transparent and can not get hwnd 2022-05-25 19:06:06 +08:00
GyDi
ae94993b09 fix: create main window 2022-05-25 16:45:18 +08:00
GyDi
72f10aaed1 chore: update deps 2022-05-25 16:12:48 +08:00
GyDi
4a74bae8c7 chore: update clash core 2022-05-25 16:08:58 +08:00
ctaoist
5164aec37b feat: light mode wip (#96)
* 关闭窗口释放UI资源

* windows 还有左键点击事件

* 兼容enhance profile

* bug 修复
2022-05-25 16:06:39 +08:00
GyDi
1581e9b1cd chore: update clash version 2022-05-18 14:14:37 +08:00
GyDi
536e3ffb11 chore: alpha ci 2022-05-18 14:07:41 +08:00
GyDi
d6103191ba chore: error text 2022-05-18 09:58:20 +08:00
GyDi
49e37a19a5 chore: ci 2022-05-17 12:35:42 +08:00
GyDi
d02431e260 chore: fix alpha ci 2022-05-17 12:34:30 +08:00
GyDi
f24f6ead92 chore: alpha ci 2022-05-17 11:41:45 +08:00
GyDi
b7bdb7ae50 chore: meta 2022-05-17 02:46:15 +08:00
GyDi
2dcf8ac96f chore: fix ci 2022-05-17 02:21:58 +08:00
GyDi
5421c94853 chore: test ci 2022-05-17 02:17:02 +08:00
GyDi
10f6bc092a chore: alpha 2022-05-17 02:11:29 +08:00
GyDi
be9ea4ea8e feat: clash meta core supports 2022-05-17 01:59:49 +08:00
GyDi
f5c6fa842a fix: adjust notice 2022-05-17 00:51:37 +08:00
GyDi
e0943ce905 fix: label text 2022-05-16 20:26:13 +08:00
GyDi
61e7df77a7 feat: script mode 2022-05-16 20:18:56 +08:00
GyDi
a5434360bc chore: test ci 2022-05-16 17:55:14 +08:00
GyDi
ba29c66e3b chore: update deps 2022-05-16 17:54:21 +08:00
GyDi
b3a72d55ae feat: clash meta core support (wip) 2022-05-16 01:52:50 +08:00
GyDi
c382ad1cc8 fix: icon path 2022-05-13 12:28:36 +08:00
GyDi
363e28f323 fix: icon issue 2022-05-13 02:46:06 +08:00
GyDi
d695656b8c fix: notice ui blocking 2022-05-13 02:29:43 +08:00
GyDi
31c6cbc0a2 fix: service mode error 2022-05-13 02:11:03 +08:00
GyDi
b93284bc2f chore: fix linux build ci 2022-05-11 12:39:41 +08:00
GyDi
99c855b01b chore: test ci 2022-05-11 12:20:20 +08:00
GyDi
c2eaedc959 chore: test ci 2022-05-11 12:12:55 +08:00
GyDi
a631cd67ec chore: test ci fix deps 2022-05-11 11:30:19 +08:00
GyDi
66fccd3c68 chore: test ci 2022-05-11 11:22:09 +08:00
GyDi
9a4ebf4daa chore: test ci 2022-05-11 10:51:28 +08:00
GyDi
91c14211c6 chore: continue on error 2022-05-11 01:26:11 +08:00
GyDi
1ea07c458b chore: test linux 2022-05-11 00:52:20 +08:00
GyDi
591add6e0c chore: test ci 2022-05-11 00:31:19 +08:00
GyDi
b99fff66df v1.0.1 2022-05-10 21:40:33 +08:00
GyDi
f54ba05b00 chore: update log 2022-05-10 21:40:07 +08:00
GyDi
6596fb00c7 fix: win11 drag lag 2022-05-09 14:41:26 +08:00
GyDi
df0b5a80dc chore: change default height 2022-05-09 14:01:46 +08:00
GyDi
8cdbb31dbe fix: rm unwrap 2022-05-09 14:01:14 +08:00
GyDi
0be4b1222d feat: reduce gpu usage when hidden 2022-05-07 14:43:52 +08:00
GyDi
18a6bfd73a feat: interval update from now field 2022-05-07 14:43:08 +08:00
GyDi
5e2271b237 feat: adjust theme 2022-05-06 14:04:39 +08:00
GyDi
798999d490 fix: edit profile info 2022-05-06 12:46:27 +08:00
GyDi
0e68c5e8bc feat: supports more remote headers close #81 2022-05-06 10:52:59 +08:00
GyDi
9694af82f4 feat: check the remote profile 2022-05-06 01:26:24 +08:00
GyDi
28a4386975 fix: change window default size 2022-05-06 01:15:15 +08:00
GyDi
a238f7beba chore: update deps 2022-05-06 01:14:13 +08:00
GyDi
75ba16281b chore: change default user agent 2022-05-06 00:42:20 +08:00
GyDi
ad65a278d4 chore: merge
feat: remove outdated config
2022-04-28 16:28:18 +08:00
tianyoulan
a2320b3f8d feat: fix typo 2022-04-28 15:35:17 +08:00
tianyoulan
ae12853ad0 feat: remove trailing comma 2022-04-28 15:05:10 +08:00
tianyoulan
68adf6dc2f feat: remove outdated config 2022-04-28 15:02:37 +08:00
GyDi
f88989bd4b fix: change service installer and uninstaller 2022-04-27 15:46:44 +08:00
GyDi
77ef3847ce v1.0.0 2022-04-26 00:50:48 +08:00
GyDi
b8291837fc chore: update log 2022-04-26 00:49:07 +08:00
GyDi
423a7f951a fix: adjust connection scroll 2022-04-26 00:37:28 +08:00
GyDi
557f5fe364 fix: adjust something 2022-04-25 20:00:11 +08:00
GyDi
5308970ad8 fix: adjust debounce wait time 2022-04-25 19:39:21 +08:00
GyDi
6b368953f4 feat: windows service mode ui 2022-04-25 16:12:04 +08:00
GyDi
2d0b63c29d feat: add some commands 2022-04-24 21:03:47 +08:00
GyDi
34e941c8cb feat: windows service mode 2022-04-24 21:00:17 +08:00
GyDi
76cf007fff chore: update check script 2022-04-24 15:35:30 +08:00
GyDi
321963be83 wip: windows service mode 2022-04-23 17:26:32 +08:00
GyDi
c733bda6c3 test: windows service 2022-04-21 21:28:44 +08:00
GyDi
e67b50b976 chore: adjust guard log 2022-04-21 19:51:20 +08:00
GyDi
9e3c080909 fix: adjust dns config 2022-04-21 19:50:22 +08:00
GyDi
cb661aaebd feat: add update interval 2022-04-21 14:26:41 +08:00
GyDi
573571978c chore: adjust 2022-04-21 14:24:35 +08:00
GyDi
dc492a2a0a chore: update clash version 2022-04-20 21:02:41 +08:00
GyDi
e47747dd0e feat: refactor and supports cron tasks 2022-04-20 20:39:27 +08:00
GyDi
4de944b41e feat: supports cron update profiles 2022-04-20 20:37:16 +08:00
GyDi
5f7a1fa5cd refactor: verge 2022-04-20 11:17:54 +08:00
GyDi
b8ad328cde refactor: wip 2022-04-20 01:44:47 +08:00
GyDi
3076fd19c1 refactor: mutex 2022-04-19 14:38:59 +08:00
GyDi
fac437b8c1 fix: traffic graph adapt to different fps 2022-04-19 13:55:26 +08:00
GyDi
697c25015e refactor: wip 2022-04-19 01:41:20 +08:00
GyDi
d83b404fc3 chore: check script proxy agent supports 2022-04-17 00:37:21 +08:00
GyDi
ab7313cbc4 feat: optimize traffic graph quadratic curve 2022-04-16 22:32:44 +08:00
GyDi
1b8d70322b feat: optimize the animation of the traffic graph 2022-04-16 17:28:30 +08:00
GyDi
844ffab4ed fix: optimize clash launch 2022-04-15 01:29:25 +08:00
GyDi
f5d0513d1f fix: reset after exit 2022-04-14 01:29:33 +08:00
GyDi
557abd4285 fix: adjust code 2022-04-14 01:29:02 +08:00
GyDi
f4f1a0fbc6 v0.0.29 2022-04-13 01:30:08 +08:00
GyDi
74e10dc012 chore: update log 2022-04-13 01:29:48 +08:00
GyDi
359812b7ed chore: update dep 2022-04-13 01:21:49 +08:00
GyDi
c2449e53c4 chore: rename green to portable 2022-04-13 01:17:40 +08:00
GyDi
1a91249da2 feat: system tray add tun mode 2022-04-13 01:09:51 +08:00
GyDi
b9162f9576 fix: adjust log 2022-04-13 00:49:30 +08:00
GyDi
f726e8a7b3 feat: supports change config dir 2022-04-12 23:09:32 +08:00
GyDi
2f284cfdc9 chore: update clash version 2022-04-12 23:04:19 +08:00
GyDi
b74696adba feat: add default user agent 2022-04-12 01:05:51 +08:00
GyDi
c8ccba0192 chore: rm console 2022-04-11 02:26:09 +08:00
GyDi
41b0e05f62 feat: connections page supports filter 2022-04-11 02:25:34 +08:00
GyDi
847d5f1b3b feat: log page supports filter 2022-04-11 01:46:33 +08:00
GyDi
0445f9dfc2 fix: check button hover style 2022-04-10 03:33:17 +08:00
GyDi
3001c780bd feat: optimize delay checker concurrency strategy 2022-04-10 02:58:48 +08:00
GyDi
68ad5e2320 feat: support sort proxy node and custom test url 2022-04-10 02:09:36 +08:00
GyDi
b5e229b19c chore: update deps 2022-04-10 01:31:05 +08:00
GyDi
453d798fcf refactor: proxy head 2022-04-08 01:35:18 +08:00
GyDi
451afdb660 v0.0.28 2022-04-07 01:28:30 +08:00
GyDi
d298bda92c chore: update log 2022-04-07 01:27:48 +08:00
GyDi
fd99ba6255 feat: handle remote clash config fields 2022-04-07 01:20:44 +08:00
GyDi
6ade0b2b1a fix: icon button color inherit 2022-04-06 22:52:00 +08:00
GyDi
9902003da9 fix: remove the lonely zero 2022-04-06 22:48:10 +08:00
GyDi
0ff2fcac11 chore: readme 2022-04-06 01:52:20 +08:00
228 changed files with 29612 additions and 9984 deletions

View File

@@ -5,3 +5,9 @@ charset = utf-8
end_of_line = lf end_of_line = lf
indent_size = 2 indent_size = 2
insert_final_newline = true insert_final_newline = true
[*.rs]
charset = utf-8
end_of_line = lf
indent_size = 4
insert_final_newline = true

56
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,56 @@
name: 问题反馈 / Bug report
title: "[BUG] "
description: 反馈你遇到的问题 / Report the issue you are experiencing
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
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:
required: true
- type: textarea
attributes:
label: 复现步骤 / To Reproduce
description: 请提供复现问题的步骤 / Steps to reproduce the behavior
validations:
required: true
- type: dropdown
attributes:
label: 操作系统 / OS
options:
- Windows
- Linux
- MacOS
validations:
required: true
- type: input
attributes:
label: 操作系统版本 / OS Version
description: 请提供你的操作系统版本Linux请额外提供桌面环境及窗口系统 / Please provide your OS version, for Linux, please also provide the desktop environment and window system
validations:
required: true
- type: textarea
attributes:
label: 日志 / Log
description: 请提供完整或相关部分的Debug日志 / Please provide the complete or relevant part of the Debug log
validations:
required: true

4
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
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

View File

@@ -0,0 +1,35 @@
name: 功能请求 / Feature request
title: "[Feature] "
description: 提出你的功能请求 / Propose your feature request
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
## 在提交问题之前,请确认以下事项:
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:
required: true
- type: textarea
attributes:
label: 使用场景 / Use case
description: 请描述你的功能请求的使用场景 / Please describe the use case of your feature request
validations:
required: true

4
.github/build-for-linux/Dockerfile vendored Normal file
View File

@@ -0,0 +1,4 @@
FROM rust:buster
COPY entrypoint.sh /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

14
.github/build-for-linux/action.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
name: "Build for Linux"
branding:
icon: user-check
color: gray-dark
inputs:
target:
required: true
description: "Rust Target"
runs:
using: "docker"
image: "Dockerfile"
args:
- ${{ inputs.target }}

8
.github/build-for-linux/build.sh vendored Normal file
View File

@@ -0,0 +1,8 @@
pnpm install
pnpm check $INPUT_TARGET
sed -i "s/#openssl/openssl={version=\"0.10\",features=[\"vendored\"]}/g" src-tauri/Cargo.toml
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
pnpm build --target $INPUT_TARGET
else
pnpm build --target $INPUT_TARGET -b deb
fi

47
.github/build-for-linux/entrypoint.sh vendored Normal file
View File

@@ -0,0 +1,47 @@
#!/bin/bash
wget https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz
tar -Jxvf ./node-v20.10.0-linux-x64.tar.xz
export PATH=$(pwd)/node-v20.10.0-linux-x64/bin:$PATH
npm install pnpm -g
rustup target add "$INPUT_TARGET"
if [ "$INPUT_TARGET" = "x86_64-unknown-linux-gnu" ]; then
apt-get update
apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
dpkg --add-architecture arm64
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 --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 CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc
export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++
export PKG_CONFIG_PATH=/usr/lib/aarch64-linux-gnu/pkgconfig
export PKG_CONFIG_ALLOW_CROSS=1
elif [ "$INPUT_TARGET" = "armv7-unknown-linux-gnueabihf" ]; then
dpkg --add-architecture armhf
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 --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 CC_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-gcc
export CXX_armv7_unknown_linux_gnueabihf=arm-linux-gnueabihf-g++
export PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig
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
echo "Unknown target: $INPUT_TARGET" && exit 1
fi
bash .github/build-for-linux/build.sh

205
.github/workflows/alpha.yml vendored Normal file
View File

@@ -0,0 +1,205 @@
name: Alpha Build
on:
workflow_dispatch:
push:
branches: [main]
tags-ignore: [updater, alpha]
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
alpha:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: alpha
releaseName: "Clash Verge Rev Alpha"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: pnpm portable ${{ matrix.target }} --alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
alpha-for-linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build for Linux
uses: ./.github/build-for-linux
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
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
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
update_tag:
name: Update tag
runs-on: ubuntu-latest
needs: [alpha, alpha-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set Env
run: |
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash
- name: Update Tag
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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
uses: softprops/action-gh-release@v1
with:
tag_name: alpha
name: "Clash Verge Rev Alpha"
body_path: release.txt
prerelease: true
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: true

View File

@@ -1,112 +0,0 @@
name: Release CI
on: [push]
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
release:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
if: |
startsWith(github.repository, 'zzzgydi') &&
startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@ce325b60658c1b38465c06cc965b79baf32c1e72
- name: Install Node
uses: actions/setup-node@v1
with:
node-version: 14
- name: Install Dependencies (ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install and check
run: |
yarn install --network-timeout 1000000
yarn run check
- name: Tauri build
uses: tauri-apps/tauri-action@0e558392ccadcb49bcc89e7df15a400e8f0c954d
# enable cache even though failed
# continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: true
- name: Green zip bundle
if: matrix.os == 'windows-latest'
run: |
yarn run green
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-update:
needs: release
runs-on: macos-latest
if: |
startsWith(github.repository, 'zzzgydi') &&
startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Get yarn cache dir path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install
run: yarn install
- name: Release updater file
run: yarn run updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

153
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,153 @@
name: Release Build
on:
workflow_dispatch:
permissions: write-all
env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: short
jobs:
release:
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: windows-latest
target: aarch64-pc-windows-msvc
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Rust Stable
uses: dtolnay/rust-toolchain@stable
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
- uses: pnpm/action-setup@v3
name: Install pnpm
with:
version: 9
run_install: false
- name: Pnpm install and check
run: |
pnpm i
pnpm check ${{ matrix.target }}
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tagName: v__VERSION__
releaseName: "Clash Verge Rev v__VERSION__"
releaseBody: "More new features are now supported."
releaseDraft: false
prerelease: false
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Portable Bundle
if: matrix.os == 'windows-latest'
run: pnpm portable ${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
release-for-linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: aarch64-unknown-linux-gnu
- os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Build for Linux
uses: ./.github/build-for-linux
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
target: ${{ matrix.target }}
- name: Get Version
run: |
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
- name: Upload Release
if: startsWith(matrix.target, 'x86_64')
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/appimage/*.AppImage*
- name: Upload Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge Rev v${{env.VERSION}}"
body: "More new features are now supported."
token: ${{ secrets.GITHUB_TOKEN }}
files: src-tauri/target/${{ matrix.target }}/release/bundle/deb/*.deb
release-update:
runs-on: ubuntu-latest
needs: [release, release-for-linux]
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: "20"
- uses: pnpm/action-setup@v2
name: Install pnpm
with:
version: 8
run_install: false
- name: Pnpm install
run: pnpm i
- name: Release updater file
run: pnpm updater
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,32 +1,29 @@
name: Updater CI name: Updater CI
on: workflow_dispatch on: workflow_dispatch
permissions: write-all
jobs: jobs:
release-update: release-update:
runs-on: macos-latest runs-on: ubuntu-latest
if: startsWith(github.repository, 'zzzgydi')
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Get yarn cache dir path - name: Install Node
id: yarn-cache-dir-path uses: actions/setup-node@v3
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Yarn Cache
uses: actions/cache@v2
id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} node-version: "20"
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install - uses: pnpm/action-setup@v2
run: yarn install name: Install pnpm
with:
version: 8
run_install: false
- name: Pnpm install
run: pnpm i
- name: Release updater file - name: Release updater file
run: yarn run updater run: pnpm updater
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@@ -1,6 +1,10 @@
node_modules node_modules
.pnpm-store
.DS_Store .DS_Store
dist dist
dist-ssr dist-ssr
*.local *.local
update.json update.json
scripts/_env.sh
.vscode
.tool-versions

2
.husky/pre-commit Normal file → Executable file
View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
yarn pretty-quick --staged pnpm pretty-quick --staged

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
nodejs 21.7.1

67
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,67 @@
# CONTRIBUTING
Thank you for your interest in contributing to Clash Verge Rev! This document provides guidelines and instructions to help you set up your development environment and start contributing.
## Development Setup
Before you start contributing to the project, you need to set up your development environment. Here are the steps you need to follow:
### Prerequisites
1. **Install Rust and Node.js**: Our project requires both Rust and Node.js. Please follow the instructions provided [here](https://tauri.app/v1/guides/getting-started/prerequisites) to install them on your system.
### Setup for Windows Users
If you're a Windows user, you may need to perform some additional steps:
- Make sure to add Rust and Node.js to your system's PATH. This is usually done during the installation process, but you can verify and manually add them if necessary.
- The gnu `patch` tool should be installed
### Install Node.js Packages
After installing Rust and Node.js, install the necessary Node.js packages:
```shell
pnpm i
```
### Download the Clash Binary
You have two options for downloading the clash binary:
- Automatically download it via the provided script:
```shell
pnpm run check
# Use '--force' to force update to the latest version
# pnpm run check --force
```
- Manually download it from the [Clash Meta release](https://github.com/MetaCubeX/Clash.Meta/releases). After downloading, rename the binary according to the [Tauri configuration](https://tauri.app/v1/api/config#bundleconfig.externalbin).
### Run the Development Server
To run the development server, use the following command:
```shell
pnpm dev
# If an app instance already exists, use a different command
pnpm dev:diff
```
### Build the Project
If you want to build the project, use:
```shell
pnpm build
```
## Contributing Your Changes
Once you have made your changes:
1. Fork the repository.
2. Create a new branch for your feature or bug fix.
3. Commit your changes with clear and concise commit messages.
4. Push your branch to your fork and submit a pull request to our repository.
We appreciate your contributions and look forward to your active participation in our project!

View File

@@ -1,83 +1,80 @@
<h1 align="center"> <h1 align="center">
<img src="./src/assets/image/logo.png" alt="Clash" width="128" /> <img src="./src/assets/image/logo.png" alt="Clash" width="128" />
<br> <br>
Clash Verge Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br> <br>
</h1> </h1>
<h3 align="center"> <h3 align="center">
A <a href="https://github.com/Dreamacro/clash">Clash</a> 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>
## Features ## Preview
- Full `clash` config supported, Partial `clash premium` config supported. ![preview](./docs/preview.png)
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/issues/12)
- System proxy setting and guard.
## Install ## Install
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+ 请到发布页面下载对应的安装包:[Release page](https://github.com/clash-verge-rev/clash-verge-rev/releases)<br>
Go to the [release page](https://github.com/clash-verge-rev/clash-verge-rev/releases) to download the corresponding installation package<br>
Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+ ### 安装说明和常见问题,请到[文档页](https://clash-verge-rev.github.io/)查看:[Doc](https://clash-verge-rev.github.io/)
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed. ---
### TG Group: [@clash_verge_rev](https://t.me/clash_verge_rev)
## Promotion
[狗狗加速 —— 技术流机场 Doggygo VPN](https://狗狗加速.com)
- 高性能海外机场,免费试用,优惠套餐,解锁流媒体,全球首家支持 Hysteria 协议。
- 使用 Clash Verge 专属邀请链接注册送 3 天,每天 1G 流量免费试用https://verge.狗狗加速.com/#/register?code=oaxsAGo6
- Clash Verge 专属 8 折优惠码: verge20 (仅有 500 份)
- 优惠套餐每月仅需 15.8 元160G 流量,年付 8 折
- 海外团队,无跑路风险,高达 50% 返佣
- 集群负载均衡设计,高速专线(兼容老客户端)极低延迟无视晚高峰4K 秒开
- 全球首家 Hysteria 协议机场,现已上线更快的 `Hysteria2` 协议(Clash Verge 客户端最佳搭配)
- 解锁流媒体及 ChatGPT
- 官网https://狗狗加速.com
## Features
- Since the clash core has been removed. The project no longer maintains the clash core, but only the Clash Meta core.
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://clash-verge-rev.github.io)
- Improved UI and supports custom theme color.
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
- System proxy setting and guard.
### FAQ
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/install/)
## Development ## Development
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
To run the development server, execute the following commands after all prerequisites for **Tauri** are installed:
```shell ```shell
yarn install pnpm i
pnpm run check
pnpm dev
``` ```
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
```shell
yarn run check
```
Then run
```shell
yarn dev
```
Or you can build it
```shell
yarn build
```
## Todos
> This keng is a little big...
## Screenshots
<div align="center">
<img src="./docs/demo1.png" alt="demo1" width="32%" />
<img src="./docs/demo2.png" alt="demo2" width="32%" />
<img src="./docs/demo3.png" alt="demo3" width="32%" />
<img src="./docs/demo4.png" alt="demo4" width="32%" />
<img src="./docs/demo5.png" alt="demo5" width="32%" />
<img src="./docs/demo6.png" alt="demo6" width="32%" />
</div>
## Disclaimer
This is a learning project for Rust practice.
## Contributions ## Contributions
Issue and PR welcome! Issue and PR welcome!
## Acknowledgement ## Acknowledgement
Clash Verge was based on or inspired by these projects and so on: Clash Verge rev was based on or inspired by these projects and so on:
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Clash GUI based on tauri. Supports Windows, macOS and Linux.
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend. - [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go. - [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
- [MetaCubeX/mihomo](https://github.com/MetaCubeX/mihomo): A rule-based tunnel in Go.
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash. - [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast! - [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!

View File

@@ -1,3 +1,815 @@
## 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
### Features
- 优化 UI 各种细节
- 提供菜单栏图标样式切换选项(单色/彩色/禁用)
- 添加自动检查更新开关
- MacOS 开启 Tun 模式自动修改 DNS
- 调整可拖动区域(尝试修复触摸屏无法拖动的问题)
---
## v1.5.6
### Features
- 全新专属 Verge rev UI 界面 (by @Amnesiash) 及细节调整
- 提供允许无效证书的开关
- 删除不必要的快捷键
- Provider 更新添加动画
- Merge 支持 Provider
- 更换订阅框的粘贴按钮,删除默认的"Remote File" Profile 名称
- 链接菜单添加节点显示
### Bugs Fixes
- Linux 下图片显示错误
---
## v1.5.4
### Features
- 支持自定义托盘图标
- 支持禁用代理组图标
- 代理组显示当前代理
- 修改 `打开面板` 快捷键为`打开/关闭面板`
---
## v1.5.3
### Features
- Tun 设置添加重置按钮
### Bugs Fixes
- Tun 设置项显示错误的问题
- 修改一些默认值
- 启动时不更改启动项设置
---
## v1.5.2
### Features
- 支持自定义延迟测试超时时间
- 优化 Tun 相关设置
### Bugs Fixes
- Merge 操作出错
- 安装后重启服务
- 修复管理员权限启动时开机启动失效的问题
---
## v1.5.1
### Features
- 保存窗口最大化状态
- Proxy Provider 显示数量
- 不再提供 32 位安装包(因为 32 位经常出现各种奇怪问题,比如 tun 模式无法开启;现在系统也几乎没有 32 位了)
### Bugs Fixes
- 优化设置项名称
- 自定义 GLOBAL 代理组时代理组显示错误的问题
---
## v1.5.0
### Features
- 删除 Clash 字段过滤功能
- 添加 socks 端口和 http 端口设置
- 升级内核到 1.18.1
### Bugs Fixes
- 修复 32 位版本无法显示流量信息的问题
---
## v1.4.11
### Break Changes
- 此版本更改了 Windows 安装包安装模式,需要卸载后手动安装,否则无法安装到正确位置
### Features
- 优化了系统代理开启的代码,解决了稀有场景下代理开启卡顿的问题
- 添加 MacOS 下的 debug 日志,以便日后调试稀有场景下 MacOS 下无法开启系统代理的问题
- MacOS 关闭 GUI 时同步杀除后台 GUI [#306](https://github.com/clash-verge-rev/clash-verge-rev/issues/306)
### Bugs Fixes
- 解决自动更新时文件占用问题
- 解决稀有场景下系统代理开启失败的问题
- 删除冗余内核代码
---
## v1.4.10
### Features
- 设置中添加退出按钮
- 支持自定义软件启动页
- 在 Proxy Provider 页面展示订阅信息
- 优化 Provider 支持
### Bugs Fixes
- 更改端口时立即重设系统代理
- 网站测试超时错误
---
## v1.4.9
### Features
- 支持启动时运行脚本
- 支持代理组显示图标
- 新增测试页面
### Bugs Fixes
- 连接页面时间排序错误
- 连接页面表格宽度优化
---
## v1.4.8
### Features
- 连接页面总流量显示
### Bugs Fixes
- 连接页面数据排序错误
- 新建订阅时设置更新间隔无效
- Windows 拨号网络无法设置系统代理
- Windows 开启/关闭系统代理延迟(使用注册表即可)
- 删除无效的背景模糊选项
---
## v1.4.7
### Features
- Windows 便携版禁用应用内更新
- 支持代理组 Hidden 选项
- 支持 URL Scheme(MacOS & Linux)
---
## v1.4.6
### Features
- 更新 Clash Meta(mihomo) 内核到 v1.18.0
- 支持 URL Scheme(暂时仅支持 Windows)
- 添加窗口置顶按钮
- UI 优化调整
### Bugs Fixes
- 修复一些编译错误
- 获取订阅名称错误
- 订阅信息解析错误
---
## v1.4.5
### Features
- 更新 MacOS 托盘图标样式(@gxx2778 贡献)
### Bugs Fixes
- Windows 下更新时无法覆盖`clash-verge-service.exe`的问题(需要卸载重装一次服务,下次更新生效)
- 窗口最大化按钮变化问题
- 窗口尺寸保存错误问题
- 复制环境变量类型无法切换问题
- 某些情况下闪退的问题
- 某些订阅无法导入的问题
---
## v1.4.4
### Features
- 支持 Windows aarch64(arm64) 版本
- 支持一键更新 GeoData
- 支持一键更新 Alpha 内核
- MacOS 支持在系统代理时显示不同的托盘图标
- Linux 支持在系统代理时显示不同的托盘图标
- 优化复制环境变量逻辑
### Bugs Fixes
- 修改 PID 文件的路径
### Performance
- 优化创建窗口的速度
---
## v1.4.3
### Break Changes
- 更改配置文件路径到标准目录(可以保证卸载时没有残留)
- 更改 appid 为 `io.github.clash-verge-rev.clash-verge-rev`
- 建议卸载旧版本后再安装新版本,该版本安装后不会使用旧版配置文件,你可以手动将旧版配置文件迁移到新版配置文件目录下
### Features
- 移除页面切换动画
- 更改 Tun 模式托盘图标颜色
- Portable 版本默认使用当前目录作为配置文件目录
- 禁用 Clash 字段过滤时隐藏 Clash 字段选项
- 优化拖拽时光标样式
### Bugs Fixes
- 修复 windows 下更新时没有关闭内核导致的更新失败的问题
- 修复打开文件报错的问题
- 修复 url 导入时无法获取中文配置名称的问题
- 修复 alpha 内核无法显示内存信息的问题
---
## v1.4.2
### Features
- update clash meta core to mihomo 1.17.0
- support both clash meta stable release and prerelease-alpha release
- fixed the problem of not being able to set the system proxy when there is a dial-up link on windows system [#833](https://github.com/zzzgydi/clash-verge/issues/833)
- support new clash field
- support random mixed port
- add windows x86 and linux armv7 support
- support disable tray click event
- add download progress for updater
- support drag to reorder the profile
- embed emoji fonts
- update depends
- improve UI style
---
## v1.4.1
### Features
- update clash meta core to newest 虚空终端(2023.11.23)
- delete clash core UI
- improve UI
- change Logo to original
---
## v1.4.0
### Features
- update clash meta core to newest 虚空终端
- delete clash core, no longer maintain
- merge Clash nyanpasu changes
- remove delay display different color
- use Meta Country.mmdb
- update dependencies
- small changes here and there
---
## v1.3.8
### Features
- update clash meta core
- add default valid keys
- adjust the delay display interval and color
### Bug Fixes
- fix connections page undefined exception
---
## v1.3.7
### Features
- update clash and clash meta core
- profiles page add paste button
- subscriptions url textfield use multi lines
- set min window size
- add check for updates buttons
- add open dashboard to the hotkey list
### Bug Fixes
- fix profiles page undefined exception
---
## v1.3.6
### Features
- add russian translation
- support to show connection detail
- support clash meta memory usage display
- support proxy provider update ui
- update geo data file from meta repo
- adjust setting page
### Bug Fixes
- center the window when it is out of screen
- use `sudo` when `pkexec` not found (Linux)
- reconnect websocket when window focus
### Notes
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
---
## v1.3.5
### Features
- update clash core
### Bug Fixes
- fix blurry system tray icon (Windows)
- fix v1.3.4 wintun.dll not found (Windows)
- fix v1.3.4 clash core not found (macOS, Linux)
---
## v1.3.4
### Features
- update clash and clash meta core
- optimize traffic graph high CPU usage when window hidden
- use polkit to elevate permission (Linux)
- support app log level setting
- support copy environment variable
- overwrite resource file according to file modified
- save window size and position
### Bug Fixes
- remove fallback group select status
- enable context menu on editable element (Windows)
---
## v1.3.3
### Features
- update clash and clash meta core
- show tray icon variants in different system proxy status (Windows)
- close all connections when mode changed
### Bug Fixes
- encode controller secret into uri
- error boundary for each page
---
## v1.3.2
### Features
- update clash and clash meta core
### Bug Fixes
- fix import url issue
- fix profile undefined issue
---
## v1.3.1
### Features
- update clash and clash meta core
### Bug Fixes
- fix open url issue
- fix appimage path panic
- fix grant root permission in macOS
- fix linux system proxy default bypass
---
## v1.3.0
### Features
- update clash and clash meta
- support opening dir on tray
- support updating all profiles with one click
- support granting root permission to clash core(Linux, macOS)
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
### Bug Fixes
- deb add openssl depend(Linux)
- fix the AppImage auto launch path(Linux)
- fix get the default network service(macOS)
- remove the esc key listener in macOS, cmd+w instead(macOS)
- fix infinite retry when websocket error
---
## v1.2.3
### Features
- update clash
- adjust macOS window style
- profile supports UTF8 with BOM
### Bug Fixes
- fix selected proxy
- fix error log
---
## v1.2.2
### Features
- update clash meta
- recover clash core after panic
- use system window decorations(Linux)
### Bug Fixes
- flush system proxy settings(Windows)
- fix parse log panic
- fix ui bug
---
## v1.2.1
### Features
- update clash version
- proxy groups support multi columns
- optimize ui
### Bug Fixes
- fix ui websocket connection
- adjust delay check concurrency
- avoid setting login item repeatedly(macOS)
---
## v1.2.0
### Features
- update clash meta version
- support to change external-controller
- support to change default latency test URL
- close all connections when proxy changed or profile changed
- check the config by using the core
- increase the robustness of the program
- optimize windows service mode (need to reinstall)
- optimize ui
### Bug Fixes
- invalid hotkey cause panic
- invalid theme setting cause panic
- fix some other glitches
---
## v1.1.2
### Features
- the system tray follows i18n
- change the proxy group ui of global mode
- support to update profile with the system proxy/clash proxy
- check the remote profile more strictly
### Bug Fixes
- use app version as default user agent
- the clash not exit in service mode
- reset the system proxy when quit the app
- fix some other glitches
---
## v1.1.1
### Features
- optimize clash config feedback
- hide macOS dock icon
- use clash meta compatible version (Linux)
### Bug Fixes
- fix some other glitches
---
## v1.1.0
### Features
- add rule page
- supports proxy providers delay check
- add proxy delay check loading status
- supports hotkey/shortcut management
- supports displaying connections data in table layout(refer to yacd)
### Bug Fixes
- supports yaml merge key in clash config
- detect the network interface and set the system proxy(macOS)
- fix some other glitches
---
## v1.0.6
### Features
- update clash and clash.meta
### Bug Fixes
- only script profile display console
- automatic configuration update on demand at launch
---
## v1.0.5
### Features
- reimplement profile enhanced mode with quick-js
- optimize the runtime config generation process
- support web ui management
- support clash field management
- support viewing the runtime config
- adjust some pages style
### Bug Fixes
- fix silent start
- fix incorrectly reset system proxy on exit
---
## v1.0.4
### Features
- update clash core and clash meta version
- support switch clash mode on system tray
- theme mode support follows system
### Bug Fixes
- config load error on first use
---
## v1.0.3
### Features
- save some states such as URL test, filter, etc
- update clash core and clash-meta core
- new icon for macOS
---
## v1.0.2
### Features
- supports for switching clash core
- supports release UI processes
- supports script mode setting
### Bug Fixes
- fix service mode bug (Windows)
---
## v1.0.1
### Features
- adjust default theme settings
- reduce gpu usage of traffic graph when hidden
- supports more remote profile response header setting
- check remote profile data format when imported
### Bug Fixes
- service mode install and start issue (Windows)
- fix launch panic (Some Windows)
---
## v1.0.0
### Features
- update clash core
- optimize traffic graph animation
- supports interval update profiles
- supports service mode (Windows)
### Bug Fixes
- reset system proxy when exit from dock (macOS)
- adjust clash dns config process strategy
---
## v0.0.29
### Features
- sort proxy node
- custom proxy test url
- logs page filter
- connections page filter
- default user agent for subscription
- system tray add tun mode toggle
- enable to change the config dir (Windows only)
---
## v0.0.28
### Features
- enable to use clash config fields (UI)
### Bug Fixes
- remove the character
- fix some icon color
---
## v0.0.27 ## v0.0.27
### Features ### Features

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

View File

@@ -1,59 +1,78 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "0.0.27", "version": "1.6.1",
"license": "GPL-3.0", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "tauri dev", "dev": "tauri dev",
"dev:diff": "tauri dev -f verge-dev",
"build": "tauri build", "build": "tauri build",
"tauri": "tauri", "tauri": "tauri",
"web:dev": "vite", "web:dev": "vite",
"web:build": "tsc && vite build", "web:build": "tsc && vite build",
"web:serve": "vite preview", "web:serve": "vite preview",
"check": "node scripts/check.mjs", "check": "node scripts/check.mjs",
"green": "node scripts/green.mjs",
"publish": "node scripts/publish.mjs",
"updater": "node scripts/updater.mjs", "updater": "node scripts/updater.mjs",
"portable": "node scripts/portable.mjs",
"prepare": "husky install" "prepare": "husky install"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.8.2", "@dnd-kit/core": "^6.1.0",
"@emotion/styled": "^11.8.1", "@dnd-kit/sortable": "^8.0.0",
"@mui/icons-material": "^5.5.1", "@dnd-kit/utilities": "^3.2.2",
"@mui/material": "^5.5.3", "@emotion/react": "^11.11.4",
"@tauri-apps/api": "^1.0.0-rc.3", "@emotion/styled": "^11.11.5",
"ahooks": "^3.2.0", "@juggle/resize-observer": "^3.4.0",
"axios": "^0.26.0", "@mui/icons-material": "^5.15.15",
"dayjs": "^1.11.0", "@mui/lab": "5.0.0-alpha.149",
"i18next": "^21.6.14", "@mui/material": "^5.15.15",
"monaco-editor": "^0.33.0", "@mui/x-data-grid": "^6.19.11",
"react": "^17.0.2", "@tauri-apps/api": "^1.5.4",
"react-dom": "^17.0.2", "@types/json-schema": "^7.0.15",
"react-i18next": "^11.15.6", "ahooks": "^3.7.11",
"react-router-dom": "^6.2.2", "axios": "^1.6.8",
"react-virtuoso": "~2.7.2", "dayjs": "1.11.5",
"recoil": "^0.6.1", "i18next": "^23.11.2",
"snarkdown": "^2.0.0", "lodash-es": "^4.17.21",
"swr": "^1.2.2" "meta-json-schema": "1.18.4-beta2",
"monaco-editor": "^0.47.0",
"monaco-yaml": "^5.1.1",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.51.3",
"react-i18next": "^13.5.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.23.0",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.7.10",
"recoil": "^0.7.7",
"swr": "^1.3.0",
"tar": "^6.2.1"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^5.0.0", "@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.0.0-rc.8", "@tauri-apps/cli": "^1.5.12",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.1", "@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.14.180", "@types/lodash-es": "^4.17.12",
"@types/react": "^17.0.0", "@types/react": "^18.3.1",
"@types/react-dom": "^17.0.0", "@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^1.2.0", "@types/react-transition-group": "^4.4.10",
"adm-zip": "^0.5.9", "@vitejs/plugin-react": "^4.2.1",
"fs-extra": "^10.0.0", "adm-zip": "^0.5.12",
"husky": "^7.0.0", "cross-env": "^7.0.3",
"node-fetch": "^3.2.0", "fs-extra": "^11.2.0",
"pretty-quick": "^3.1.3", "https-proxy-agent": "^5.0.1",
"sass": "^1.49.7", "husky": "^7.0.4",
"typescript": "^4.5.5", "node-fetch": "^3.3.2",
"vite": "^2.8.6", "prettier": "^2.8.8",
"vite-plugin-monaco-editor": "^1.0.10", "pretty-quick": "^3.3.1",
"vite-plugin-svgr": "^1.1.0" "sass": "^1.75.0",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
}, },
"prettier": { "prettier": {
"tabWidth": 2, "tabWidth": 2,

5602
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,159 +1,317 @@
import fs from "fs-extra"; import fs from "fs-extra";
import zlib from "zlib"; import zlib from "zlib";
import tar from "tar";
import path from "path"; import path from "path";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import fetch from "node-fetch"; import fetch from "node-fetch";
import proxyAgent from "https-proxy-agent";
import { execSync } from "child_process"; import { execSync } from "child_process";
const cwd = process.cwd(); const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force"); const FORCE = process.argv.includes("--force");
/** const PLATFORM_MAP = {
* get the correct clash release infomation "x86_64-pc-windows-msvc": "win32",
*/ "i686-pc-windows-msvc": "win32",
function resolveClash() { "aarch64-pc-windows-msvc": "win32",
const { platform, arch } = process; "x86_64-apple-darwin": "darwin",
"aarch64-apple-darwin": "darwin",
"x86_64-unknown-linux-gnu": "linux",
"i686-unknown-linux-gnu": "linux",
"aarch64-unknown-linux-gnu": "linux",
"armv7-unknown-linux-gnueabihf": "linux",
"riscv64gc-unknown-linux-gnu": "linux",
"loongarch64-unknown-linux-gnu": "linux",
};
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"i686-pc-windows-msvc": "ia32",
"aarch64-pc-windows-msvc": "arm64",
"x86_64-apple-darwin": "x64",
"aarch64-apple-darwin": "arm64",
"x86_64-unknown-linux-gnu": "x64",
"i686-unknown-linux-gnu": "ia32",
"aarch64-unknown-linux-gnu": "arm64",
"armv7-unknown-linux-gnueabihf": "arm",
"riscv64gc-unknown-linux-gnu": "riscv64",
"loongarch64-unknown-linux-gnu": "loong64",
};
const CLASH_URL_PREFIX = const arg1 = process.argv.slice(2)[0];
"https://github.com/Dreamacro/clash/releases/download/premium/"; const arg2 = process.argv.slice(2)[1];
const CLASH_LATEST_DATE = "2022.03.21"; const target = arg1 === "--force" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
// todo const SIDECAR_HOST = target
const map = { ? target
"win32-x64": "clash-windows-amd64", : execSync("rustc -vV")
"darwin-x64": "clash-darwin-amd64", .toString()
"darwin-arm64": "clash-darwin-arm64", .match(/(?<=host: ).+(?=\s*)/g)[0];
"linux-x64": "clash-linux-amd64",
};
const name = map[`${platform}-${arch}`]; /* ======= clash meta alpha======= */
const META_ALPHA_VERSION_URL =
"https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt";
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`;
let META_ALPHA_VERSION;
if (!name) { const META_ALPHA_MAP = {
throw new Error(`unsupport platform "${platform}-${arch}"`); "win32-x64": "mihomo-windows-amd64-compatible",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest alpha release version from the version.txt file
async function getLatestAlphaVersion() {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
} }
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
...options,
method: "GET",
});
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest alpha version: ${META_ALPHA_VERSION}`);
} catch (error) {
console.error("Error fetching latest alpha version:", error.message);
process.exit(1);
}
}
const isWin = platform === "win32"; /* ======= clash meta stable ======= */
const zip = isWin ? "zip" : "gz"; const META_VERSION_URL =
const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`; "https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt";
const exefile = `${name}${isWin ? ".exe" : ""}`; const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
const zipfile = `${name}.${zip}`; let META_VERSION;
return { url, zip, exefile, zipfile }; const META_MAP = {
"win32-x64": "mihomo-windows-amd64-compatible",
"win32-ia32": "mihomo-windows-386",
"win32-arm64": "mihomo-windows-arm64",
"darwin-x64": "mihomo-darwin-amd64",
"darwin-arm64": "mihomo-darwin-arm64",
"linux-x64": "mihomo-linux-amd64-compatible",
"linux-ia32": "mihomo-linux-386",
"linux-arm64": "mihomo-linux-arm64",
"linux-arm": "mihomo-linux-armv7",
"linux-riscv64": "mihomo-linux-riscv64",
"linux-loong64": "mihomo-linux-loong64",
};
// Fetch the latest release version from the version.txt file
async function getLatestReleaseVersion() {
const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
}
try {
const response = await fetch(META_VERSION_URL, {
...options,
method: "GET",
});
let v = await response.text();
META_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
/*
* check available
*/
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
);
} }
/** /**
* get the sidecar bin * core info
*/ */
async function resolveSidecar() { function clashMetaAlpha() {
const sidecarDir = path.join(cwd, "src-tauri", "sidecar"); const name = META_ALPHA_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_ALPHA_VERSION}.${urlExt}`;
const host = execSync("rustc -vV | grep host").toString().slice(6).trim(); return {
const ext = process.platform === "win32" ? ".exe" : ""; name: "clash-meta-alpha",
const sidecarFile = `clash-${host}${ext}`; targetFile: `clash-meta-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
const sidecarPath = path.join(sidecarDir, sidecarFile); exeFile,
zipFile,
downloadURL,
};
}
function clashMeta() {
const name = META_MAP[`${platform}-${arch}`];
const isWin = platform === "win32";
const urlExt = isWin ? "zip" : "gz";
const downloadURL = `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
const exeFile = `${name}${isWin ? ".exe" : ""}`;
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
return {
name: "clash-meta",
targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
exeFile,
zipFile,
downloadURL,
};
}
/**
* download sidecar and rename
*/
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
const sidecarPath = path.join(sidecarDir, targetFile);
await fs.mkdirp(sidecarDir); await fs.mkdirp(sidecarDir);
if (!FORCE && (await fs.pathExists(sidecarPath))) return; if (!FORCE && (await fs.pathExists(sidecarPath))) return;
// download sidecar const tempDir = path.join(TEMP_DIR, name);
const binInfo = resolveClash(); const tempZip = path.join(tempDir, zipFile);
const tempDir = path.join(cwd, "pre-dev-temp"); const tempExe = path.join(tempDir, exeFile);
const tempZip = path.join(tempDir, binInfo.zipfile);
const tempExe = path.join(tempDir, binInfo.exefile);
await fs.mkdirp(tempDir); await fs.mkdirp(tempDir);
if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip); try {
if (!(await fs.pathExists(tempZip))) {
await downloadFile(downloadURL, tempZip);
}
if (binInfo.zip === "zip") { if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip); const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => { zip.getEntries().forEach((entry) => {
console.log("[INFO]: entry name", entry.entryName); console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
}); });
zip.extractAllTo(tempDir, true); zip.extractAllTo(tempDir, true);
// save as sidecar await fs.rename(tempExe, sidecarPath);
await fs.rename(tempExe, sidecarPath); console.log(`[INFO]: "${name}" unzip finished`);
console.log(`[INFO]: unzip finished`); } else if (zipFile.endsWith(".tgz")) {
} else { // tgz
// gz await fs.mkdirp(tempDir);
const readStream = fs.createReadStream(tempZip); await tar.extract({
const writeStream = fs.createWriteStream(sidecarPath); cwd: tempDir,
readStream file: tempZip,
.pipe(zlib.createGunzip()) //strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
.pipe(writeStream) });
.on("finish", () => { const files = await fs.readdir(tempDir);
console.log(`[INFO]: gunzip finished`); console.log(`[DEBUG]: "${name}" files in tempDir:`, files);
const extractedFile = files.find((file) => file.startsWith("虚空终端-"));
if (extractedFile) {
const extractedFilePath = path.join(tempDir, extractedFile);
await fs.rename(extractedFilePath, sidecarPath);
console.log(`[INFO]: "${name}" file renamed to "${sidecarPath}"`);
execSync(`chmod 755 ${sidecarPath}`); execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: chmod binary finished`); console.log(`[INFO]: "${name}" chmod binary finished`);
}) } else {
.on("error", (error) => console.error(error)); throw new Error(`Expected file not found in ${tempDir}`);
}
} else {
// gz
const readStream = fs.createReadStream(tempZip);
const writeStream = fs.createWriteStream(sidecarPath);
await new Promise((resolve, reject) => {
const onError = (error) => {
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
reject(error);
};
readStream
.pipe(zlib.createGunzip().on("error", onError))
.pipe(writeStream)
.on("finish", () => {
console.log(`[INFO]: "${name}" gunzip finished`);
execSync(`chmod 755 ${sidecarPath}`);
console.log(`[INFO]: "${name}" chmod binary finished`);
resolve();
})
.on("error", onError);
});
}
} catch (err) {
// 需要删除文件
await fs.remove(sidecarPath);
throw err;
} finally {
// delete temp dir
await fs.remove(tempDir);
} }
// delete temp dir
await fs.remove(tempDir);
} }
/** /**
* only Windows * download the file to the resources dir
* get the wintun.dll (not required)
*/ */
async function resolveWintun() { async function resolveResource(binInfo) {
const { platform } = process; const { file, downloadURL } = binInfo;
if (platform !== "win32") return; const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
const url = "https://www.wintun.net/builds/wintun-0.14.1.zip";
const tempDir = path.join(cwd, "pre-dev-temp-1");
const tempZip = path.join(tempDir, "wintun.zip");
const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll");
const targetPath = path.join(cwd, "src-tauri/resources", "wintun.dll");
if (!FORCE && (await fs.pathExists(targetPath))) return; if (!FORCE && (await fs.pathExists(targetPath))) return;
await fs.mkdirp(tempDir);
if (!(await fs.pathExists(tempZip))) {
await downloadFile(url, tempZip);
}
// unzip
const zip = new AdmZip(tempZip);
zip.extractAllTo(tempDir, true);
if (!(await fs.pathExists(wintunPath))) {
throw new Error(`path not found "${wintunPath}"`);
}
// move wintun.dll to resources
await fs.rename(wintunPath, targetPath);
// delete temp dir
await fs.remove(tempDir);
console.log(`[INFO]: resolve wintun.dll finished`);
}
/**
* get the Country.mmdb (not required)
*/
async function resolveMmdb() {
const url =
"https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb";
const resDir = path.join(cwd, "src-tauri", "resources");
const resPath = path.join(resDir, "Country.mmdb");
if (!FORCE && (await fs.pathExists(resPath))) return;
await fs.mkdirp(resDir); await fs.mkdirp(resDir);
await downloadFile(url, resPath); await downloadFile(downloadURL, targetPath);
console.log(`[INFO]: ${file} finished`);
} }
/** /**
* download file and save to `path` * download file and save to `path`
*/ */
async function downloadFile(url, path) { async function downloadFile(url, path) {
console.log(`[INFO]: downloading from "${url}"`); const options = {};
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
}
const response = await fetch(url, { const response = await fetch(url, {
...options,
method: "GET", method: "GET",
headers: { "Content-Type": "application/octet-stream" }, headers: { "Content-Type": "application/octet-stream" },
}); });
@@ -163,7 +321,171 @@ async function downloadFile(url, path) {
console.log(`[INFO]: download finished "${url}"`); console.log(`[INFO]: download finished "${url}"`);
} }
/// main // SimpleSC.dll
resolveSidecar().catch(console.error); const resolvePlugin = async () => {
resolveWintun().catch(console.error); const url =
resolveMmdb().catch(console.error); "https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip";
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
const pluginPath = path.join(pluginDir, "SimpleSC.dll");
await fs.mkdirp(pluginDir);
await fs.mkdirp(tempDir);
if (!FORCE && (await fs.pathExists(pluginPath))) return;
try {
if (!(await fs.pathExists(tempZip))) {
await downloadFile(url, tempZip);
}
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "SimpleSC" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.copyFile(tempDll, pluginPath);
console.log(`[INFO]: "SimpleSC" unzip finished`);
} finally {
await fs.remove(tempDir);
}
};
// 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
*/
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({
file: "clash-verge-service" + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
};
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({
file: "install-service" + ext,
downloadURL: `${SERVICE_URL}/install-service${ext}`,
});
};
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({
file: "uninstall-service" + ext,
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 = () =>
resolveResource({
file: "Country.mmdb",
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
});
const resolveGeosite = () =>
resolveResource({
file: "geosite.dat",
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
});
const resolveGeoIP = () =>
resolveResource({
file: "geoip.dat",
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
});
const resolveEnableLoopback = () =>
resolveResource({
file: "enableLoopback.exe",
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
});
const tasks = [
// { name: "clash", func: resolveClash, retry: 5 },
{
name: "clash-meta-alpha",
func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: "clash-meta",
func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,
},
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5 },
{ name: "install", func: resolveInstall, retry: 5 },
{ name: "uninstall", func: resolveUninstall, retry: 5 },
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 },
{ name: "geosite", func: resolveGeosite, retry: 5 },
{ name: "geoip", func: resolveGeoIP, retry: 5 },
{
name: "enableLoopback",
func: resolveEnableLoopback,
retry: 5,
winOnly: true,
},
{
name: "service_chmod",
func: resolveServicePermission,
retry: 1,
unixOnly: true,
},
];
async function runTask() {
const task = tasks.shift();
if (!task) return;
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++) {
try {
await task.func();
break;
} catch (err) {
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
if (i === task.retry - 1) throw err;
}
}
return runTask();
}
runTask();
runTask();

View File

@@ -4,31 +4,47 @@ import AdmZip from "adm-zip";
import { createRequire } from "module"; import { createRequire } from "module";
import { getOctokit, context } from "@actions/github"; import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const alpha = process.argv.slice(2)[1];
const ARCH_MAP = {
"x86_64-pc-windows-msvc": "x64",
"aarch64-pc-windows-msvc": "arm64",
};
/// Script for ci /// Script for ci
/// 打包绿色版/便携版 (only Windows) /// 打包绿色版/便携版 (only Windows)
async function resolveGreen() { async function resolvePortable() {
if (process.platform !== "win32") return; if (process.platform !== "win32") return;
const releaseDir = "./src-tauri/target/release"; const releaseDir = target
? `./src-tauri/target/${target}/release`
: `./src-tauri/target/release`;
const configDir = path.join(releaseDir, ".config");
if (!(await fs.pathExists(releaseDir))) { if (!(await fs.pathExists(releaseDir))) {
throw new Error("could not found the release dir"); throw new Error("could not found the release dir");
} }
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
const zip = new AdmZip(); const zip = new AdmZip();
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe")); zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
zip.addLocalFile(path.join(releaseDir, "clash.exe")); zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
zip.addLocalFile(path.join(releaseDir, "clash-meta-alpha.exe"));
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources"); zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
zip.addLocalFolder(configDir, ".config");
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const packageJson = require("../package.json"); const packageJson = require("../package.json");
const { version } = packageJson; const { version } = packageJson;
const zipFile = `Clash.Verge_${version}_x64_green.zip`; const zipFile = `Clash.Verge_${version}_${ARCH_MAP[target]}_portable.zip`;
zip.writeZip(zipFile); zip.writeZip(zipFile);
console.log("[INFO]: create green zip successfully"); console.log("[INFO]: create portable zip successfully");
// push release assets // push release assets
if (process.env.GITHUB_TOKEN === undefined) { if (process.env.GITHUB_TOKEN === undefined) {
@@ -37,12 +53,25 @@ async function resolveGreen() {
const options = { owner: context.repo.owner, repo: context.repo.repo }; const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN); const github = getOctokit(process.env.GITHUB_TOKEN);
const tag = alpha ? "alpha" : process.env.TAG_NAME || `v${version}`;
console.log("[INFO]: upload to ", tag);
const { data: release } = await github.rest.repos.getReleaseByTag({ const { data: release } = await github.rest.repos.getReleaseByTag({
...options, ...options,
tag: `v${version}`, tag,
}); });
let assets = release.assets.filter((x) => {
return x.name === zipFile;
});
if (assets.length > 0) {
let id = assets[0].id;
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: id,
});
}
console.log(release.name); console.log(release.name);
await github.rest.repos.uploadReleaseAsset({ await github.rest.repos.uploadReleaseAsset({
@@ -53,4 +82,4 @@ async function resolveGreen() {
}); });
} }
resolveGreen().catch(console.error); resolvePortable().catch(console.error);

View File

@@ -1,53 +0,0 @@
import fs from "fs-extra";
import { createRequire } from "module";
import { execSync } from "child_process";
import { resolveUpdateLog } from "./updatelog.mjs";
const require = createRequire(import.meta.url);
// publish
async function resolvePublish() {
const flag = process.argv[2] ?? "patch";
const packageJson = require("../package.json");
const tauriJson = require("../src-tauri/tauri.conf.json");
let [a, b, c] = packageJson.version.split(".").map(Number);
if (flag === "major") {
a += 1;
b = 0;
c = 0;
} else if (flag === "minor") {
b += 1;
c = 0;
} else if (flag === "patch") {
c += 1;
} else throw new Error(`invalid flag "${flag}"`);
const nextVersion = `${a}.${b}.${c}`;
packageJson.version = nextVersion;
tauriJson.package.version = nextVersion;
// 发布更新前先写更新日志
const nextTag = `v${nextVersion}`;
await resolveUpdateLog(nextTag);
await fs.writeFile(
"./package.json",
JSON.stringify(packageJson, undefined, 2)
);
await fs.writeFile(
"./src-tauri/tauri.conf.json",
JSON.stringify(tauriJson, undefined, 2)
);
execSync("git add ./package.json");
execSync("git add ./src-tauri/tauri.conf.json");
execSync(`git commit -m "v${nextVersion}"`);
execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
execSync(`git push`);
execSync(`git push origin v${nextVersion}`);
console.log(`Publish Successfully...`);
}
resolvePublish();

View File

@@ -43,9 +43,12 @@ async function resolveUpdater() {
darwin: { signature: "", url: "" }, // compatible with older formats darwin: { signature: "", url: "" }, // compatible with older formats
"darwin-aarch64": { signature: "", url: "" }, "darwin-aarch64": { signature: "", url: "" },
"darwin-intel": { signature: "", url: "" }, "darwin-intel": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" }, "linux-x86_64": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"linux-armv7": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" }, "windows-x86_64": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" }, // no supported "windows-aarch64": { signature: "", url: "" },
}, },
}; };
@@ -53,49 +56,67 @@ async function resolveUpdater() {
const { name, browser_download_url } = asset; const { name, browser_download_url } = asset;
// win64 url // win64 url
if (name.endsWith(".msi.zip")) { if (name.endsWith("x64-setup.nsis.zip")) {
updateData.platforms.win64.url = browser_download_url; updateData.platforms.win64.url = browser_download_url;
updateData.platforms["windows-x86_64"].url = browser_download_url; updateData.platforms["windows-x86_64"].url = browser_download_url;
} }
// win64 signature // win64 signature
if (name.endsWith(".msi.zip.sig")) { if (name.endsWith("x64-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.win64.signature = sig; updateData.platforms.win64.signature = sig;
updateData.platforms["windows-x86_64"].signature = sig; updateData.platforms["windows-x86_64"].signature = sig;
} }
// win arm url
if (name.endsWith("arm64-setup.nsis.zip")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
}
// win arm signature
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-aarch64"].signature = sig;
}
// darwin url (intel) // darwin url (intel)
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) { if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
updateData.platforms.darwin.url = browser_download_url; updateData.platforms.darwin.url = browser_download_url;
updateData.platforms["darwin-intel"].url = browser_download_url; updateData.platforms["darwin-intel"].url = browser_download_url;
updateData.platforms["darwin-x86_64"].url = browser_download_url;
} }
// darwin signature (intel) // darwin signature (intel)
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) { if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.darwin.signature = sig; updateData.platforms.darwin.signature = sig;
updateData.platforms["darwin-intel"].signature = sig; updateData.platforms["darwin-intel"].signature = sig;
updateData.platforms["darwin-x86_64"].signature = sig;
} }
// darwin url (aarch) // darwin url (aarch)
if (name.endsWith("aarch.app.tar.gz")) { if (name.endsWith("aarch64.app.tar.gz")) {
updateData.platforms["darwin-aarch64"].url = browser_download_url; updateData.platforms["darwin-aarch64"].url = browser_download_url;
} }
// darwin signature (aarch) // darwin signature (aarch)
if (name.endsWith("aarch.app.tar.gz.sig")) { if (name.endsWith("aarch64.app.tar.gz.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms["darwin-aarch64"].signature = sig; updateData.platforms["darwin-aarch64"].signature = sig;
} }
// linux url // linux x64 url
if (name.endsWith(".AppImage.tar.gz")) { if (name.endsWith("amd64.AppImage.tar.gz")) {
updateData.platforms.linux.url = browser_download_url; updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url; updateData.platforms["linux-x86_64"].url = browser_download_url;
// 暂时使用x64版本的url和sig使得可以检查更新但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].url = browser_download_url;
updateData.platforms["linux-armv7"].url = browser_download_url;
} }
// linux signature // linux x64 signature
if (name.endsWith(".AppImage.tar.gz.sig")) { if (name.endsWith("amd64.AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.linux.signature = sig; updateData.platforms.linux.signature = sig;
updateData.platforms["linux-x86_64"].signature = sig; updateData.platforms["linux-x86_64"].signature = sig;
// 暂时使用x64版本的url和sig使得可以检查更新但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].signature = sig;
updateData.platforms["linux-armv7"].signature = sig;
} }
}); });
@@ -117,10 +138,8 @@ async function resolveUpdater() {
Object.entries(updateDataNew.platforms).forEach(([key, value]) => { Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) { if (value.url) {
updateDataNew.platforms[key].url = value.url.replace( updateDataNew.platforms[key].url =
"https://github.com/", "https://mirror.ghproxy.com/" + value.url;
"https://hub.fastgit.xyz/"
);
} else { } else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`); console.log(`[Error]: updateDataNew.platforms.${key} is null`);
} }

5660
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,48 +1,57 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "0.1.0" version = "1.6.1"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0" license = "GPL-3.0-only"
repository = "https://github.com/zzzgydi/clash-verge.git" repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge" default-run = "clash-verge"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.0.0-rc.5", features = [] } tauri-build = { version = "1", features = [] }
[dependencies] [dependencies]
anyhow = "1.0"
dirs = "4.0.0"
open = "2.1.1"
dunce = "1.0.2"
nanoid = "0.4.0"
chrono = "0.4.19"
serde_json = "1.0"
serde_yaml = "0.8"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-rc.6", features = ["process-all", "shell-all", "system-tray", "updater", "window-all"] }
window-shadows = { git = "https://github.com/tauri-apps/window-shadows" }
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy" }
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
log = "0.4.14"
log4rs = "1.0.0"
warp = "0.3" warp = "0.3"
which = "4.2.2" anyhow = "1.0"
auto-launch = "0.2" dirs = "5.0"
open = "5.0"
log = "0.4"
dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.30"
boa_engine = "0.18"
serde_json = "1.0"
serde_yaml = "0.9"
once_cell = "1.18"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11.5"
parking_lot = "0.12"
percent-encoding = "2.3.1"
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
auto-launch = { git="https://github.com/zzzgydi/auto-launch", branch = "main" }
tauri = { 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]
winreg = { version = "0.10", features = ["transactions"] } runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.52.0"
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
#openssl
[features] [features]
default = [ "custom-protocol" ] default = ["custom-protocol"]
custom-protocol = [ "tauri/custom-protocol" ] custom-protocol = ["tauri/custom-protocol"]
verge-dev = [] verge-dev = []
debug-yml = []
[profile.release] [profile.release]
panic = "abort" panic = "abort"

17
src-tauri/Info.plist Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Clash Verge</string>
<key>CFBundleURLSchemes</key>
<array>
<string>clash</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -1,3 +1,3 @@
fn main() { fn main() {
tauri_build::build() tauri_build::build()
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,6 +1,6 @@
max_width = 100 max_width = 100
hard_tabs = false hard_tabs = false
tab_spaces = 2 tab_spaces = 4
newline_style = "Auto" newline_style = "Auto"
use_small_heuristics = "Default" use_small_heuristics = "Default"
reorder_imports = true reorder_imports = true

View File

@@ -1,370 +1,371 @@
use crate::{ use crate::{
core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig}, config::*,
states::{ClashState, ProfilesState, VergeState}, core::*,
utils::{dirs, sysopt::SysProxyConfig}, feat,
utils::{dirs, help, resolve},
}; };
use crate::{ret_err, wrap_err}; use crate::{ret_err, wrap_err};
use anyhow::Result; use anyhow::{Context, Result};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::process::Command; use std::collections::{HashMap, VecDeque};
use tauri::{api, Manager, State}; use sysproxy::Sysproxy;
use tauri::{api, Manager};
type CmdResult<T = ()> = Result<T, String>;
/// get all profiles from `profiles.yaml`
#[tauri::command] #[tauri::command]
pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> { pub fn get_profiles() -> CmdResult<IProfiles> {
let profiles = profiles_state.0.lock().unwrap(); Ok(Config::profiles().data().clone())
Ok(profiles.clone())
} }
/// synchronize data irregularly
#[tauri::command] #[tauri::command]
pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), String> { pub async fn enhance_profiles() -> CmdResult {
let mut profiles = profiles_state.0.lock().unwrap(); wrap_err!(CoreManager::global().update_config().await)?;
wrap_err!(profiles.sync_file()) handle::Handle::refresh_clash();
Ok(())
} }
/// import the profile from url
/// and save to `profiles.yaml`
#[tauri::command] #[tauri::command]
pub async fn import_profile( pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
url: String, let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
option: Option<PrfOption>, wrap_err!(Config::profiles().data().append_item(item))
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.append_item(item))
} }
/// new a profile
/// append a temp profile item file to the `profiles` dir
/// view the temp profile file by using vscode or other editor
#[tauri::command] #[tauri::command]
pub async fn create_profile( pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
item: PrfItem, // partial wrap_err!(Config::profiles().data().reorder(active_id, over_id))
file_data: Option<String>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.append_item(item))
} }
/// Update the profile
#[tauri::command] #[tauri::command]
pub async fn update_profile( pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
index: String, let item = wrap_err!(PrfItem::from(item, file_data).await)?;
option: Option<PrfOption>, wrap_err!(Config::profiles().data().append_item(item))
clash_state: State<'_, ClashState>, }
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { #[tauri::command]
let (url, opt) = { pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
// must release the lock here wrap_err!(feat::update_profile(index, option).await)
let profiles = profiles_state.0.lock().unwrap(); }
#[tauri::command]
pub async fn delete_profile(index: String) -> CmdResult {
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
if should_update {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
}
Ok(())
}
/// 修改profiles的
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
let _ = handle::Handle::update_systray_part();
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
Ok(())
}
Err(err) => {
Config::profiles().discard();
log::error!(target: "app", "{err}");
Err(format!("{err}"))
}
}
}
/// 修改某个profile item的
#[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
wrap_err!(timer::Timer::global().refresh())
}
#[tauri::command]
pub fn view_profile(app_handle: tauri::AppHandle, index: String) -> CmdResult {
let file = {
wrap_err!(Config::profiles().latest().get_item(&index))?
.file
.clone()
.ok_or("the file field is null")
}?;
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
if !path.exists() {
ret_err!("the file not found");
}
wrap_err!(help::open_file(app_handle, path))
}
#[tauri::command]
pub fn read_profile_file(index: String) -> CmdResult<String> {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = wrap_err!(profiles.get_item(&index))?; let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
}
// check the profile type #[tauri::command]
if let Some(typ) = item.itype.as_ref() { pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if *typ != "remote" { if file_data.is_none() {
ret_err!(format!("could not update the `{typ}` profile")); return Ok(());
}
} }
if item.url.is_none() { let profiles = Config::profiles();
ret_err!("failed to get the item url"); let profiles = profiles.latest();
} let item = wrap_err!(profiles.get_item(&index))?;
wrap_err!(item.save_file(file_data.unwrap()))
(item.url.clone().unwrap(), item.option.clone())
};
let fetch_opt = PrfOption::merge(opt, option);
let item = wrap_err!(PrfItem::from_url(&url, None, None, fetch_opt).await)?;
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.update_item(index.clone(), item))?;
// reactivate the profile
if Some(index) == profiles.get_current() {
let clash = clash_state.0.lock().unwrap();
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
}
Ok(())
} }
/// change the current profile
#[tauri::command] #[tauri::command]
pub fn select_profile( pub fn get_clash_info() -> CmdResult<ClashInfo> {
index: String, Ok(Config::clash().latest().get_client_info())
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.put_current(index))?;
let clash = clash_state.0.lock().unwrap();
wrap_err!(clash.activate_enhanced(&profiles, false, false))
} }
/// change the profile chain
#[tauri::command] #[tauri::command]
pub fn change_profile_chain( pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
chain: Option<Vec<String>>, Ok(Config::runtime().latest().config.clone())
app_handle: tauri::AppHandle,
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut clash = clash_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap();
profiles.put_chain(chain);
clash.set_window(app_handle.get_window("main"));
wrap_err!(clash.activate_enhanced(&profiles, false, false))
} }
/// manually exec enhanced profile
#[tauri::command] #[tauri::command]
pub fn enhance_profiles( pub fn get_runtime_yaml() -> CmdResult<String> {
app_handle: tauri::AppHandle, let runtime = Config::runtime();
clash_state: State<'_, ClashState>, let runtime = runtime.latest();
profiles_state: State<'_, ProfilesState>, let config = runtime.config.as_ref();
) -> Result<(), String> { wrap_err!(config
let mut clash = clash_state.0.lock().unwrap(); .ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
let profiles = profiles_state.0.lock().unwrap(); .and_then(
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
clash.set_window(app_handle.get_window("main")); ))
wrap_err!(clash.activate_enhanced(&profiles, false, false))
} }
/// delete profile item
#[tauri::command] #[tauri::command]
pub fn delete_profile( pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
index: String, Ok(Config::runtime().latest().exists_keys.clone())
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap();
if wrap_err!(profiles.delete_item(index))? {
let clash = clash_state.0.lock().unwrap();
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
}
Ok(())
} }
/// patch the profile config
#[tauri::command] #[tauri::command]
pub fn patch_profile( pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
index: String, Ok(Config::runtime().latest().chain_logs.clone())
profile: PrfItem,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.patch_item(index, profile))
} }
/// run vscode command to edit the profile
#[tauri::command] #[tauri::command]
pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> { pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
let profiles = profiles_state.0.lock().unwrap(); wrap_err!(feat::patch_clash(payload).await)
let item = wrap_err!(profiles.get_item(&index))?;
let file = item.file.clone();
if file.is_none() {
ret_err!("the file is null");
}
let path = dirs::app_profiles_dir().join(file.unwrap());
if !path.exists() {
ret_err!("the file not found");
}
// use vscode first
if let Ok(code) = which::which("code") {
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
if let Err(err) = Command::new(code)
.creation_flags(0x08000000)
.arg(path)
.spawn()
{
log::error!("failed to open file by VScode for {err}");
return Err("failed to open file by VScode".into());
}
}
#[cfg(not(target_os = "windows"))]
if let Err(err) = Command::new(code).arg(path).spawn() {
log::error!("failed to open file by VScode for {err}");
return Err("failed to open file by VScode".into());
}
return Ok(());
}
wrap_err!(open::that(path))
} }
/// read the profile item file data
#[tauri::command] #[tauri::command]
pub fn read_profile_file( pub fn get_verge_config() -> CmdResult<IVerge> {
index: String, Ok(Config::verge().data().clone())
profiles_state: State<'_, ProfilesState>,
) -> Result<String, String> {
let profiles = profiles_state.0.lock().unwrap();
let item = wrap_err!(profiles.get_item(&index))?;
let data = wrap_err!(item.read_file())?;
Ok(data)
} }
/// save the profile item file data
#[tauri::command] #[tauri::command]
pub fn save_profile_file( pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
index: String, wrap_err!(feat::patch_verge(payload).await)
file_data: Option<String>, }
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
if file_data.is_none() {
return Ok(());
}
let profiles = profiles_state.0.lock().unwrap(); #[tauri::command]
let item = wrap_err!(profiles.get_item(&index))?; pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
wrap_err!(item.save_file(file_data.unwrap())) wrap_err!(CoreManager::global().change_core(clash_core).await)
} }
/// restart the sidecar /// restart the sidecar
#[tauri::command] #[tauri::command]
pub fn restart_sidecar( pub async fn restart_sidecar() -> CmdResult {
clash_state: State<'_, ClashState>, wrap_err!(CoreManager::global().run_core().await)
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut clash = clash_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(clash.restart_sidecar(&mut profiles))
} }
/// get the clash core info from the state
/// the caller can also get the infomation by clash's api
#[tauri::command] #[tauri::command]
pub fn get_clash_info(clash_state: State<'_, ClashState>) -> Result<ClashInfo, String> { pub fn grant_permission(_core: String) -> CmdResult {
let clash = clash_state.0.lock().unwrap(); #[cfg(any(target_os = "macos", target_os = "linux"))]
Ok(clash.info.clone()) return wrap_err!(manager::grant_permission(_core));
}
/// update the clash core config #[cfg(not(any(target_os = "macos", target_os = "linux")))]
/// after putting the change to the clash core return Err("Unsupported target".into());
/// then we should save the latest config
#[tauri::command]
pub fn patch_clash_config(
payload: Mapping,
clash_state: State<'_, ClashState>,
verge_state: State<'_, VergeState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut clash = clash_state.0.lock().unwrap();
let mut verge = verge_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(clash.patch_config(payload, &mut verge, &mut profiles))
} }
/// get the system proxy /// get the system proxy
#[tauri::command] #[tauri::command]
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> { pub fn get_sys_proxy() -> CmdResult<Mapping> {
wrap_err!(SysProxyConfig::get_sys()) let current = wrap_err!(Sysproxy::get_system_proxy())?;
let mut map = Mapping::new();
map.insert("enable".into(), current.enable.into());
map.insert(
"server".into(),
format!("{}:{}", current.host, current.port).into(),
);
map.insert("bypass".into(), current.bypass.into());
Ok(map)
} }
/// get the current proxy config
/// which may not the same as system proxy
#[tauri::command] #[tauri::command]
pub fn get_cur_proxy(verge_state: State<'_, VergeState>) -> Result<Option<SysProxyConfig>, String> { pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
let verge = verge_state.0.lock().unwrap(); Ok(logger::Logger::global().get_log())
Ok(verge.cur_sysproxy.clone())
} }
/// get the verge config
#[tauri::command] #[tauri::command]
pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result<VergeConfig, String> { pub fn open_app_dir() -> CmdResult<()> {
let verge = verge_state.0.lock().unwrap(); let app_dir = wrap_err!(dirs::app_home_dir())?;
let mut config = verge.config.clone(); wrap_err!(open::that(app_dir))
if config.system_proxy_bypass.is_none() && verge.cur_sysproxy.is_some() {
config.system_proxy_bypass = Some(verge.cur_sysproxy.clone().unwrap().bypass)
}
Ok(config)
} }
/// patch the verge config
/// this command only save the config and not responsible for other things
#[tauri::command] #[tauri::command]
pub fn patch_verge_config( pub fn open_core_dir() -> CmdResult<()> {
payload: VergeConfig, let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
app_handle: tauri::AppHandle, let core_dir = core_dir.parent().ok_or("failed to get core dir")?;
clash_state: State<'_, ClashState>, wrap_err!(open::that(core_dir))
verge_state: State<'_, VergeState>, }
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let tun_mode = payload.enable_tun_mode.clone();
let system_proxy = payload.enable_system_proxy.clone();
let mut verge = verge_state.0.lock().unwrap(); #[tauri::command]
wrap_err!(verge.patch_config(payload))?; pub fn open_logs_dir() -> CmdResult<()> {
let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir))
}
// change system tray #[tauri::command]
if system_proxy.is_some() { pub fn open_web_url(url: String) -> CmdResult<()> {
app_handle wrap_err!(open::that(url))
.tray_handle() }
.get_item("system_proxy")
.set_selected(system_proxy.unwrap())
.unwrap();
}
// change tun mode #[cfg(windows)]
if tun_mode.is_some() { pub mod uwp {
#[cfg(target_os = "windows")] use super::*;
if *tun_mode.as_ref().unwrap() { use crate::core::win_uwp;
let wintun_dll = dirs::app_home_dir().join("wintun.dll");
if !wintun_dll.exists() { #[tauri::command]
log::error!("failed to enable TUN for missing `wintun.dll`"); pub async fn invoke_uwp_tool() -> CmdResult {
return Err("failed to enable TUN for missing `wintun.dll`".into()); wrap_err!(win_uwp::invoke_uwptools().await)
} }
}
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
timeout: i32,
) -> CmdResult<clash_api::DelayRes> {
match clash_api::get_proxy_delay(name, url, timeout).await {
Ok(res) => Ok(res),
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
pub fn get_portable_flag() -> CmdResult<bool> {
Ok(*dirs::PORTABLE_FLAG.get().unwrap_or(&false))
}
#[tauri::command]
pub async fn test_delay(url: String) -> CmdResult<u32> {
Ok(feat::test_delay(url).await.unwrap_or(10000u32))
}
#[tauri::command]
pub fn get_app_dir() -> CmdResult<String> {
let app_home_dir = wrap_err!(dirs::app_home_dir())?
.to_string_lossy()
.to_string();
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]
pub fn copy_icon_file(path: String, name: String) -> CmdResult<String> {
let file_path = std::path::Path::new(&path);
let icon_dir = wrap_err!(dirs::app_home_dir())?.join("icons");
if !icon_dir.exists() {
let _ = std::fs::create_dir_all(&icon_dir);
}
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() {
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) {
Ok(_) => Ok(dest_path.to_string_lossy().to_string()),
Err(err) => Err(err.to_string()),
}
} else {
return Err("file not found".to_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]
pub fn exit_app(app_handle: tauri::AppHandle) {
let _ = resolve::save_window_size_position(&app_handle, true);
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
std::process::exit(0);
}
pub mod service {
use super::*;
use crate::core::service;
#[tauri::command]
pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(service::check_service().await)
} }
let clash = clash_state.0.lock().unwrap(); #[tauri::command]
let profiles = profiles_state.0.lock().unwrap(); pub async fn install_service() -> CmdResult {
wrap_err!(service::install_service().await)
}
wrap_err!(clash.activate_enhanced(&profiles, false, false))?; #[tauri::command]
} pub async fn uninstall_service() -> CmdResult {
wrap_err!(service::uninstall_service().await)
Ok(()) }
} }
/// kill all sidecars when update app #[cfg(not(windows))]
#[tauri::command] pub mod uwp {
pub fn kill_sidecars() { use super::*;
api::process::kill_children();
}
/// open app config dir #[tauri::command]
#[tauri::command] pub async fn invoke_uwp_tool() -> CmdResult {
pub fn open_app_dir() -> Result<(), String> { Ok(())
let app_dir = dirs::app_home_dir(); }
wrap_err!(open::that(app_dir))
}
/// open logs dir
#[tauri::command]
pub fn open_logs_dir() -> Result<(), String> {
let log_dir = dirs::app_logs_dir();
wrap_err!(open::that(log_dir))
} }

View File

@@ -0,0 +1,360 @@
use crate::utils::{dirs, help};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
str::FromStr,
};
#[derive(Default, Debug, Clone)]
pub struct IClashTemp(pub Mapping);
impl IClashTemp {
pub fn new() -> Self {
let template = Self::template();
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
Ok(mut map) => {
template.0.keys().for_each(|key| {
if !map.contains_key(key) {
map.insert(key.clone(), template.0.get(key).unwrap().clone());
}
});
Self(Self::guard(map))
}
Err(err) => {
log::error!(target: "app", "{err}");
template
}
}
}
pub fn template() -> Self {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("stack".into(), "gvisor".into());
tun.insert("device".into(), "Meta".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
tun.insert("dns-hijack".into(), vec!["any:53"].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("socks-port".into(), 7898.into());
map.insert("port".into(), 7899.into());
map.insert("log-level".into(), "info".into());
map.insert("allow-lan".into(), false.into());
map.insert("mode".into(), "rule".into());
map.insert("external-controller".into(), "127.0.0.1:9097".into());
map.insert("secret".into(), "".into());
map.insert("tun".into(), tun.into());
Self(map)
}
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 socks_port = Self::guard_socks_port(&config);
let port = Self::guard_port(&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("socks-port".into(), socks_port.into());
config.insert("port".into(), port.into());
config.insert("external-controller".into(), ctrl.into());
config
}
pub fn patch_config(&mut self, patch: Mapping) {
for (key, value) in patch.into_iter() {
self.0.insert(key, value);
}
}
pub fn save_config(&self) -> Result<()> {
help::save_yaml(
&dirs::clash_path()?,
&self.0,
Some("# Generated by Clash Verge"),
)
}
pub fn get_mixed_port(&self) -> u16 {
Self::guard_mixed_port(&self.0)
}
#[allow(unused)]
pub fn get_socks_port(&self) -> u16 {
Self::guard_socks_port(&self.0)
}
#[allow(unused)]
pub fn get_port(&self) -> u16 {
Self::guard_port(&self.0)
}
pub fn get_client_info(&self) -> ClashInfo {
let config = &self.0;
ClashInfo {
mixed_port: Self::guard_mixed_port(config),
socks_port: Self::guard_socks_port(config),
port: Self::guard_port(config),
server: Self::guard_client_ctrl(config),
secret: config.get("secret").and_then(|value| match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
}),
}
}
#[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 {
let mut port = config
.get("mixed-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(7897);
if port == 0 {
port = 7897;
}
port
}
pub fn guard_socks_port(config: &Mapping) -> u16 {
let mut port = config
.get("socks-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(7898);
if port == 0 {
port = 7898;
}
port
}
pub fn guard_port(config: &Mapping) -> u16 {
let mut port = config
.get("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(7899);
if port == 0 {
port = 7899;
}
port
}
pub fn guard_server_ctrl(config: &Mapping) -> String {
config
.get("external-controller")
.and_then(|value| match value.as_str() {
Some(val_str) => {
let val_str = val_str.trim();
let val = match val_str.starts_with(':') {
true => format!("127.0.0.1{val_str}"),
false => val_str.to_owned(),
};
SocketAddr::from_str(val.as_str())
.ok()
.map(|s| s.to_string())
}
None => None,
})
.unwrap_or("127.0.0.1:9097".into())
}
pub fn guard_client_ctrl(config: &Mapping) -> String {
let value = Self::guard_server_ctrl(config);
match SocketAddr::from_str(value.as_str()) {
Ok(mut socket) => {
if socket.ip().is_unspecified() {
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
}
socket.to_string()
}
Err(_) => "127.0.0.1:9097".into(),
}
}
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct ClashInfo {
/// clash core port
pub mixed_port: u16,
pub socks_port: u16,
pub port: u16,
/// same as `external-controller`
pub server: String,
/// clash secret
pub secret: Option<String>,
}
#[test]
fn test_clash_info() {
fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
let mut map = Mapping::new();
map.insert("mixed-port".into(), mp.into());
map.insert("external-controller".into(), ec.into());
IClashTemp(IClashTemp::guard(map)).get_client_info()
}
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
ClashInfo {
mixed_port: port,
socks_port: 7898,
port: 7899,
server: server.into(),
secret: None,
}
}
assert_eq!(
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
get_result(7897, "127.0.0.1:9097")
);
assert_eq!(get_case("", ""), get_result(7897, "127.0.0.1:9097"));
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9097"));
assert_eq!(
get_case(8888, "127.0.0.1:8888"),
get_result(8888, "127.0.0.1:8888")
);
assert_eq!(
get_case(8888, " :98888 "),
get_result(8888, "127.0.0.1:9097")
);
assert_eq!(
get_case(8888, "0.0.0.0:8080 "),
get_result(8888, "127.0.0.1:8080")
);
assert_eq!(
get_case(8888, "0.0.0.0:8080"),
get_result(8888, "127.0.0.1:8080")
);
assert_eq!(
get_case(8888, "[::]:8080"),
get_result(8888, "127.0.0.1:8080")
);
assert_eq!(
get_case(8888, "192.168.1.1:8080"),
get_result(8888, "192.168.1.1:8080")
);
assert_eq!(
get_case(8888, "192.168.1.1:80800"),
get_result(8888, "127.0.0.1:9097")
);
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClash {
pub mixed_port: Option<u16>,
pub allow_lan: Option<bool>,
pub log_level: Option<String>,
pub ipv6: Option<bool>,
pub mode: Option<String>,
pub external_controller: Option<String>,
pub secret: Option<String>,
pub dns: Option<IClashDNS>,
pub tun: Option<IClashTUN>,
pub interface_name: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashTUN {
pub enable: Option<bool>,
pub stack: Option<String>,
pub auto_route: Option<bool>,
pub auto_detect_interface: Option<bool>,
pub dns_hijack: Option<Vec<String>>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashDNS {
pub enable: Option<bool>,
pub listen: Option<String>,
pub default_nameserver: Option<Vec<String>>,
pub enhanced_mode: Option<String>,
pub fake_ip_range: Option<String>,
pub use_hosts: Option<bool>,
pub fake_ip_filter: Option<Vec<String>>,
pub nameserver: Option<Vec<String>>,
pub fallback: Option<Vec<String>>,
pub fallback_filter: Option<IClashFallbackFilter>,
pub nameserver_policy: Option<Vec<String>>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct IClashFallbackFilter {
pub geoip: Option<bool>,
pub geoip_code: Option<String>,
pub ipcidr: Option<Vec<String>>,
pub domain: Option<Vec<String>>,
}

View File

@@ -0,0 +1,103 @@
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
use crate::{
enhance,
utils::{dirs, help},
};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::{env::temp_dir, path::PathBuf};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
pub struct Config {
clash_config: Draft<IClashTemp>,
verge_config: Draft<IVerge>,
profiles_config: Draft<IProfiles>,
runtime_config: Draft<IRuntime>,
}
impl Config {
pub fn global() -> &'static Config {
static CONFIG: OnceCell<Config> = OnceCell::new();
CONFIG.get_or_init(|| Config {
clash_config: Draft::from(IClashTemp::new()),
verge_config: Draft::from(IVerge::new()),
profiles_config: Draft::from(IProfiles::new()),
runtime_config: Draft::from(IRuntime::new()),
})
}
pub fn clash() -> Draft<IClashTemp> {
Self::global().clash_config.clone()
}
pub fn verge() -> Draft<IVerge> {
Self::global().verge_config.clone()
}
pub fn profiles() -> Draft<IProfiles> {
Self::global().profiles_config.clone()
}
pub fn runtime() -> Draft<IRuntime> {
Self::global().runtime_config.clone()
}
/// 初始化订阅
pub fn init_config() -> Result<()> {
crate::log_err!(Self::generate());
if let Err(err) = Self::generate_file(ConfigType::Run) {
log::error!(target: "app", "{err}");
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
// 如果不存在就将默认的clash文件拿过来
if !runtime_path.exists() {
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
)?;
}
}
Ok(())
}
/// 将订阅丢到对应的文件中
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
let path = match typ {
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
};
let runtime = Config::runtime();
let runtime = runtime.latest();
let config = runtime
.config
.as_ref()
.ok_or(anyhow!("failed to get runtime config"))?;
help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?;
Ok(path)
}
/// 生成订阅存好
pub fn generate() -> Result<()> {
let (config, exists_keys, logs) = enhance::enhance();
*Config::runtime().draft() = IRuntime {
config: Some(config),
exists_keys,
chain_logs: logs,
};
Ok(())
}
}
#[derive(Debug)]
pub enum ConfigType {
Run,
Check,
}

View File

@@ -0,0 +1,127 @@
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct Draft<T: Clone + ToOwned> {
inner: Arc<Mutex<(T, Option<T>)>>,
}
macro_rules! draft_define {
($id: ident) => {
impl Draft<$id> {
#[allow(unused)]
pub fn data(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
}
pub fn latest(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
&mut inner.0
} else {
inner.1.as_mut().unwrap()
}
})
}
pub fn draft(&self) -> MappedMutexGuard<$id> {
MutexGuard::map(self.inner.lock(), |inner| {
if inner.1.is_none() {
inner.1 = Some(inner.0.clone());
}
inner.1.as_mut().unwrap()
})
}
pub fn apply(&self) -> Option<$id> {
let mut inner = self.inner.lock();
match inner.1.take() {
Some(draft) => {
let old_value = inner.0.to_owned();
inner.0 = draft.to_owned();
Some(old_value)
}
None => None,
}
}
pub fn discard(&self) -> Option<$id> {
let mut inner = self.inner.lock();
inner.1.take()
}
}
impl From<$id> for Draft<$id> {
fn from(data: $id) -> Self {
Draft {
inner: Arc::new(Mutex::new((data, None))),
}
}
}
};
}
// draft_define!(IClash);
draft_define!(IClashTemp);
draft_define!(IProfiles);
draft_define!(IRuntime);
draft_define!(IVerge);
#[test]
fn test_draft() {
let verge = IVerge {
enable_auto_launch: Some(true),
enable_tun_mode: Some(false),
..IVerge::default()
};
let draft = Draft::from(verge);
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert_eq!(draft.draft().enable_tun_mode, Some(false));
let mut d = draft.draft();
d.enable_auto_launch = Some(false);
d.enable_tun_mode = Some(true);
drop(d);
assert_eq!(draft.data().enable_auto_launch, Some(true));
assert_eq!(draft.data().enable_tun_mode, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
assert_eq!(draft.latest().enable_auto_launch, Some(false));
assert_eq!(draft.latest().enable_tun_mode, Some(true));
assert!(draft.apply().is_some());
assert!(draft.apply().is_none());
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.data().enable_tun_mode, Some(true));
assert_eq!(draft.draft().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_tun_mode, Some(true));
let mut d = draft.draft();
d.enable_auto_launch = Some(true);
drop(d);
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert_eq!(draft.draft().enable_auto_launch, Some(true));
assert!(draft.discard().is_some());
assert_eq!(draft.data().enable_auto_launch, Some(false));
assert!(draft.discard().is_none());
assert_eq!(draft.draft().enable_auto_launch, Some(false));
}

View File

@@ -0,0 +1,15 @@
mod clash;
mod config;
mod draft;
mod prfitem;
mod profiles;
mod runtime;
mod verge;
pub use self::clash::*;
pub use self::config::*;
pub use self::draft::*;
pub use self::prfitem::*;
pub use self::profiles::*;
pub use self::runtime::*;
pub use self::verge::*;

View File

@@ -0,0 +1,404 @@
use crate::utils::{dirs, help, resolve::VERSION, tmpl};
use anyhow::{bail, Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::fs;
use sysproxy::Sysproxy;
use super::Config;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct PrfItem {
pub uid: Option<String>,
/// profile item type
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
pub itype: Option<String>,
/// profile name
pub name: Option<String>,
/// profile file
pub file: Option<String>,
/// profile description
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>,
/// source url
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// selected information
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<Vec<PrfSelected>>,
/// subscription user info
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<PrfExtra>,
/// updated time
pub updated: Option<usize>,
/// some options of the item
#[serde(skip_serializing_if = "Option::is_none")]
pub option: Option<PrfOption>,
/// profile web page url
#[serde(skip_serializing_if = "Option::is_none")]
pub home: Option<String>,
/// the file data
#[serde(skip)]
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct PrfSelected {
pub name: Option<String>,
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
pub struct PrfExtra {
pub upload: u64,
pub download: u64,
pub total: u64,
pub expire: u64,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
pub struct PrfOption {
/// for `remote` profile's http request
/// see issue #13
#[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
/// for `remote` profile
/// use system proxy
#[serde(skip_serializing_if = "Option::is_none")]
pub with_proxy: Option<bool>,
/// for `remote` profile
/// use self proxy
#[serde(skip_serializing_if = "Option::is_none")]
pub self_proxy: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub update_interval: Option<u64>,
/// for `remote` profile
/// disable certificate validation
/// default is `false`
#[serde(skip_serializing_if = "Option::is_none")]
pub danger_accept_invalid_certs: Option<bool>,
}
impl PrfOption {
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
match (one, other) {
(Some(mut a), Some(b)) => {
a.user_agent = b.user_agent.or(a.user_agent);
a.with_proxy = b.with_proxy.or(a.with_proxy);
a.self_proxy = b.self_proxy.or(a.self_proxy);
a.danger_accept_invalid_certs = b.danger_accept_invalid_certs.or(a.danger_accept_invalid_certs);
a.update_interval = b.update_interval.or(a.update_interval);
Some(a)
}
t => t.0.or(t.1),
}
}
}
impl PrfItem {
/// From partial item
/// must contain `itype`
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
if item.itype.is_none() {
bail!("type should not be null");
}
match item.itype.unwrap().as_str() {
"remote" => {
if item.url.is_none() {
bail!("url should not be null");
}
let url = item.url.as_ref().unwrap().as_str();
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, item.option).await
}
"local" => {
let name = item.name.unwrap_or("Local File".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc, file_data)
}
"merge" => {
let name = item.name.unwrap_or("Merge".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_merge(name, desc)
}
"script" => {
let name = item.name.unwrap_or("Script".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
}
typ => bail!("invalid profile item type \"{typ}\""),
}
}
/// ## Local type
/// create a new item from name/desc
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
let uid = help::get_uid("l");
let file = format!("{uid}.yaml");
Ok(PrfItem {
uid: Some(uid),
itype: Some("local".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
})
}
/// ## Remote type
/// create a new item from url
pub async fn from_url(
url: &str,
name: Option<String>,
desc: Option<String>,
option: Option<PrfOption>,
) -> Result<PrfItem> {
let opt_ref = option.as_ref();
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
let accept_invalid_certs = opt_ref.map_or(false, |o| o.danger_accept_invalid_certs.unwrap_or(false));
let user_agent = opt_ref.and_then(|o| o.user_agent.clone());
let update_interval = opt_ref.and_then(|o| o.update_interval);
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
// 使用软件自己的代理
if self_proxy {
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let proxy_scheme = format!("http://127.0.0.1:{port}");
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
// 使用系统代理
else if with_proxy {
if let Ok(p @ Sysproxy { enable: true, .. }) = Sysproxy::get_system_proxy() {
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
}
let version = match VERSION.get() {
Some(v) => format!("clash-verge/v{}", v),
None => "clash-verge/unknown".to_string(),
};
builder = builder.danger_accept_invalid_certs(accept_invalid_certs);
builder = builder.user_agent(user_agent.unwrap_or(version));
let resp = builder.build()?.get(url).send().await?;
let status_code = resp.status();
if !StatusCode::is_success(&status_code) {
bail!("failed to fetch remote profile with status {status_code}")
}
let header = resp.headers();
// parse the Subscription UserInfo
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload").unwrap_or(0),
download: help::parse_str(sub_info, "download").unwrap_or(0),
total: help::parse_str(sub_info, "total").unwrap_or(0),
expire: help::parse_str(sub_info, "expire").unwrap_or(0),
})
}
None => None,
};
// parse the Content-Disposition
let filename = match header.get("Content-Disposition") {
Some(value) => {
let filename = format!("{value:?}");
let filename = filename.trim_matches('"');
match help::parse_str::<String>(filename, "filename*") {
Some(filename) => {
let iter = percent_encoding::percent_decode(filename.as_bytes());
let filename = iter.decode_utf8().unwrap_or_default();
filename.split("''").last().map(|s| s.to_string())
}
None => match help::parse_str::<String>(filename, "filename") {
Some(filename) => {
let filename = filename.trim_matches('"');
Some(filename.to_string())
}
None => None,
},
}
}
None => Some(
crate::utils::help::get_last_part_and_decode(url).unwrap_or("Remote File".into()),
),
};
let option = match update_interval {
Some(val) => Some(PrfOption {
update_interval: Some(val),
..PrfOption::default()
}),
None => match header.get("profile-update-interval") {
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
Ok(val) => Some(PrfOption {
update_interval: Some(val * 60), // hour -> min
..PrfOption::default()
}),
Err(_) => None,
},
None => None,
},
};
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 file = format!("{uid}.yaml");
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
let data = resp.text_with_charset("utf-8").await?;
// process the charset "UTF-8 with BOM"
let data = data.trim_start_matches('\u{feff}');
// check the data whether the valid yaml format
let yaml = serde_yaml::from_str::<Mapping>(data)
.context("the remote profile data is invalid yaml")?;
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
bail!("profile does not contain `proxies` or `proxy-providers`");
}
Ok(PrfItem {
uid: Some(uid),
itype: Some("remote".into()),
name: Some(name),
desc,
file: Some(file),
url: Some(url.into()),
selected: None,
extra,
option,
home,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(data.into()),
})
}
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
Ok(PrfItem {
uid: Some(uid),
itype: Some("merge".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
home: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_MERGE.into()),
})
}
/// ## Script type (enhance)
/// create the enhanced item by using javascript quick.js
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("s");
let file = format!("{uid}.js"); // js ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("script".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
home: None,
selected: None,
extra: None,
option: None,
updated: Some(chrono::Local::now().timestamp() as usize),
file_data: Some(tmpl::ITEM_SCRIPT.into()),
})
}
/// get the file data
pub fn read_file(&self) -> Result<String> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir()?.join(file);
fs::read_to_string(path).context("failed to read the file")
}
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir()?.join(file);
fs::write(path, data.as_bytes()).context("failed to save the file")
}
}

View File

@@ -0,0 +1,298 @@
use super::prfitem::PrfItem;
use crate::utils::{dirs, help};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{fs, io::Write};
/// Define the `profiles.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IProfiles {
/// same as PrfConfig.current
pub current: Option<String>,
/// same as PrfConfig.chain
pub chain: Option<Vec<String>>,
/// profile list
pub items: Option<Vec<PrfItem>>,
}
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key;
}
};
}
impl IProfiles {
pub fn new() -> Self {
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
Ok(mut profiles) => {
if profiles.items.is_none() {
profiles.items = Some(vec![]);
}
// compatible with the old old old version
if let Some(items) = profiles.items.as_mut() {
for item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
}
}
}
profiles
}
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
}
}
pub fn template() -> Self {
Self {
items: Some(vec![]),
..Self::default()
}
}
pub fn save_file(&self) -> Result<()> {
help::save_yaml(
&dirs::profiles_path()?,
self,
Some("# Profiles Config for Clash Verge"),
)
}
/// 只修改currentvalid和chain
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(current) = patch.current {
let items = self.items.as_ref().unwrap();
let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid;
}
}
if let Some(chain) = patch.chain {
self.chain = Some(chain);
}
Ok(())
}
pub fn get_current(&self) -> Option<String> {
self.current.clone()
}
/// get items ref
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
self.items.as_ref()
}
/// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if let Some(items) = self.items.as_ref() {
let some_uid = Some(uid.clone());
for each in items.iter() {
if each.uid == some_uid {
return Ok(each);
}
}
}
bail!("failed to get the profile item \"uid:{uid}\"");
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() {
bail!("the uid should not be null");
}
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
if item.file.is_none() {
bail!("the file should not be null");
}
let file = item.file.clone().unwrap();
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
.with_context(|| format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?;
}
if self.items.is_none() {
self.items = Some(vec![]);
}
if let Some(items) = self.items.as_mut() {
items.push(item)
}
self.save_file()
}
/// reorder items
pub fn reorder(&mut self, active_id: String, over_id: String) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
let mut old_index = None;
let mut new_index = None;
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(active_id.clone()) {
old_index = Some(i);
}
if items[i].uid == Some(over_id.clone()) {
new_index = Some(i);
}
}
if old_index.is_none() || new_index.is_none() {
return Ok(());
}
let item = items.remove(old_index.unwrap());
items.insert(new_index.unwrap(), item);
self.items = Some(items);
self.save_file()
}
/// update the item value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or_default();
for each in items.iter_mut() {
if each.uid == Some(uid.clone()) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
self.items = Some(items);
return self.save_file();
}
}
self.items = Some(items);
bail!("failed to find the profile item \"uid:{uid}\"")
}
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
// find the item
let _ = self.get_item(&uid)?;
if let Some(items) = self.items.as_mut() {
let some_uid = Some(uid.clone());
for each in items.iter_mut() {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
each.home = item.home;
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
let file = each.file.take();
let file =
file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
// the file must exists
each.file = Some(file.clone());
let path = dirs::app_profiles_dir()?.join(&file);
fs::File::create(path)
.with_context(|| format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.with_context(|| format!("failed to write to file \"{}\"", file))?;
}
break;
}
}
}
self.save_file()
}
/// delete item
/// if delete the current then return true
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
let mut items = self.items.take().unwrap_or_default();
let mut index = None;
// get the index
for (i, _) in items.iter().enumerate() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
}
}
if let Some(index) = index {
if let Some(file) = items.remove(index).file {
let _ = dirs::app_profiles_dir().map(|path| {
let path = path.join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
}
// delete the original uid
if current == uid {
self.current = match !items.is_empty() {
true => items[0].uid.clone(),
false => None,
};
}
self.items = Some(items);
self.save_file()?;
Ok(current == uid)
}
/// 获取current指向的订阅内容
pub fn current_mapping(&self) -> Result<Mapping> {
match (self.current.as_ref(), self.items.as_ref()) {
(Some(current), Some(items)) => {
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
let file_path = match item.file.as_ref() {
Some(file) => dirs::app_profiles_dir()?.join(file),
None => bail!("failed to get the file field"),
};
return help::read_merge_mapping(&file_path);
}
bail!("failed to find the current profile \"uid:{current}\"");
}
_ => Ok(Mapping::new()),
}
}
}

View File

@@ -0,0 +1,45 @@
use crate::enhance::field::use_keys;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use std::collections::HashMap;
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IRuntime {
pub config: Option<Mapping>,
// 记录在订阅中包括merge和script生成的出现过的keys
// 这些keys不一定都生效
pub exists_keys: Vec<String>,
pub chain_logs: HashMap<String, Vec<(String, String)>>,
}
impl IRuntime {
pub fn new() -> Self {
Self::default()
}
// 这里只更改 allow-lan | ipv6 | log-level | tun
pub fn patch_config(&mut self, patch: Mapping) {
if let Some(config) = self.config.as_mut() {
["allow-lan", "ipv6", "log-level"]
.into_iter()
.for_each(|key| {
if let Some(value) = patch.get(key).to_owned() {
config.insert(key.into(), value.clone());
}
});
let tun = config.get("tun");
let mut tun = tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = patch.get("tun");
let patch_tun = patch_tun.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
use_keys(&patch_tun).into_iter().for_each(|key| {
if let Some(value) = patch_tun.get(&key).to_owned() {
tun.insert(key.into(), value.clone());
}
});
config.insert("tun".into(), Value::from(tun));
}
}
}

View File

@@ -0,0 +1,310 @@
use crate::utils::{dirs, help};
use anyhow::Result;
use log::LevelFilter;
use serde::{Deserialize, Serialize};
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVerge {
/// app listening port for app singleton
pub app_singleton_port: Option<u16>,
/// app log level
/// silent | error | warn | info | debug | trace
pub app_log_level: Option<String>,
// i18n
pub language: Option<String>,
/// `light` or `dark` or `system`
pub theme_mode: Option<String>,
/// tray click event
pub tray_event: Option<String>,
/// copy env type
pub env_type: Option<String>,
/// start page
pub start_page: Option<String>,
/// startup script path
pub startup_script: Option<String>,
/// enable traffic graph default is true
pub traffic_graph: Option<bool>,
/// show memory info (only for Clash Meta)
pub enable_memory_usage: Option<bool>,
/// enable group icon
pub enable_group_icon: Option<bool>,
/// common tray icon
pub common_tray_icon: Option<bool>,
/// menu icon
pub menu_icon: Option<String>,
/// sysproxy tray icon
pub sysproxy_tray_icon: Option<bool>,
/// tun tray icon
pub tun_tray_icon: Option<bool>,
/// clash tun mode
pub enable_tun_mode: Option<bool>,
/// windows service mode
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_service_mode: Option<bool>,
/// can the app auto startup
pub enable_auto_launch: Option<bool>,
/// not show the window on launch
pub enable_silent_start: Option<bool>,
/// set system proxy
pub enable_system_proxy: Option<bool>,
/// enable proxy guard
pub enable_proxy_guard: Option<bool>,
/// set system proxy bypass
pub system_proxy_bypass: Option<String>,
/// proxy guard duration
pub proxy_guard_duration: Option<u64>,
/// theme setting
pub theme_setting: Option<IVergeTheme>,
/// web ui list
pub web_ui_list: Option<Vec<String>>,
/// clash core path
#[serde(skip_serializing_if = "Option::is_none")]
pub clash_core: Option<String>,
/// hotkey map
/// format: {func},{key}
pub hotkeys: Option<Vec<String>>,
/// 切换代理时自动关闭连接
pub auto_close_connection: Option<bool>,
/// 是否自动检查更新
pub auto_check_update: Option<bool>,
/// 默认的延迟测试连接
pub default_latency_test: Option<String>,
/// 默认的延迟测试超时时间
pub default_latency_timeout: Option<i32>,
/// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>,
/// 测试网站列表
pub test_list: Option<Vec<IVergeTestItem>>,
/// 日志清理
/// 0: 不清理; 1: 7天; 2: 30天; 3: 90天
pub auto_log_clean: Option<i32>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_size_position: Option<Vec<f64>>,
/// window size and position
#[serde(skip_serializing_if = "Option::is_none")]
pub window_is_maximized: Option<bool>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
/// 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_socks_port: Option<u16>,
pub verge_port: Option<u16>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVergeTestItem {
pub uid: Option<String>,
pub name: Option<String>,
pub icon: Option<String>,
pub url: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct IVergeTheme {
pub primary_color: Option<String>,
pub secondary_color: Option<String>,
pub primary_text: Option<String>,
pub secondary_text: Option<String>,
pub info_color: Option<String>,
pub error_color: Option<String>,
pub warning_color: Option<String>,
pub success_color: Option<String>,
pub font_family: Option<String>,
pub css_injection: Option<String>,
}
impl IVerge {
pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config,
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
}
}
pub fn template() -> Self {
Self {
clash_core: Some("clash-meta".into()),
language: Some("zh".into()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
env_type: Some("bash".into()),
#[cfg(target_os = "windows")]
env_type: Some("powershell".into()),
start_page: Some("/".into()),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_group_icon: Some(true),
menu_icon: Some("monochrome".into()),
common_tray_icon: Some(false),
sysproxy_tray_icon: Some(false),
tun_tray_icon: Some(false),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_system_proxy: 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_socks_port: Some(7898),
verge_port: Some(7899),
enable_proxy_guard: Some(false),
proxy_guard_duration: Some(30),
auto_close_connection: Some(true),
auto_check_update: Some(true),
enable_builtin_enhanced: Some(true),
auto_log_clean: Some(3),
..Self::default()
}
}
/// Save IVerge App Config
pub fn save_file(&self) -> Result<()> {
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config"))
}
/// patch verge config
/// only save to file
pub fn patch_config(&mut self, patch: IVerge) {
macro_rules! patch {
($key: tt) => {
if patch.$key.is_some() {
self.$key = patch.$key;
}
};
}
patch!(app_log_level);
patch!(language);
patch!(theme_mode);
patch!(tray_event);
patch!(env_type);
patch!(start_page);
patch!(startup_script);
patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_group_icon);
patch!(menu_icon);
patch!(common_tray_icon);
patch!(sysproxy_tray_icon);
patch!(tun_tray_icon);
patch!(enable_tun_mode);
patch!(enable_service_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
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_socks_port);
patch!(verge_port);
patch!(enable_system_proxy);
patch!(enable_proxy_guard);
patch!(system_proxy_bypass);
patch!(proxy_guard_duration);
patch!(theme_setting);
patch!(web_ui_list);
patch!(clash_core);
patch!(hotkeys);
patch!(auto_close_connection);
patch!(auto_check_update);
patch!(default_latency_test);
patch!(default_latency_timeout);
patch!(enable_builtin_enhanced);
patch!(proxy_layout_column);
patch!(test_list);
patch!(auto_log_clean);
patch!(window_size_position);
patch!(window_is_maximized);
}
/// 在初始化前尝试拿到单例端口的值
pub fn get_singleton_port() -> u16 {
#[cfg(not(feature = "verge-dev"))]
const SERVER_PORT: u16 = 33331;
#[cfg(feature = "verge-dev")]
const SERVER_PORT: u16 = 11233;
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
Err(_) => SERVER_PORT, // 这里就不log错误了
}
}
/// 获取日志等级
pub fn get_log_level(&self) -> LevelFilter {
if let Some(level) = self.app_log_level.as_ref() {
match level.to_lowercase().as_str() {
"silent" => LevelFilter::Off,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"info" => LevelFilter::Info,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
_ => LevelFilter::Info,
}
} else {
LevelFilter::Info
}
}
}

View File

@@ -1,518 +0,0 @@
use super::{PrfEnhancedResult, Profiles, Verge, VergeConfig};
use crate::log_if_err;
use crate::utils::{config, dirs, help};
use anyhow::{bail, Result};
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
use std::{collections::HashMap, time::Duration};
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tauri::Window;
use tokio::time::sleep;
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct ClashInfo {
/// clash sidecar status
pub status: String,
/// clash core port
pub port: Option<String>,
/// same as `external-controller`
pub server: Option<String>,
/// clash secret
pub secret: Option<String>,
}
pub struct Clash {
/// maintain the clash config
pub config: Mapping,
/// some info
pub info: ClashInfo,
/// clash sidecar
pub sidecar: Option<CommandChild>,
/// save the main window
pub window: Option<Window>,
}
impl Clash {
pub fn new() -> Clash {
let config = Clash::read_config();
let info = Clash::get_info(&config);
Clash {
config,
info,
sidecar: None,
window: None,
}
}
/// get clash config
fn read_config() -> Mapping {
config::read_yaml::<Mapping>(dirs::clash_path())
}
/// save the clash config
fn save_config(&self) -> Result<()> {
config::save_yaml(
dirs::clash_path(),
&self.config,
Some("# Default Config For Clash Core\n\n"),
)
}
/// parse the clash's config.yaml
/// get some information
fn get_info(clash_config: &Mapping) -> ClashInfo {
let key_port_1 = Value::String("port".to_string());
let key_port_2 = Value::String("mixed-port".to_string());
let key_server = Value::String("external-controller".to_string());
let key_secret = Value::String("secret".to_string());
let port = match clash_config.get(&key_port_1) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
},
_ => None,
};
let port = match port {
Some(_) => port,
None => match clash_config.get(&key_port_2) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
},
_ => None,
},
};
let server = match clash_config.get(&key_server) {
Some(value) => match value {
Value::String(val_str) => {
// `external-controller` could be
// "127.0.0.1:9090" or ":9090"
// Todo: maybe it could support single port
let server = val_str.clone();
let server = match server.starts_with(":") {
true => format!("127.0.0.1{server}"),
false => server,
};
Some(server)
}
_ => None,
},
_ => None,
};
let secret = match clash_config.get(&key_secret) {
Some(value) => match value {
Value::String(val_str) => Some(val_str.clone()),
Value::Bool(val_bool) => Some(val_bool.to_string()),
Value::Number(val_num) => Some(val_num.to_string()),
_ => None,
},
_ => None,
};
ClashInfo {
status: "init".into(),
port,
server,
secret,
}
}
/// save the main window
pub fn set_window(&mut self, win: Option<Window>) {
self.window = win;
}
/// run clash sidecar
pub fn run_sidecar(&mut self) -> Result<()> {
let app_dir = dirs::app_home_dir();
let app_dir = app_dir.as_os_str().to_str().unwrap();
match Command::new_sidecar("clash") {
Ok(cmd) => match cmd.args(["-d", app_dir]).spawn() {
Ok((mut rx, cmd_child)) => {
self.sidecar = Some(cmd_child);
// clash log
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => log::info!("[clash]: {}", line),
CommandEvent::Stderr(err) => log::error!("[clash]: {}", err),
_ => {}
}
}
});
Ok(())
}
Err(err) => bail!(err.to_string()),
},
Err(err) => bail!(err.to_string()),
}
}
/// drop clash sidecar
pub fn drop_sidecar(&mut self) -> Result<()> {
if let Some(sidecar) = self.sidecar.take() {
sidecar.kill()?;
}
Ok(())
}
/// restart clash sidecar
/// should reactivate profile after restart
pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> {
self.update_config();
self.drop_sidecar()?;
self.run_sidecar()?;
self.activate(profiles)?;
self.activate_enhanced(profiles, false, true)
}
/// update the clash info
pub fn update_config(&mut self) {
self.config = Clash::read_config();
self.info = Clash::get_info(&self.config);
}
/// patch update the clash config
pub fn patch_config(
&mut self,
patch: Mapping,
verge: &mut Verge,
profiles: &mut Profiles,
) -> Result<()> {
let mix_port_key = Value::from("mixed-port");
let mut port = None;
for (key, value) in patch.into_iter() {
let value = value.clone();
// check whether the mix_port is changed
if key == mix_port_key {
if value.is_number() {
port = value.as_i64().as_ref().map(|n| n.to_string());
} else {
port = value.as_str().as_ref().map(|s| s.to_string());
}
}
self.config.insert(key.clone(), value);
}
self.save_config()?;
if let Some(port) = port {
self.restart_sidecar(profiles)?;
verge.init_sysproxy(Some(port));
}
Ok(())
}
/// revise the `tun` and `dns` config
fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping {
macro_rules! revise {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
$map.insert(ret_key, Value::from($val));
};
}
// if key not exists then append value
macro_rules! append {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
if !$map.contains_key(&ret_key) {
$map.insert(ret_key, Value::from($val));
}
};
}
// tun config
let tun_val = config.get(&Value::from("tun"));
let mut new_tun = Mapping::new();
if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() {
new_tun = tun_val.as_ref().unwrap().as_mapping().unwrap().clone();
}
revise!(new_tun, "enable", enable);
if enable {
append!(new_tun, "stack", "gvisor");
append!(new_tun, "dns-hijack", vec!["198.18.0.2:53"]);
append!(new_tun, "auto-route", true);
append!(new_tun, "auto-detect-interface", true);
}
revise!(config, "tun", new_tun);
// dns config
let dns_val = config.get(&Value::from("dns"));
let mut new_dns = Mapping::new();
if dns_val.is_some() && dns_val.as_ref().unwrap().is_mapping() {
new_dns = dns_val.as_ref().unwrap().as_mapping().unwrap().clone();
}
// 借鉴cfw的默认配置
revise!(new_dns, "enable", enable);
if enable {
append!(new_dns, "enhanced-mode", "fake-ip");
append!(
new_dns,
"nameserver",
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
);
append!(new_dns, "fallback", vec![] as Vec<&str>);
#[cfg(target_os = "windows")]
append!(
new_dns,
"fake-ip-filter",
vec![
"dns.msftncsi.com",
"www.msftncsi.com",
"www.msftconnecttest.com"
]
);
}
revise!(config, "dns", new_dns);
config
}
/// activate the profile
/// generate a new profile to the temp_dir
/// then put the path to the clash core
fn _activate(info: ClashInfo, config: Mapping, window: Option<Window>) -> Result<()> {
let verge_config = VergeConfig::new();
let tun_enable = verge_config.enable_tun_mode.unwrap_or(false);
let config = Clash::_tun_mode(config, tun_enable);
let temp_path = dirs::profiles_temp_path();
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
tauri::async_runtime::spawn(async move {
let server = info.server.unwrap();
let server = format!("http://{server}/configs");
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse().unwrap());
if let Some(secret) = info.secret.as_ref() {
let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
headers.insert("Authorization", secret);
}
let mut data = HashMap::new();
data.insert("path", temp_path.as_os_str().to_str().unwrap());
// retry 5 times
for _ in 0..5 {
match reqwest::ClientBuilder::new().no_proxy().build() {
Ok(client) => {
let builder = client.put(&server).headers(headers.clone()).json(&data);
match builder.send().await {
Ok(resp) => {
if resp.status() != 204 {
log::error!("failed to activate clash for status \"{}\"", resp.status());
}
// emit the window to update something
if let Some(window) = window {
window.emit("verge://refresh-clash-config", "yes").unwrap();
}
// do not retry
break;
}
Err(err) => log::error!("failed to activate for `{err}`"),
}
}
Err(err) => log::error!("failed to activate for `{err}`"),
}
sleep(Duration::from_millis(500)).await;
}
});
Ok(())
}
/// enhanced profiles mode
/// - (sync) refresh config if enhance chain is null
/// - (async) enhanced config
pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> {
if self.window.is_none() {
bail!("failed to get the main window");
}
let event_name = help::get_uid("e");
let event_name = format!("enhanced-cb-{event_name}");
// generate the payload
let payload = profiles.gen_enhanced(event_name.clone())?;
let info = self.info.clone();
// do not run enhanced
if payload.chain.len() == 0 {
if skip {
return Ok(());
}
let mut config = self.config.clone();
let filter_data = Clash::strict_filter(payload.current);
for (key, value) in filter_data.into_iter() {
config.insert(key, value);
}
return Clash::_activate(info, config, self.window.clone());
}
let window = self.window.clone().unwrap();
let window_move = self.window.clone();
window.once(&event_name, move |event| {
if let Some(result) = event.payload() {
let result: PrfEnhancedResult = serde_json::from_str(result).unwrap();
if let Some(data) = result.data {
let mut config = Clash::read_config();
let filter_data = Clash::loose_filter(data); // loose filter
for (key, value) in filter_data.into_iter() {
config.insert(key, value);
}
log_if_err!(Clash::_activate(info, config, window_move));
log::info!("profile enhanced status {}", result.status);
}
result.error.map(|err| log::error!("{err}"));
}
});
tauri::async_runtime::spawn(async move {
// wait the window setup during resolve app
if delay {
sleep(Duration::from_secs(2)).await;
}
window.emit("script-handler", payload).unwrap();
});
Ok(())
}
/// activate the profile
/// auto activate enhanced profile
pub fn activate(&self, profiles: &Profiles) -> Result<()> {
let data = profiles.gen_activate()?;
let data = Clash::strict_filter(data);
let info = self.info.clone();
let mut config = self.config.clone();
for (key, value) in data.into_iter() {
config.insert(key, value);
}
Clash::_activate(info, config, self.window.clone())
}
/// only 5 default fields available (clash config fields)
/// convert to lowercase
fn strict_filter(config: Mapping) -> Mapping {
// Only the following fields are allowed:
// proxies/proxy-providers/proxy-groups/rule-providers/rules
let valid_keys = vec![
"proxies",
"proxy-providers",
"proxy-groups",
"rules",
"rule-providers",
];
let mut new_config = Mapping::new();
for (key, value) in config.into_iter() {
key.as_str().map(|key_str| {
// change to lowercase
let mut key_str = String::from(key_str);
key_str.make_ascii_lowercase();
// filter
if valid_keys.contains(&&*key_str) {
new_config.insert(Value::String(key_str), value);
}
});
}
new_config
}
/// more clash config fields available
/// convert to lowercase
fn loose_filter(config: Mapping) -> Mapping {
// all of these can not be revised by script or merge
// http/https/socks port should be under control
let not_allow = vec![
"port",
"socks-port",
"mixed-port",
"allow-lan",
"mode",
"external-controller",
"secret",
"log-level",
];
let mut new_config = Mapping::new();
for (key, value) in config.into_iter() {
key.as_str().map(|key_str| {
// change to lowercase
let mut key_str = String::from(key_str);
key_str.make_ascii_lowercase();
// filter
if !not_allow.contains(&&*key_str) {
new_config.insert(Value::String(key_str), value);
}
});
}
new_config
}
}
impl Default for Clash {
fn default() -> Self {
Clash::new()
}
}
impl Drop for Clash {
fn drop(&mut self) {
if let Err(err) = self.drop_sidecar() {
log::error!("{err}");
}
}
}

View File

@@ -0,0 +1,145 @@
use crate::config::Config;
use anyhow::{bail, Result};
use reqwest::header::HeaderMap;
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::collections::HashMap;
/// PUT /configs
/// path 是绝对路径
pub async fn put_configs(path: &str) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs");
let mut data = HashMap::new();
data.insert("path", path);
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.put(&url).headers(headers).json(&data);
let response = builder.send().await?;
match response.status().as_u16() {
204 => Ok(()),
status => {
bail!("failed to put configs with status \"{status}\"")
}
}
}
/// PATCH /configs
pub async fn patch_configs(config: &Mapping) -> Result<()> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/configs");
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client.patch(&url).headers(headers.clone()).json(config);
builder.send().await?;
Ok(())
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct DelayRes {
delay: u64,
}
/// GET /proxies/{name}/delay
/// 获取代理延迟
pub async fn get_proxy_delay(
name: String,
test_url: Option<String>,
timeout: i32,
) -> Result<DelayRes> {
let (url, headers) = clash_client_info()?;
let url = format!("{url}/proxies/{name}/delay");
let default_url = "http://1.1.1.1";
let test_url = test_url
.map(|s| if s.is_empty() { default_url.into() } else { s })
.unwrap_or(default_url.into());
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
let builder = client
.get(&url)
.headers(headers)
.query(&[("timeout", &format!("{timeout}")), ("url", &test_url)]);
let response = builder.send().await?;
Ok(response.json::<DelayRes>().await?)
}
/// 根据clash info获取clash服务地址和请求头
fn clash_client_info() -> Result<(String, HeaderMap)> {
let client = { Config::clash().data().get_client_info() };
let server = format!("http://{}", client.server);
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json".parse()?);
if let Some(secret) = client.secret {
let secret = format!("Bearer {}", secret).parse()?;
headers.insert("Authorization", secret);
}
Ok((server, headers))
}
/// 缩短clash的日志
pub fn parse_log(log: String) -> String {
if log.starts_with("time=") && log.len() > 33 {
return (log[33..]).to_owned();
}
if log.len() > 9 {
return (log[9..]).to_owned();
}
log
}
/// 缩短clash -t的错误输出
/// 仅适配 clash p核 8-26、clash meta 1.13.1
pub fn parse_check_output(log: String) -> String {
let t = log.find("time=");
let m = log.find("msg=");
let mr = log.rfind('"');
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
let e = match log.find("level=error msg=") {
Some(e) => e + 17,
None => m + 5,
};
if mr > m {
return (log[e..mr]).to_owned();
}
}
let l = log.find("error=");
let r = log.find("path=").or(Some(log.len()));
if let (Some(l), Some(r)) = (l, r) {
return (log[(l + 6)..(r - 1)]).to_owned();
}
log
}
#[test]
fn test_parse_check_output() {
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
let str3 = r#"
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
configuration file xxx\n
"#;
let res1 = parse_check_output(str1.into());
let res2 = parse_check_output(str2.into());
let res3 = parse_check_output(str3.into());
println!("res1: {res1}");
println!("res2: {res2}");
println!("res3: {res3}");
assert_eq!(res1, res3);
}

319
src-tauri/src/core/core.rs Normal file
View File

@@ -0,0 +1,319 @@
use super::service;
use super::{clash_api, logger::Logger};
use crate::log_err;
use crate::{config::*, utils::dirs};
use anyhow::{bail, Context, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{fs, io::Write, sync::Arc, time::Duration};
use sysinfo::{Pid, System};
use tauri::api::process::{Command, CommandChild, CommandEvent};
use tokio::time::sleep;
#[derive(Debug)]
pub struct CoreManager {
sidecar: Arc<Mutex<Option<CommandChild>>>,
#[allow(unused)]
use_service_mode: Arc<Mutex<bool>>,
}
impl CoreManager {
pub fn global() -> &'static CoreManager {
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
CORE_MANAGER.get_or_init(|| CoreManager {
sidecar: Arc::new(Mutex::new(None)),
use_service_mode: Arc::new(Mutex::new(false)),
})
}
pub fn init(&self) -> Result<()> {
// kill old clash process
let _ = dirs::clash_pid_path()
.and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
.and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
.map(|pid| {
let mut system = System::new();
system.refresh_all();
if let Some(proc) = system.process(Pid::from_u32(pid)) {
if proc.name().contains("clash") {
log::debug!(target: "app", "kill old clash process");
proc.kill();
}
}
});
tauri::async_runtime::spawn(async {
// 启动clash
log_err!(Self::global().run_core().await);
});
Ok(())
}
/// 检查订阅是否正确
pub fn check_config(&self) -> Result<()> {
let config_path = Config::generate_file(ConfigType::Check)?;
let config_path = dirs::path_to_str(&config_path)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
let app_dir = dirs::app_home_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?;
let output = Command::new_sidecar(clash_core)?
.args(["-t", "-d", app_dir, "-f", config_path])
.output()?;
if !output.status.success() {
let error = clash_api::parse_check_output(output.stdout.clone());
let error = match !error.is_empty() {
true => error,
false => output.stdout.clone(),
};
Logger::global().set_log(output.stdout);
bail!("{error}");
}
Ok(())
}
/// 启动核心
pub async fn run_core(&self) -> Result<()> {
let config_path = Config::generate_file(ConfigType::Run)?;
#[allow(unused_mut)]
let mut should_kill = match self.sidecar.lock().take() {
Some(child) => {
log::debug!(target: "app", "stop the core by sidecar");
let _ = child.kill();
true
}
None => false,
};
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await);
should_kill = true;
}
// 这里得等一会儿
if should_kill {
sleep(Duration::from_millis(500)).await;
}
// 服务模式
let enable = { Config::verge().latest().enable_service_mode };
let enable = enable.unwrap_or(false);
*self.use_service_mode.lock() = enable;
if enable {
// 服务模式启动失败就直接运行sidecar
log::debug!(target: "app", "try to run core in service mode");
match (|| async {
service::check_service().await?;
service::run_core_by_service(&config_path).await
})()
.await
{
Ok(_) => return Ok(()),
Err(err) => {
// 修改这个值免得stop出错
*self.use_service_mode.lock() = false;
log::error!(target: "app", "{err}");
}
}
}
let app_dir = dirs::app_home_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
let is_clash = clash_core == "clash";
let config_path = dirs::path_to_str(&config_path)?;
let args = match clash_core.as_str() {
"clash-meta" => vec!["-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-d", app_dir, "-f", config_path],
_ => vec!["-d", app_dir, "-f", config_path],
};
let cmd = Command::new_sidecar(clash_core)?;
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
// 将pid写入文件中
crate::log_err!((|| {
let pid = cmd_child.pid();
let path = dirs::clash_pid_path()?;
fs::File::create(path)
.context("failed to create the pid file")?
.write(format!("{pid}").as_bytes())
.context("failed to write pid to the file")?;
<Result<()>>::Ok(())
})());
let mut sidecar = self.sidecar.lock();
*sidecar = Some(cmd_child);
drop(sidecar);
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
if is_clash {
let stdout = clash_api::parse_log(line.clone());
log::info!(target: "app", "[clash]: {stdout}");
} else {
log::info!(target: "app", "[clash]: {line}");
};
Logger::global().set_log(line);
}
CommandEvent::Stderr(err) => {
// let stdout = clash_api::parse_log(err.clone());
log::error!(target: "app", "[clash]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Error(err) => {
log::error!(target: "app", "[clash]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Terminated(_) => {
log::info!(target: "app", "clash core terminated");
let _ = CoreManager::global().recover_core();
break;
}
_ => {}
}
}
});
Ok(())
}
/// 重启内核
pub fn recover_core(&'static self) -> Result<()> {
// 服务模式不管
if *self.use_service_mode.lock() {
return Ok(());
}
// 清空原来的sidecar值
if let Some(sidecar) = self.sidecar.lock().take() {
let _ = sidecar.kill();
}
tauri::async_runtime::spawn(async move {
// 6秒之后再查看服务是否正常 (时间随便搞的)
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
sleep(Duration::from_millis(6666)).await;
if self.sidecar.lock().is_none() {
log::info!(target: "app", "recover clash core");
// 重新启动app
if let Err(err) = self.run_core().await {
log::error!(target: "app", "failed to recover clash core");
log::error!(target: "app", "{err}");
let _ = self.recover_core();
}
}
});
Ok(())
}
/// 停止核心运行
pub fn stop_core(&self) -> Result<()> {
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move {
log_err!(service::stop_core_by_service().await);
});
return Ok(());
}
let mut sidecar = self.sidecar.lock();
if let Some(child) = sidecar.take() {
log::debug!(target: "app", "stop the core by sidecar");
let _ = child.kill();
}
Ok(())
}
/// 切换核心
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
const CLASH_CORES: [&str; 2] = ["clash-meta", "clash-meta-alpha"];
if !CLASH_CORES.contains(&clash_core.as_str()) {
bail!("invalid clash core name \"{clash_core}\"");
}
log::debug!(target: "app", "change core to `{clash_core}`");
Config::verge().draft().clash_core = Some(clash_core);
// 更新订阅
Config::generate()?;
self.check_config()?;
// 清掉旧日志
Logger::global().clear_log();
match self.run_core().await {
Ok(_) => {
Config::verge().apply();
Config::runtime().apply();
log_err!(Config::verge().latest().save_file());
Ok(())
}
Err(err) => {
Config::verge().discard();
Config::runtime().discard();
Err(err)
}
}
}
/// 更新proxies那些
/// 如果涉及端口和外部控制则需要重启
pub async fn update_config(&self) -> Result<()> {
log::debug!(target: "app", "try to update clash config");
// 更新订阅
Config::generate()?;
// 检查订阅是否正常
self.check_config()?;
// 更新运行时订阅
let path = Config::generate_file(ConfigType::Run)?;
let path = dirs::path_to_str(&path)?;
// 发送请求 发送5次
for i in 0..5 {
match clash_api::put_configs(path).await {
Ok(_) => break,
Err(err) => {
if i < 4 {
log::info!(target: "app", "{err}");
} else {
bail!(err);
}
}
}
sleep(Duration::from_millis(250)).await;
}
Ok(())
}
}

View File

@@ -0,0 +1,77 @@
use super::tray::Tray;
use crate::log_err;
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::sync::Arc;
use tauri::{AppHandle, Manager, Window};
#[derive(Debug, Default, Clone)]
pub struct Handle {
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
}
impl Handle {
pub fn global() -> &'static Handle {
static HANDLE: OnceCell<Handle> = OnceCell::new();
HANDLE.get_or_init(|| Handle {
app_handle: Arc::new(Mutex::new(None)),
})
}
pub fn init(&self, app_handle: AppHandle) {
*self.app_handle.lock() = Some(app_handle);
}
pub fn get_window(&self) -> Option<Window> {
self.app_handle
.lock()
.as_ref()
.and_then(|a| a.get_window("main"))
}
pub fn refresh_clash() {
if let Some(window) = Self::global().get_window() {
log_err!(window.emit("verge://refresh-clash-config", "yes"));
}
}
pub fn refresh_verge() {
if let Some(window) = Self::global().get_window() {
log_err!(window.emit("verge://refresh-verge-config", "yes"));
}
}
#[allow(unused)]
pub fn refresh_profiles() {
if let Some(window) = Self::global().get_window() {
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
}
}
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
if let Some(window) = Self::global().get_window() {
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
}
}
pub fn update_systray() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
}
Tray::update_systray(app_handle.as_ref().unwrap())?;
Ok(())
}
/// update the system tray state
pub fn update_systray_part() -> Result<()> {
let app_handle = Self::global().app_handle.lock();
if app_handle.is_none() {
bail!("update_systray unhandled error");
}
Tray::update_part(app_handle.as_ref().unwrap())?;
Ok(())
}
}

View File

@@ -0,0 +1,160 @@
use crate::{config::Config, feat, log_err};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use tauri::{AppHandle, GlobalShortcutManager};
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
app_handle: Arc<Mutex<Option<AppHandle>>>,
}
impl Hotkey {
pub fn global() -> &'static Hotkey {
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
HOTKEY.get_or_init(|| Hotkey {
current: Arc::new(Mutex::new(Vec::new())),
app_handle: Arc::new(Mutex::new(None)),
})
}
pub fn init(&self, app_handle: AppHandle) -> Result<()> {
*self.app_handle.lock() = Some(app_handle);
let verge = Config::verge();
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
for hotkey in hotkeys.iter() {
let mut iter = hotkey.split(',');
let func = iter.next();
let key = iter.next();
match (key, func) {
(Some(key), Some(func)) => {
log_err!(self.register(key, func));
}
_ => {
let key = key.unwrap_or("None");
let func = func.unwrap_or("None");
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
}
}
}
*self.current.lock() = hotkeys.clone();
}
Ok(())
}
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
let app_handle = self.app_handle.lock();
if app_handle.is_none() {
bail!("failed to get the hotkey manager");
}
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
}
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let mut manager = self.get_manager()?;
if manager.is_registered(hotkey)? {
manager.unregister(hotkey)?;
}
let f = match func.trim() {
"open_or_close_dashboard" => feat::open_or_close_dashboard,
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => feat::toggle_system_proxy,
"toggle_tun_mode" => feat::toggle_tun_mode,
_ => bail!("invalid function \"{func}\""),
};
manager.register(hotkey, f)?;
log::info!(target: "app", "register hotkey {hotkey} {func}");
Ok(())
}
fn unregister(&self, hotkey: &str) -> Result<()> {
self.get_manager()?.unregister(hotkey)?;
log::info!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
let mut current = self.current.lock();
let old_map = Self::get_map_from_vec(&current);
let new_map = Self::get_map_from_vec(&new_hotkeys);
let (del, add) = Self::get_diff(old_map, new_map);
del.iter().for_each(|key| {
let _ = self.unregister(key);
});
add.iter().for_each(|(key, func)| {
log_err!(self.register(key, func));
});
*current = new_hotkeys;
Ok(())
}
fn get_map_from_vec(hotkeys: &Vec<String>) -> HashMap<&str, &str> {
let mut map = HashMap::new();
hotkeys.iter().for_each(|hotkey| {
let mut iter = hotkey.split(',');
let func = iter.next();
let key = iter.next();
if func.is_some() && key.is_some() {
let func = func.unwrap().trim();
let key = key.unwrap().trim();
map.insert(key, func);
}
});
map
}
fn get_diff<'a>(
old_map: HashMap<&'a str, &'a str>,
new_map: HashMap<&'a str, &'a str>,
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
let mut del_list = vec![];
let mut add_list = vec![];
old_map.iter().for_each(|(&key, func)| {
match new_map.get(key) {
Some(new_func) => {
if new_func != func {
del_list.push(key);
add_list.push((key, *new_func));
}
}
None => del_list.push(key),
};
});
new_map.iter().for_each(|(&key, &func)| {
if old_map.get(key).is_none() {
add_list.push((key, func));
}
});
(del_list, add_list)
}
}
impl Drop for Hotkey {
fn drop(&mut self) {
if let Ok(mut manager) = self.get_manager() {
let _ = manager.unregister_all();
}
}
}

View File

@@ -0,0 +1,36 @@
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::VecDeque, sync::Arc};
const LOGS_QUEUE_LEN: usize = 100;
pub struct Logger {
log_data: Arc<Mutex<VecDeque<String>>>,
}
impl Logger {
pub fn global() -> &'static Logger {
static LOGGER: OnceCell<Logger> = OnceCell::new();
LOGGER.get_or_init(|| Logger {
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
})
}
pub fn get_log(&self) -> VecDeque<String> {
self.log_data.lock().clone()
}
pub fn set_log(&self, text: String) {
let mut logs = self.log_data.lock();
if logs.len() > LOGS_QUEUE_LEN {
logs.pop_front();
}
logs.push_back(text);
}
pub fn clear_log(&self) {
let mut logs = self.log_data.lock();
logs.clear();
}
}

View File

@@ -0,0 +1,47 @@
/// 给clash内核的tun模式授权
#[cfg(any(target_os = "macos", target_os = "linux"))]
pub fn grant_permission(core: String) -> anyhow::Result<()> {
use std::process::Command;
use tauri::utils::platform::current_exe;
let path = current_exe()?.with_file_name(core).canonicalize()?;
let path = path.display().to_string();
log::debug!("grant_permission path: {path}");
#[cfg(target_os = "macos")]
let output = {
let path = path.replace(' ', "\\\\ ");
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
Command::new("osascript")
.args(vec!["-e", &command])
.output()?
};
#[cfg(target_os = "linux")]
let output = {
let path = path.replace(' ', "\\ "); // 避免路径中有空格
let shell = format!("setcap cap_net_bind_service,cap_net_admin,cap_dac_override=+ep {path}");
let sudo = match Command::new("which").arg("pkexec").output() {
Ok(output) => {
if output.stdout.is_empty() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
};
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
};
if output.status.success() {
Ok(())
} else {
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
anyhow::bail!("{stderr}");
}
}

View File

@@ -1,7 +1,13 @@
mod clash; pub mod clash_api;
mod profiles; mod core;
mod verge; pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod manager;
pub mod sysopt;
pub mod timer;
pub mod tray;
pub mod service;
pub mod win_uwp;
pub use self::clash::*; pub use self::core::*;
pub use self::profiles::*;
pub use self::verge::*;

View File

@@ -1,667 +0,0 @@
use crate::utils::{config, dirs, help, tmpl};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::{fs, io::Write};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PrfItem {
pub uid: Option<String>,
/// profile item type
/// enum value: remote | local | script | merge
#[serde(rename = "type")]
pub itype: Option<String>,
/// profile name
pub name: Option<String>,
/// profile description
#[serde(skip_serializing_if = "Option::is_none")]
pub desc: Option<String>,
/// profile file
pub file: Option<String>,
/// source url
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
/// selected infomation
#[serde(skip_serializing_if = "Option::is_none")]
pub selected: Option<Vec<PrfSelected>>,
/// subscription user info
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<PrfExtra>,
/// updated time
pub updated: Option<usize>,
/// some options of the item
#[serde(skip_serializing_if = "Option::is_none")]
pub option: Option<PrfOption>,
/// the file data
#[serde(skip)]
pub file_data: Option<String>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct PrfSelected {
pub name: Option<String>,
pub now: Option<String>,
}
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
pub struct PrfExtra {
pub upload: usize,
pub download: usize,
pub total: usize,
pub expire: usize,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct PrfOption {
/// for `remote` profile's http request
/// see issue #13
#[serde(skip_serializing_if = "Option::is_none")]
pub user_agent: Option<String>,
/// for `remote` profile
#[serde(skip_serializing_if = "Option::is_none")]
pub with_proxy: Option<bool>,
}
impl PrfOption {
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
if one.is_some() && other.is_some() {
let mut one = one.unwrap();
let other = other.unwrap();
if let Some(val) = other.user_agent {
one.user_agent = Some(val);
}
if let Some(val) = other.with_proxy {
one.with_proxy = Some(val);
}
return Some(one);
}
if one.is_none() {
return other;
}
return one;
}
}
impl Default for PrfItem {
fn default() -> Self {
PrfItem {
uid: None,
itype: None,
name: None,
desc: None,
file: None,
url: None,
selected: None,
extra: None,
updated: None,
option: None,
file_data: None,
}
}
}
impl PrfItem {
/// From partial item
/// must contain `itype`
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
if item.itype.is_none() {
bail!("type should not be null");
}
match item.itype.unwrap().as_str() {
"remote" => {
if item.url.is_none() {
bail!("url should not be null");
}
let url = item.url.as_ref().unwrap().as_str();
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, item.option).await
}
"local" => {
let name = item.name.unwrap_or("Local File".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc, file_data)
}
"merge" => {
let name = item.name.unwrap_or("Merge".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_merge(name, desc)
}
"script" => {
let name = item.name.unwrap_or("Script".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
}
typ @ _ => bail!("invalid type \"{typ}\""),
}
}
/// ## Local type
/// create a new item from name/desc
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
let uid = help::get_uid("l");
let file = format!("{uid}.yaml");
Ok(PrfItem {
uid: Some(uid),
itype: Some("local".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
updated: Some(help::get_now()),
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
})
}
/// ## Remote type
/// create a new item from url
pub async fn from_url(
url: &str,
name: Option<String>,
desc: Option<String>,
option: Option<PrfOption>,
) -> Result<PrfItem> {
let with_proxy = match option.as_ref() {
Some(opt) => opt.with_proxy.unwrap_or(false),
None => false,
};
let user_agent = match option.as_ref() {
Some(opt) => opt.user_agent.clone(),
None => None,
};
let mut builder = reqwest::ClientBuilder::new();
if !with_proxy {
builder = builder.no_proxy();
}
if let Some(user_agent) = user_agent {
builder = builder.user_agent(user_agent);
}
let resp = builder.build()?.get(url).send().await?;
let header = resp.headers();
// parse the Subscription Userinfo
let extra = match header.get("Subscription-Userinfo") {
Some(value) => {
let sub_info = value.to_str().unwrap_or("");
Some(PrfExtra {
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
download: help::parse_str(sub_info, "download=").unwrap_or(0),
total: help::parse_str(sub_info, "total=").unwrap_or(0),
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
})
}
None => None,
};
let uid = help::get_uid("r");
let file = format!("{uid}.yaml");
let name = name.unwrap_or(uid.clone());
let data = resp.text_with_charset("utf-8").await?;
Ok(PrfItem {
uid: Some(uid),
itype: Some("remote".into()),
name: Some(name),
desc,
file: Some(file),
url: Some(url.into()),
selected: None,
extra,
option,
updated: Some(help::get_now()),
file_data: Some(data),
})
}
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
Ok(PrfItem {
uid: Some(uid),
itype: Some("merge".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_MERGE.into()),
})
}
/// ## Script type (enhance)
/// create the enhanced item by using javascript(browserjs)
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("s");
let file = format!("{uid}.js"); // js ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("script".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
option: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_SCRIPT.into()),
})
}
/// get the file data
pub fn read_file(&self) -> Result<String> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(file);
fs::read_to_string(path).context("failed to read the file")
}
/// save the file data
pub fn save_file(&self, data: String) -> Result<()> {
if self.file.is_none() {
bail!("could not find the file");
}
let file = self.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(file);
fs::write(path, data.as_bytes()).context("failed to save the file")
}
}
///
/// ## Profiles Config
///
/// Define the `profiles.yaml` schema
///
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct Profiles {
/// same as PrfConfig.current
current: Option<String>,
/// same as PrfConfig.chain
chain: Option<Vec<String>>,
/// profile list
items: Option<Vec<PrfItem>>,
}
macro_rules! patch {
($lv: expr, $rv: expr, $key: tt) => {
if ($rv.$key).is_some() {
$lv.$key = $rv.$key;
}
};
}
impl Profiles {
/// read the config from the file
pub fn read_file() -> Self {
let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
if profiles.items.is_none() {
profiles.items = Some(vec![]);
}
profiles.items.as_mut().map(|items| {
for mut item in items.iter_mut() {
if item.uid.is_none() {
item.uid = Some(help::get_uid("d"));
}
}
});
profiles
}
/// save the config to the file
pub fn save_file(&self) -> Result<()> {
config::save_yaml(
dirs::profiles_path(),
self,
Some("# Profiles Config for Clash Verge\n\n"),
)
}
/// sync the config between file and memory
pub fn sync_file(&mut self) -> Result<()> {
let data = Self::read_file();
if data.current.is_none() && data.items.is_none() {
bail!("failed to read profiles.yaml");
}
self.current = data.current;
self.chain = data.chain;
self.items = data.items;
Ok(())
}
/// get the current uid
pub fn get_current(&self) -> Option<String> {
self.current.clone()
}
/// only change the main to the target id
pub fn put_current(&mut self, uid: String) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
let items = self.items.as_ref().unwrap();
let some_uid = Some(uid.clone());
for each in items.iter() {
if each.uid == some_uid {
self.current = some_uid;
return self.save_file();
}
}
bail!("invalid uid \"{uid}\"");
}
/// just change the `chain`
pub fn put_chain(&mut self, chain: Option<Vec<String>>) {
self.chain = chain;
}
/// find the item by the uid
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
if self.items.is_some() {
let items = self.items.as_ref().unwrap();
let some_uid = Some(uid.clone());
for each in items.iter() {
if each.uid == some_uid {
return Ok(each);
}
}
}
bail!("failed to get the item by \"{}\"", uid);
}
/// append new item
/// if the file_data is some
/// then should save the data to file
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
if item.uid.is_none() {
bail!("the uid should not be null");
}
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
if item.file.is_none() {
bail!("the file should not be null");
}
let file = item.file.clone().unwrap();
let path = dirs::app_profiles_dir().join(&file);
fs::File::create(path)
.context(format!("failed to create file \"{}\"", file))?
.write(file_data.as_bytes())
.context(format!("failed to write to file \"{}\"", file))?;
}
if self.items.is_none() {
self.items = Some(vec![]);
}
self.items.as_mut().map(|items| items.push(item));
self.save_file()
}
/// update the item's value
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
let mut items = self.items.take().unwrap_or(vec![]);
for mut each in items.iter_mut() {
if each.uid == Some(uid.clone()) {
patch!(each, item, itype);
patch!(each, item, name);
patch!(each, item, desc);
patch!(each, item, file);
patch!(each, item, url);
patch!(each, item, selected);
patch!(each, item, extra);
patch!(each, item, updated);
patch!(each, item, option);
self.items = Some(items);
return self.save_file();
}
}
self.items = Some(items);
bail!("failed to found the uid \"{uid}\"")
}
/// be used to update the remote item
/// only patch `updated` `extra` `file_data`
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
if self.items.is_none() {
self.items = Some(vec![]);
}
// find the item
let _ = self.get_item(&uid)?;
self.items.as_mut().map(|items| {
let some_uid = Some(uid.clone());
for mut each in items.iter_mut() {
if each.uid == some_uid {
each.extra = item.extra;
each.updated = item.updated;
// save the file data
// move the field value after save
if let Some(file_data) = item.file_data.take() {
let file = each.file.take();
let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
// the file must exists
each.file = Some(file.clone());
let path = dirs::app_profiles_dir().join(&file);
fs::File::create(path)
.unwrap()
.write(file_data.as_bytes())
.unwrap();
}
break;
}
}
});
self.save_file()
}
/// delete item
/// if delete the current then return true
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
let current = self.current.as_ref().unwrap_or(&uid);
let current = current.clone();
let mut items = self.items.take().unwrap_or(vec![]);
let mut index = None;
// get the index
for i in 0..items.len() {
if items[i].uid == Some(uid.clone()) {
index = Some(i);
break;
}
}
if let Some(index) = index {
items.remove(index).file.map(|file| {
let path = dirs::app_profiles_dir().join(file);
if path.exists() {
let _ = fs::remove_file(path);
}
});
}
// delete the original uid
if current == uid {
self.current = match items.len() > 0 {
true => items[0].uid.clone(),
false => None,
};
}
self.items = Some(items);
self.save_file()?;
Ok(current == uid)
}
/// only generate config mapping
pub fn gen_activate(&self) -> Result<Mapping> {
let config = Mapping::new();
if self.current.is_none() || self.items.is_none() {
return Ok(config);
}
let current = self.current.clone().unwrap();
for item in self.items.as_ref().unwrap().iter() {
if item.uid == Some(current.clone()) {
let file_path = match item.file.clone() {
Some(file) => dirs::app_profiles_dir().join(file),
None => bail!("failed to get the file field"),
};
if !file_path.exists() {
bail!("failed to read the file \"{}\"", file_path.display());
}
return Ok(config::read_yaml::<Mapping>(file_path.clone()));
}
}
bail!("failed to found the uid \"{current}\"");
}
/// gen the enhanced profiles
pub fn gen_enhanced(&self, callback: String) -> Result<PrfEnhanced> {
let current = self.gen_activate()?;
let chain = match self.chain.as_ref() {
Some(chain) => chain
.iter()
.map(|uid| self.get_item(uid))
.filter(|item| item.is_ok())
.map(|item| item.unwrap())
.map(|item| PrfData::from_item(item))
.filter(|o| o.is_some())
.map(|o| o.unwrap())
.collect::<Vec<PrfData>>(),
None => vec![],
};
Ok(PrfEnhanced {
current,
chain,
callback,
})
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PrfEnhanced {
pub current: Mapping,
pub chain: Vec<PrfData>,
pub callback: String,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PrfEnhancedResult {
pub data: Option<Mapping>,
pub status: String,
pub error: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct PrfData {
item: PrfItem,
#[serde(skip_serializing_if = "Option::is_none")]
merge: Option<Mapping>,
#[serde(skip_serializing_if = "Option::is_none")]
script: Option<String>,
}
impl PrfData {
pub fn from_item(item: &PrfItem) -> Option<PrfData> {
match item.itype.as_ref() {
Some(itype) => {
let file = item.file.clone()?;
let path = dirs::app_profiles_dir().join(file);
if !path.exists() {
return None;
}
match itype.as_str() {
"script" => Some(PrfData {
item: item.clone(),
script: Some(fs::read_to_string(path).unwrap_or("".into())),
merge: None,
}),
"merge" => Some(PrfData {
item: item.clone(),
merge: Some(config::read_yaml::<Mapping>(path)),
script: None,
}),
_ => None,
}
}
None => None,
}
}
}

View File

@@ -0,0 +1,299 @@
use crate::config::Config;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep;
// Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211";
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody {
pub core_type: Option<String>,
pub bin_path: String,
pub config_dir: String,
pub log_file: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse {
pub code: u64,
pub msg: String,
pub data: Option<ResponseBody>,
}
/// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
///
#[cfg(target_os = "windows")]
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 install_path = binary_path.with_file_name("install-service.exe");
if !install_path.exists() {
bail!("installer exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
_ => StdCommand::new(install_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
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
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")]
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 uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !uninstall_path.exists() {
bail!("uninstaller exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
);
}
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
pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.get(url)
.send()
.await
.context("failed to connect to the Clash Verge Service")?
.json::<JsonResponse>()
.await
.context("failed to parse the Clash Verge Service response")?;
Ok(response)
}
/// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let status = check_service().await?;
if status.code == 0 {
stop_core_by_service().await?;
sleep(Duration::from_secs(1)).await;
}
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
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 = dirs::path_to_str(&bin_path)?;
let config_dir = dirs::app_home_dir()?;
let config_dir = dirs::path_to_str(&config_dir)?;
let log_path = dirs::service_log_file()?;
let log_path = dirs::path_to_str(&log_path)?;
let config_file = dirs::path_to_str(config_file)?;
let mut map = HashMap::new();
map.insert("core_type", clash_core.as_str());
map.insert("bin_path", bin_path);
map.insert("config_dir", config_dir);
map.insert("config_file", config_file);
map.insert("log_file", log_path);
let url = format!("{SERVICE_URL}/start_clash");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.json(&map)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}
/// stop the clash by service
pub(super) async fn stop_core_by_service() -> Result<()> {
let url = format!("{SERVICE_URL}/stop_clash");
let res = reqwest::ClientBuilder::new()
.no_proxy()
.build()?
.post(url)
.send()
.await?
.json::<JsonResponse>()
.await
.context("failed to connect to the Clash Verge Service")?;
if res.code != 0 {
bail!(res.msg);
}
Ok(())
}

View File

@@ -0,0 +1,295 @@
use crate::{config::Config, log_err};
use anyhow::{anyhow, Result};
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::env::current_exe;
use std::sync::Arc;
use sysproxy::Sysproxy;
use tauri::async_runtime::Mutex as TokioMutex;
pub struct Sysopt {
/// current system proxy setting
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
/// record the original system proxy
/// recover it when exit
old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
/// helps to auto launch the app
auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
/// record whether the guard async is running or not
guard_state: Arc<TokioMutex<bool>>,
}
#[cfg(target_os = "windows")]
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;<local>";
#[cfg(target_os = "linux")]
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
#[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str =
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
impl Sysopt {
pub fn global() -> &'static Sysopt {
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
SYSOPT.get_or_init(|| Sysopt {
cur_sysproxy: Arc::new(Mutex::new(None)),
old_sysproxy: Arc::new(Mutex::new(None)),
auto_launch: Arc::new(Mutex::new(None)),
guard_state: Arc::new(TokioMutex::new(false)),
})
}
/// init the sysproxy
pub fn init_sysproxy(&self) -> Result<()> {
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let (enable, bypass) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.system_proxy_bypass.clone(),
)
};
let current = Sysproxy {
enable,
host: String::from("127.0.0.1"),
port,
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
};
if enable {
let old = Sysproxy::get_system_proxy().ok();
current.set_system_proxy()?;
*self.old_sysproxy.lock() = old;
*self.cur_sysproxy.lock() = Some(current);
}
// run the system proxy guard
self.guard_proxy();
Ok(())
}
/// update the system proxy
pub fn update_sysproxy(&self) -> Result<()> {
let mut cur_sysproxy = self.cur_sysproxy.lock();
let old_sysproxy = self.old_sysproxy.lock();
if cur_sysproxy.is_none() || old_sysproxy.is_none() {
drop(cur_sysproxy);
drop(old_sysproxy);
return self.init_sysproxy();
}
let (enable, bypass) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.system_proxy_bypass.clone(),
)
};
let mut sysproxy = cur_sysproxy.take().unwrap();
sysproxy.enable = enable;
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
sysproxy.port = port;
sysproxy.set_system_proxy()?;
*cur_sysproxy = Some(sysproxy);
Ok(())
}
/// reset the sysproxy
pub fn reset_sysproxy(&self) -> Result<()> {
let mut cur_sysproxy = self.cur_sysproxy.lock();
let mut old_sysproxy = self.old_sysproxy.lock();
let cur_sysproxy = cur_sysproxy.take();
if let Some(mut old) = old_sysproxy.take() {
// 如果原代理和当前代理 端口一致就disable关闭否则就恢复原代理设置
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
if old.enable && port_same {
old.enable = false;
log::info!(target: "app", "reset proxy by disabling the original proxy");
} else {
log::info!(target: "app", "reset proxy to the original proxy");
}
old.set_system_proxy()?;
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
// 没有原代理就按现在的代理设置disable即可
log::info!(target: "app", "reset proxy by disabling the current proxy");
cur.enable = false;
cur.set_system_proxy()?;
} else {
log::info!(target: "app", "reset proxy with no action");
}
Ok(())
}
/// init the auto launch
pub fn init_launch(&self) -> Result<()> {
let app_exe = current_exe()?;
// let app_exe = dunce::canonicalize(app_exe)?;
let app_name = app_exe
.file_stem()
.and_then(|f| f.to_str())
.ok_or(anyhow!("failed to get file stem"))?;
let app_path = app_exe
.as_os_str()
.to_str()
.ok_or(anyhow!("failed to get app_path"))?
.to_string();
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
// use the /Applications/Clash Verge.app path
#[cfg(target_os = "macos")]
let app_path = (|| -> Option<String> {
let path = std::path::PathBuf::from(&app_path);
let path = path.parent()?.parent()?.parent()?;
let extension = path.extension()?.to_str()?;
match extension == "app" {
true => Some(path.as_os_str().to_str()?.to_string()),
false => None,
}
})()
.unwrap_or(app_path);
// fix #403
#[cfg(target_os = "linux")]
let app_path = {
use crate::core::handle::Handle;
use tauri::Manager;
let handle = Handle::global();
match handle.app_handle.lock().as_ref() {
Some(app_handle) => {
let appimage = app_handle.env().appimage;
appimage
.and_then(|p| p.to_str().map(|s| s.to_string()))
.unwrap_or(app_path)
}
None => app_path,
}
};
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(&app_path)
.build()?;
*self.auto_launch.lock() = Some(auto);
Ok(())
}
/// update the startup
pub fn update_launch(&self) -> Result<()> {
let auto_launch = self.auto_launch.lock();
if auto_launch.is_none() {
drop(auto_launch);
return self.init_launch();
}
let enable = { Config::verge().latest().enable_auto_launch };
let enable = enable.unwrap_or(false);
let auto_launch = auto_launch.as_ref().unwrap();
match enable {
true => auto_launch.enable()?,
false => log_err!(auto_launch.disable()), // 忽略关闭的错误
};
Ok(())
}
/// launch a system proxy guard
/// read config from file directly
pub fn guard_proxy(&self) {
use tokio::time::{sleep, Duration};
let guard_state = self.guard_state.clone();
tauri::async_runtime::spawn(async move {
// if it is running, exit
let mut state = guard_state.lock().await;
if *state {
return;
}
*state = true;
drop(state);
// default duration is 10s
let mut wait_secs = 10u64;
loop {
sleep(Duration::from_secs(wait_secs)).await;
let (enable, guard, guard_duration, bypass) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.enable_system_proxy.unwrap_or(false),
verge.enable_proxy_guard.unwrap_or(false),
verge.proxy_guard_duration.unwrap_or(10),
verge.system_proxy_bypass.clone(),
)
};
// stop loop
if !enable || !guard {
break;
}
// update duration
wait_secs = guard_duration;
log::debug!(target: "app", "try to guard the system proxy");
let port = {
Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port())
};
let sysproxy = Sysproxy {
enable: true,
host: "127.0.0.1".into(),
port,
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
};
log_err!(sysproxy.set_system_proxy());
}
let mut state = guard_state.lock().await;
*state = false;
drop(state);
});
}
}

184
src-tauri/src/core/timer.rs Normal file
View File

@@ -0,0 +1,184 @@
use crate::config::Config;
use crate::feat;
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
type TaskID = u64;
pub struct Timer {
/// cron manager
delay_timer: Arc<Mutex<DelayTimer>>,
/// save the current state
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
/// increment id
timer_count: Arc<Mutex<TaskID>>,
}
impl Timer {
pub fn global() -> &'static Timer {
static TIMER: OnceCell<Timer> = OnceCell::new();
TIMER.get_or_init(|| Timer {
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
timer_map: Arc::new(Mutex::new(HashMap::new())),
timer_count: Arc::new(Mutex::new(1)),
})
}
/// restore timer
pub fn init(&self) -> Result<()> {
self.refresh()?;
let cur_timestamp = chrono::Local::now().timestamp();
let timer_map = self.timer_map.lock();
let delay_timer = self.delay_timer.lock();
if let Some(items) = Config::profiles().latest().get_items() {
items
.iter()
.filter_map(|item| {
// mins to seconds
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
let updated = item.updated? as i64;
if interval > 0 && cur_timestamp - updated >= interval {
Some(item)
} else {
None
}
})
.for_each(|item| {
if let Some(uid) = item.uid.as_ref() {
if let Some((task_id, _)) = timer_map.get(uid) {
crate::log_err!(delay_timer.advance_task(*task_id));
}
}
})
}
Ok(())
}
/// Correctly update all cron tasks
pub fn refresh(&self) -> Result<()> {
let diff_map = self.gen_diff();
let mut timer_map = self.timer_map.lock();
let mut delay_timer = self.delay_timer.lock();
for (uid, diff) in diff_map.into_iter() {
match diff {
DiffFlag::Del(tid) => {
let _ = timer_map.remove(&uid);
crate::log_err!(delay_timer.remove_task(tid));
}
DiffFlag::Add(tid, val) => {
let _ = timer_map.insert(uid.clone(), (tid, val));
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
}
DiffFlag::Mod(tid, val) => {
let _ = timer_map.insert(uid.clone(), (tid, val));
crate::log_err!(delay_timer.remove_task(tid));
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
}
}
}
Ok(())
}
/// generate a uid -> update_interval map
fn gen_map(&self) -> HashMap<String, u64> {
let mut new_map = HashMap::new();
if let Some(items) = Config::profiles().latest().get_items() {
for item in items.iter() {
if item.option.is_some() {
let option = item.option.as_ref().unwrap();
let interval = option.update_interval.unwrap_or(0);
if interval > 0 {
new_map.insert(item.uid.clone().unwrap(), interval);
}
}
}
}
new_map
}
/// generate the diff map for refresh
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
let mut diff_map = HashMap::new();
let timer_map = self.timer_map.lock();
let new_map = self.gen_map();
let cur_map = &timer_map;
cur_map.iter().for_each(|(uid, (tid, val))| {
let new_val = new_map.get(uid).unwrap_or(&0);
if *new_val == 0 {
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
} else if new_val != val {
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
}
});
let mut count = self.timer_count.lock();
new_map.iter().for_each(|(uid, val)| {
if cur_map.get(uid).is_none() {
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
*count += 1;
}
});
diff_map
}
/// add a cron task
fn add_task(
&self,
delay_timer: &mut DelayTimer,
uid: String,
tid: TaskID,
minutes: u64,
) -> Result<()> {
let task = TaskBuilder::default()
.set_task_id(tid)
.set_maximum_parallel_runnable_num(1)
.set_frequency_repeated_by_minutes(minutes)
// .set_frequency_repeated_by_seconds(minutes) // for test
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
.context("failed to create timer task")?;
delay_timer
.add_task(task)
.context("failed to add timer task")?;
Ok(())
}
/// the task runner
async fn async_task(uid: String) {
log::info!(target: "app", "running timer task `{uid}`");
crate::log_err!(feat::update_profile(uid, None).await);
}
}
#[derive(Debug)]
enum DiffFlag {
Del(TaskID),
Add(TaskID, u64),
Mod(TaskID, u64),
}

325
src-tauri/src/core/tray.rs Normal file
View File

@@ -0,0 +1,325 @@
use crate::{
cmds,
config::Config,
feat,
utils::{dirs, resolve},
};
use anyhow::Result;
use tauri::{
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu,
};
pub struct Tray {}
impl Tray {
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
let zh = { Config::verge().latest().language == Some("zh".into()) };
let version = app_handle.package_info().version.to_string();
macro_rules! t {
($en: expr, $zh: expr) => {
if zh {
$zh
} else {
$en
}
};
}
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_window",
t!("Dashboard", "打开面板"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"rule_mode",
t!("Rule Mode", "规则模式"),
))
.add_item(CustomMenuItem::new(
"global_mode",
t!("Global Mode", "全局模式"),
))
.add_item(CustomMenuItem::new(
"direct_mode",
t!("Direct Mode", "直连模式"),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new(
"system_proxy",
t!("System Proxy", "系统代理"),
))
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
.add_item(CustomMenuItem::new(
"copy_env",
t!("Copy Env", "复制环境变量"),
))
.add_submenu(SystemTraySubmenu::new(
t!("Open Dir", "打开目录"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"open_app_dir",
t!("App Dir", "应用目录"),
))
.add_item(CustomMenuItem::new(
"open_core_dir",
t!("Core Dir", "内核目录"),
))
.add_item(CustomMenuItem::new(
"open_logs_dir",
t!("Logs Dir", "日志目录"),
)),
))
.add_submenu(SystemTraySubmenu::new(
t!("More", "更多"),
SystemTrayMenu::new()
.add_item(CustomMenuItem::new(
"restart_clash",
t!("Restart Clash", "重启 Clash"),
))
.add_item(CustomMenuItem::new(
"restart_app",
t!("Restart App", "重启应用"),
))
.add_item(
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
),
))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
}
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
app_handle
.tray_handle()
.set_menu(Tray::tray_menu(app_handle))?;
Tray::update_part(app_handle)?;
Ok(())
}
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
let zh = { Config::verge().latest().language == Some("zh".into()) };
let version = app_handle.package_info().version.to_string();
macro_rules! t {
($en: expr, $zh: expr) => {
if zh {
$zh
} else {
$en
}
};
}
let mode = {
Config::clash()
.latest()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let tray = app_handle.tray_handle();
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
let _ = tray.get_item("global_mode").set_selected(mode == "global");
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 = verge.latest();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let mut indication_icon = if *system_proxy {
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
#[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
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
} else {
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon.png").to_vec();
#[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon.png").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
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
};
if *tun_mode {
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
#[cfg(target_os = "macos")]
let mut icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
if *tun_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
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
}
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
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 mut map = std::collections::HashMap::new();
map.insert(true, "on");
map.insert(false, "off");
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!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"),
switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"),
switch_map[tun_mode],
t!("Curent Profile", "当前订阅"),
current_profile_name
));
Ok(())
}
pub fn on_click(app_handle: &AppHandle) {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event = tray_event.unwrap_or("main_window".into());
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"main_window" => resolve::create_window(app_handle),
_ => {}
}
}
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
match event {
#[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() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(app_handle),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env" => feat::copy_clash_env(app_handle),
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
"restart_clash" => feat::restart_clash_core(),
"restart_app" => api::process::restart(&app_handle.env()),
"quit" => cmds::exit_app(app_handle.clone()),
_ => {}
},
_ => {}
}
}
}

View File

@@ -1,354 +0,0 @@
use crate::log_if_err;
use crate::{
core::Clash,
utils::{config, dirs, sysopt::SysProxyConfig},
};
use anyhow::{bail, Result};
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::{async_runtime::Mutex, utils::platform::current_exe};
/// ### `verge.yaml` schema
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct VergeConfig {
// i18n
pub language: Option<String>,
/// `light` or `dark`
pub theme_mode: Option<String>,
/// enable blur mode
/// maybe be able to set the alpha
pub theme_blur: Option<bool>,
/// enable traffic graph default is true
pub traffic_graph: Option<bool>,
/// clash tun mode
pub enable_tun_mode: Option<bool>,
/// can the app auto startup
pub enable_auto_launch: Option<bool>,
/// not show the window on launch
pub enable_silent_start: Option<bool>,
/// set system proxy
pub enable_system_proxy: Option<bool>,
/// enable proxy guard
pub enable_proxy_guard: Option<bool>,
/// set system proxy bypass
pub system_proxy_bypass: Option<String>,
/// proxy guard duration
pub proxy_guard_duration: Option<u64>,
/// theme setting
pub theme_setting: Option<VergeTheme>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct VergeTheme {
pub primary_color: Option<String>,
pub secondary_color: Option<String>,
pub primary_text: Option<String>,
pub secondary_text: Option<String>,
pub info_color: Option<String>,
pub error_color: Option<String>,
pub warning_color: Option<String>,
pub success_color: Option<String>,
pub font_family: Option<String>,
pub css_injection: Option<String>,
}
impl VergeConfig {
pub fn new() -> Self {
config::read_yaml::<VergeConfig>(dirs::verge_path())
}
/// Save Verge App Config
pub fn save_file(&self) -> Result<()> {
config::save_yaml(
dirs::verge_path(),
self,
Some("# The Config for Clash Verge App\n\n"),
)
}
}
/// Verge App abilities
#[derive(Debug)]
pub struct Verge {
/// manage the verge config
pub config: VergeConfig,
/// current system proxy setting
pub cur_sysproxy: Option<SysProxyConfig>,
/// record the original system proxy
/// recover it when exit
old_sysproxy: Option<SysProxyConfig>,
/// helps to auto launch the app
auto_launch: Option<AutoLaunch>,
/// record whether the guard async is running or not
guard_state: Arc<Mutex<bool>>,
}
impl Default for Verge {
fn default() -> Self {
Verge::new()
}
}
impl Verge {
pub fn new() -> Self {
Verge {
config: VergeConfig::new(),
old_sysproxy: None,
cur_sysproxy: None,
auto_launch: None,
guard_state: Arc::new(Mutex::new(false)),
}
}
/// init the sysproxy
pub fn init_sysproxy(&mut self, port: Option<String>) {
if let Some(port) = port {
let enable = self.config.enable_system_proxy.clone().unwrap_or(false);
self.old_sysproxy = match SysProxyConfig::get_sys() {
Ok(proxy) => Some(proxy),
Err(_) => None,
};
let bypass = self.config.system_proxy_bypass.clone();
let sysproxy = SysProxyConfig::new(enable, port, bypass);
if enable {
if sysproxy.set_sys().is_err() {
log::error!("failed to set system proxy");
}
}
self.cur_sysproxy = Some(sysproxy);
}
// launchs the system proxy guard
Verge::guard_proxy(self.guard_state.clone());
}
/// reset the sysproxy
pub fn reset_sysproxy(&mut self) {
if let Some(sysproxy) = self.old_sysproxy.take() {
match sysproxy.set_sys() {
Ok(_) => self.cur_sysproxy = None,
Err(_) => log::error!("failed to reset proxy for"),
}
}
}
/// init the auto launch
pub fn init_launch(&mut self) -> Result<()> {
let app_exe = current_exe().unwrap();
let app_exe = dunce::canonicalize(app_exe).unwrap();
let app_name = app_exe.file_stem().unwrap().to_str().unwrap();
let app_path = app_exe.as_os_str().to_str().unwrap();
// fix issue #26
#[cfg(target_os = "windows")]
let app_path = format!("\"{app_path}\"");
#[cfg(target_os = "windows")]
let app_path = app_path.as_str();
let auto = AutoLaunchBuilder::new()
.set_app_name(app_name)
.set_app_path(app_path)
.build();
if let Some(enable) = self.config.enable_auto_launch.as_ref() {
// fix issue #26
if *enable {
auto.enable()?;
}
}
self.auto_launch = Some(auto);
Ok(())
}
/// update the startup
fn update_launch(&mut self, enable: bool) -> Result<()> {
let conf_enable = self.config.enable_auto_launch.clone().unwrap_or(false);
if enable == conf_enable {
return Ok(());
}
let auto_launch = self.auto_launch.clone().unwrap();
match enable {
true => auto_launch.enable()?,
false => auto_launch.disable()?,
};
Ok(())
}
/// patch verge config
/// There should be only one update at a time here
/// so call the save_file at the end is savely
pub fn patch_config(&mut self, patch: VergeConfig) -> Result<()> {
// only change it
if patch.language.is_some() {
self.config.language = patch.language;
}
if patch.theme_mode.is_some() {
self.config.theme_mode = patch.theme_mode;
}
if patch.theme_blur.is_some() {
self.config.theme_blur = patch.theme_blur;
}
if patch.traffic_graph.is_some() {
self.config.traffic_graph = patch.traffic_graph;
}
if patch.enable_silent_start.is_some() {
self.config.enable_silent_start = patch.enable_silent_start;
}
if patch.theme_setting.is_some() {
self.config.theme_setting = patch.theme_setting;
}
// should update system startup
if patch.enable_auto_launch.is_some() {
let enable = patch.enable_auto_launch.unwrap();
self.update_launch(enable)?;
self.config.enable_auto_launch = Some(enable);
}
// should update system proxy
if patch.enable_system_proxy.is_some() {
let enable = patch.enable_system_proxy.unwrap();
if let Some(mut sysproxy) = self.cur_sysproxy.take() {
sysproxy.enable = enable;
if sysproxy.set_sys().is_err() {
self.cur_sysproxy = Some(sysproxy);
log::error!("failed to set system proxy");
bail!("failed to set system proxy");
}
self.cur_sysproxy = Some(sysproxy);
}
self.config.enable_system_proxy = Some(enable);
}
// should update system proxy too
if patch.system_proxy_bypass.is_some() {
let bypass = patch.system_proxy_bypass.unwrap();
if let Some(mut sysproxy) = self.cur_sysproxy.take() {
if sysproxy.enable {
sysproxy.bypass = bypass.clone();
if sysproxy.set_sys().is_err() {
self.cur_sysproxy = Some(sysproxy);
log::error!("failed to set system proxy");
bail!("failed to set system proxy");
}
}
self.cur_sysproxy = Some(sysproxy);
}
self.config.system_proxy_bypass = Some(bypass);
}
// proxy guard
// only change it
if patch.enable_proxy_guard.is_some() {
self.config.enable_proxy_guard = patch.enable_proxy_guard;
}
if patch.proxy_guard_duration.is_some() {
self.config.proxy_guard_duration = patch.proxy_guard_duration;
}
// relaunch the guard
if patch.enable_system_proxy.is_some() || patch.enable_proxy_guard.is_some() {
Verge::guard_proxy(self.guard_state.clone());
}
// handle the tun mode
if patch.enable_tun_mode.is_some() {
self.config.enable_tun_mode = patch.enable_tun_mode;
}
self.config.save_file()
}
}
impl Verge {
/// launch a system proxy guard
/// read config from file directly
pub fn guard_proxy(guard_state: Arc<Mutex<bool>>) {
use tokio::time::{sleep, Duration};
tauri::async_runtime::spawn(async move {
// if it is running, exit
let mut state = guard_state.lock().await;
if *state {
return;
}
*state = true;
std::mem::drop(state);
// default duration is 10s
let mut wait_secs = 10u64;
loop {
sleep(Duration::from_secs(wait_secs)).await;
log::debug!("guard heartbeat detection");
let verge = Verge::new();
let enable_proxy = verge.config.enable_system_proxy.unwrap_or(false);
let enable_guard = verge.config.enable_proxy_guard.unwrap_or(false);
let guard_duration = verge.config.proxy_guard_duration.unwrap_or(10);
// update duration
wait_secs = guard_duration;
// stop loop
if !enable_guard || !enable_proxy {
break;
}
log::info!("try to guard proxy");
let clash = Clash::new();
match &clash.info.port {
Some(port) => {
let bypass = verge.config.system_proxy_bypass.clone();
let sysproxy = SysProxyConfig::new(true, port.clone(), bypass);
log_if_err!(sysproxy.set_sys());
}
None => log::error!("fail to parse clash port"),
}
}
let mut state = guard_state.lock().await;
*state = false;
});
}
}

View File

@@ -0,0 +1,26 @@
#![cfg(target_os = "windows")]
use crate::utils::dirs;
use anyhow::{bail, Result};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::process::Command as StdCommand;
pub async fn invoke_uwptools() -> Result<()> {
let resource_dir = dirs::app_resources_dir()?;
let tool_path = resource_dir.join("enableLoopback.exe");
if !tool_path.exists() {
bail!("enableLoopback exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?,
_ => StdCommand::new(tool_path).status()?,
};
Ok(())
}

View File

@@ -0,0 +1,6 @@
function main(config) {
if (config.mode === "script") {
config.mode = "rule";
}
return config;
}

View File

@@ -0,0 +1,10 @@
function main(config) {
if (Array.isArray(config.proxies)) {
config.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") {
config.proxies[i].alpn = [p.alpn];
}
});
}
return config;
}

View File

@@ -0,0 +1,101 @@
use crate::{
config::PrfItem,
utils::{dirs, help},
};
use serde_yaml::Mapping;
use std::fs;
#[derive(Debug, Clone)]
pub struct ChainItem {
pub uid: String,
pub data: ChainType,
}
#[derive(Debug, Clone)]
pub enum ChainType {
Merge(Mapping),
Script(String),
}
#[derive(Debug, Clone)]
pub enum ChainSupport {
Clash,
ClashMeta,
ClashMetaAlpha,
All,
}
impl From<&PrfItem> for Option<ChainItem> {
fn from(item: &PrfItem) -> Self {
let itype = item.itype.as_ref()?.as_str();
let file = item.file.clone()?;
let uid = item.uid.clone().unwrap_or("".into());
let path = dirs::app_profiles_dir().ok()?.join(file);
if !path.exists() {
return None;
}
match itype {
"script" => Some(ChainItem {
uid,
data: ChainType::Script(fs::read_to_string(path).ok()?),
}),
"merge" => Some(ChainItem {
uid,
data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
}),
_ => None,
}
}
}
impl ChainItem {
/// 内建支持一些脚本
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
// meta 的一些处理
let meta_guard =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
// meta 的一些处理
let meta_guard_alpha =
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
// meta 1.13.2 alpn string 转 数组
let hy_alpn_alpha =
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
vec![
(ChainSupport::ClashMeta, hy_alpn),
(ChainSupport::ClashMeta, meta_guard),
(ChainSupport::ClashMetaAlpha, hy_alpn_alpha),
(ChainSupport::ClashMetaAlpha, meta_guard_alpha),
]
}
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
Self {
uid: uid.into(),
data: ChainType::Script(data.into()),
}
}
}
impl ChainSupport {
pub fn is_support(&self, core: Option<&String>) -> bool {
match core {
Some(core) => match (self, core.as_str()) {
(ChainSupport::All, _) => true,
(ChainSupport::Clash, "clash") => true,
(ChainSupport::ClashMeta, "clash-meta") => true,
(ChainSupport::ClashMetaAlpha, "clash-meta-alpha") => true,
_ => false,
},
None => true,
}
}
}

View File

@@ -0,0 +1,95 @@
use serde_yaml::{Mapping, Value};
use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 11] = [
"mode",
"redir-port",
"tproxy-port",
"mixed-port",
"socks-port",
"port",
"allow-lan",
"log-level",
"ipv6",
"secret",
"external-controller",
];
pub const DEFAULT_FIELDS: [&str; 5] = [
"proxies",
"proxy-providers",
"proxy-groups",
"rule-providers",
"rules",
];
pub fn use_filter(config: Mapping, filter: &Vec<String>) -> Mapping {
let mut ret = Mapping::new();
for (key, value) in config.into_iter() {
if let Some(key) = key.as_str() {
if filter.contains(&key.to_string()) {
ret.insert(Value::from(key), value);
}
}
}
ret
}
pub fn use_lowercase(config: Mapping) -> Mapping {
let mut ret = Mapping::new();
for (key, value) in config.into_iter() {
if let Some(key_str) = key.as_str() {
let mut key_str = String::from(key_str);
key_str.make_ascii_lowercase();
ret.insert(Value::from(key_str), value);
}
}
ret
}
pub fn use_sort(config: Mapping) -> Mapping {
let mut ret = Mapping::new();
HANDLE_FIELDS.into_iter().for_each(|key| {
let key = Value::from(key);
if let Some(value) = config.get(&key) {
ret.insert(key, value.clone());
}
});
let supported_keys: HashSet<&str> = HANDLE_FIELDS.into_iter().chain(DEFAULT_FIELDS).collect();
let config_keys: HashSet<&str> = config
.keys()
.filter_map(|e| e.as_str())
.into_iter()
.collect();
config_keys.difference(&supported_keys).for_each(|&key| {
let key = Value::from(key);
if let Some(value) = config.get(&key) {
ret.insert(key, value.clone());
}
});
DEFAULT_FIELDS.into_iter().for_each(|key| {
let key = Value::from(key);
if let Some(value) = config.get(&key) {
ret.insert(key, value.clone());
}
});
ret
}
pub fn use_keys(config: &Mapping) -> Vec<String> {
config
.iter()
.filter_map(|(key, _)| key.as_str())
.map(|s| {
let mut s = s.to_string();
s.make_ascii_lowercase();
s
})
.collect()
}

View File

@@ -0,0 +1,132 @@
use super::{use_filter, use_lowercase};
use serde_yaml::{self, Mapping, Sequence, Value};
const MERGE_FIELDS: [&str; 10] = [
"prepend-rules",
"append-rules",
"prepend-rule-providers",
"append-rule-providers",
"prepend-proxies",
"append-proxies",
"prepend-proxy-providers",
"append-proxy-providers",
"prepend-proxy-groups",
"append-proxy-groups",
];
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
// 直接覆盖原字段
use_lowercase(merge.clone())
.into_iter()
.filter(|(key, _)| !MERGE_FIELDS.contains(&key.as_str().unwrap_or_default()))
.for_each(|(key, value)| {
config.insert(key, value);
});
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
let merge = use_filter(merge, &merge_list.collect());
["rule-providers", "proxy-providers"]
.iter()
.for_each(|key_str| {
let key_val = Value::from(key_str.to_string());
let mut map = Mapping::default();
map = config.get(&key_val).map_or(map.clone(), |val| {
val.as_mapping().map_or(map, |v| v.clone())
});
let pre_key = Value::from(format!("prepend-{key_str}"));
let post_key = Value::from(format!("append-{key_str}"));
if let Some(pre_val) = merge.get(&pre_key) {
if pre_val.is_mapping() {
let mut pre_val = pre_val.as_mapping().unwrap().clone();
pre_val.extend(map);
map = pre_val;
}
}
if let Some(post_val) = merge.get(&post_key) {
if post_val.is_mapping() {
map.extend(post_val.as_mapping().unwrap().clone());
}
}
if !map.is_empty() {
config.insert(key_val, Value::from(map));
}
});
["rules", "proxies", "proxy-groups"]
.iter()
.for_each(|key_str| {
let key_val = Value::from(key_str.to_string());
let mut list = Sequence::default();
list = config.get(&key_val).map_or(list.clone(), |val| {
val.as_sequence().map_or(list, |v| v.clone())
});
let pre_key = Value::from(format!("prepend-{key_str}"));
let post_key = Value::from(format!("append-{key_str}"));
if let Some(pre_val) = merge.get(&pre_key) {
if pre_val.is_sequence() {
let mut pre_val = pre_val.as_sequence().unwrap().clone();
pre_val.extend(list);
list = pre_val;
}
}
if let Some(post_val) = merge.get(&post_key) {
if post_val.is_sequence() {
list.extend(post_val.as_sequence().unwrap().clone());
}
}
if !list.is_empty() {
config.insert(key_val, Value::from(list));
}
});
config
}
#[test]
fn test_merge() -> anyhow::Result<()> {
let merge = r"
prepend-rules:
- prepend
- 1123123
append-rules:
- append
prepend-proxies:
- 9999
append-proxies:
- 1111
rules:
- replace
proxy-groups:
- 123781923810
tun:
enable: true
dns:
enable: true
";
let config = r"
rules:
- aaaaa
script1: test
";
let merge = serde_yaml::from_str::<Mapping>(merge)?;
let config = serde_yaml::from_str::<Mapping>(config)?;
let result = serde_yaml::to_string(&use_merge(merge, config))?;
println!("{result}");
Ok(())
}

View File

@@ -0,0 +1,126 @@
mod chain;
pub mod field;
mod merge;
mod script;
mod tun;
use self::chain::*;
use self::field::*;
use self::merge::*;
use self::script::*;
use self::tun::*;
use crate::config::Config;
use serde_yaml::Mapping;
use std::collections::HashMap;
use std::collections::HashSet;
type ResultLog = Vec<(String, String)>;
/// Enhance mode
/// 返回最终订阅、该订阅包含的键、和script执行的结果
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
// config.yaml 的订阅
let clash_config = { Config::clash().latest().0.clone() };
let (clash_core, enable_tun, enable_builtin) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.clash_core.clone(),
verge.enable_tun_mode.unwrap_or(false),
verge.enable_builtin_enhanced.unwrap_or(true),
)
};
// 从profiles里拿东西
let (mut config, chain) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let current = profiles.current_mapping().unwrap_or_default();
let chain = match profiles.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| profiles.get_item(uid).ok())
.filter_map(<Option<ChainItem>>::from)
.collect::<Vec<ChainItem>>(),
None => vec![],
};
(current, chain)
};
let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys
// 处理用户的profile
chain.into_iter().for_each(|item| match item.data {
ChainType::Merge(merge) => {
exists_keys.extend(use_keys(&merge));
config = use_merge(merge, config.to_owned());
}
ChainType::Script(script) => {
let mut logs = vec![];
match use_script(script, config.to_owned()) {
Ok((res_config, res_logs)) => {
exists_keys.extend(use_keys(&res_config));
config = res_config;
logs.extend(res_logs);
}
Err(err) => logs.push(("exception".into(), err.to_string())),
}
result_map.insert(item.uid, logs);
}
});
// 合并默认的config
for (key, value) in clash_config.into_iter() {
if key.as_str() == Some("tun") {
let mut tun = config.get_mut("tun").map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let patch_tun = value.as_mapping().cloned().unwrap_or(Mapping::new());
for (key, value) in patch_tun.into_iter() {
tun.insert(key, value);
}
config.insert("tun".into(), tun.into());
} else {
config.insert(key, value);
}
}
// 内建脚本最后跑
if enable_builtin {
ChainItem::builtin()
.into_iter()
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
.map(|(_, c)| c)
.for_each(|item| {
log::debug!(target: "app", "run builtin script {}", item.uid);
match item.data {
ChainType::Script(script) => match use_script(script, config.to_owned()) {
Ok((res_config, _)) => {
config = res_config;
}
Err(err) => {
log::error!(target: "app", "builtin script error `{err}`");
}
},
_ => {}
}
});
}
config = use_tun(config, enable_tun);
config = use_sort(config);
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys.into_iter());
exists_keys = exists_set.into_iter().collect();
(config, exists_keys, result_map)
}

View File

@@ -0,0 +1,107 @@
use super::use_lowercase;
use anyhow::{Error, Result};
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
use boa_engine::{native_function::NativeFunction, Context, JsValue, Source};
use std::sync::{Arc, Mutex};
let mut context = Context::default();
let outputs = Arc::new(Mutex::new(vec![]));
let copy_outputs = outputs.clone();
unsafe {
let _ = context.register_global_builtin_callable(
"__verge_log__".into(),
2,
NativeFunction::from_closure(
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)?;
let data = data.to_std_string().unwrap();
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))},
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{{
{script};
JSON.stringify(main({config_str})||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
);
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__") {
anyhow::bail!(result[15..].to_owned());
}
if result == "\"\"" {
anyhow::bail!("main function should return object");
}
let res: Result<Mapping, Error> = Ok(serde_json::from_str::<Mapping>(result.as_str())?);
let mut out = outputs.lock().unwrap();
match res {
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
Err(err) => {
out.push(("exception".into(), err.to_string()));
Ok((config, out.to_vec()))
}
}
} else {
anyhow::bail!("main function should return object");
}
}
#[test]
fn test_script() {
let script = r#"
function main(config) {
if (Array.isArray(config.rules)) {
config.rules = [...config.rules, "add"];
}
console.log(config);
config.proxies = ["111"];
return config;
}
"#;
let config = r#"
rules:
- 111
- 222
tun:
enable: false
dns:
enable: false
"#;
let config = serde_yaml::from_str(config).unwrap();
let (config, results) = use_script(script.into(), config).unwrap();
let config_str = serde_yaml::to_string(&config).unwrap();
println!("{config_str}");
dbg!(results);
}

View File

@@ -0,0 +1,127 @@
use serde_yaml::{Mapping, Value};
macro_rules! revise {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
$map.insert(ret_key, Value::from($val));
};
}
// if key not exists then append value
macro_rules! append {
($map: expr, $key: expr, $val: expr) => {
let ret_key = Value::String($key.into());
if !$map.contains_key(&ret_key) {
$map.insert(ret_key, Value::from($val));
}
};
}
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key);
if !enable && tun_val.is_none() {
return config;
}
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
revise!(tun_val, "enable", enable);
revise!(config, "tun", tun_val);
if enable {
#[cfg(target_os = "macos")]
{
use crate::utils::dirs;
use tauri::api::process::Command;
log::info!(target: "app", "try to set system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("set_dns.sh");
let script = script.to_string_lossy();
match Command::new("bash")
.args([script])
.current_dir(resource_dir)
.status()
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "set system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "set system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "set system dns failed: {err}");
}
}
}
use_dns_for_tun(config)
} else {
#[cfg(target_os = "macos")]
{
use crate::utils::dirs;
use tauri::api::process::Command;
log::info!(target: "app", "try to unset system dns");
let resource_dir = dirs::app_resources_dir().unwrap();
let script = resource_dir.join("unset_dns.sh");
let script = script.to_string_lossy();
match Command::new("bash")
.args([script])
.current_dir(resource_dir)
.status()
{
Ok(status) => {
if status.success() {
log::info!(target: "app", "unset system dns successfully");
} else {
let code = status.code().unwrap_or(-1);
log::error!(target: "app", "unset system dns failed: {code}");
}
}
Err(err) => {
log::error!(target: "app", "unset system dns failed: {err}");
}
}
}
config
}
}
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
let dns_key = Value::from("dns");
let dns_val = config.get(&dns_key);
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
// 开启tun将同时开启dns
revise!(dns_val, "enable", true);
append!(dns_val, "enhanced-mode", "fake-ip");
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
append!(
dns_val,
"nameserver",
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
);
append!(dns_val, "fallback", vec![] as Vec<&str>);
#[cfg(target_os = "windows")]
append!(
dns_val,
"fake-ip-filter",
vec![
"dns.msftncsi.com",
"www.msftncsi.com",
"www.msftconnecttest.com"
]
);
revise!(config, "dns", dns_val);
config
}

364
src-tauri/src/feat.rs Normal file
View File

@@ -0,0 +1,364 @@
//
//! feat mod 里的函数主要用于
//! - hotkey 快捷键
//! - timer 定时器
//! - cmds 页面调用
//!
use crate::config::*;
use crate::core::*;
use crate::log_err;
use crate::utils::resolve;
use anyhow::{bail, Result};
use serde_yaml::{Mapping, Value};
use tauri::{AppHandle, ClipboardManager, Manager};
// 打开面板
pub fn open_or_close_dashboard() {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
if let Some(window) = app_handle.get_window("main") {
if let Ok(true) = window.is_focused() {
let _ = window.close();
return;
}
}
resolve::create_window(app_handle);
}
}
// 重启clash
pub fn restart_clash_core() {
tauri::async_runtime::spawn(async {
match CoreManager::global().run_core().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target:"app", "{err}");
}
}
});
}
// 切换模式 rule/global/direct/script mode
pub fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), mode.clone().into());
tauri::async_runtime::spawn(async move {
log::debug!(target: "app", "change clash mode to {mode}");
match clash_api::patch_configs(&mapping).await {
Ok(_) => {
// 更新订阅
Config::clash().data().patch_config(mapping);
if Config::clash().data().save_config().is_ok() {
handle::Handle::refresh_clash();
log_err!(handle::Handle::update_systray_part());
}
}
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
// 切换系统代理
pub fn toggle_system_proxy() {
let enable = Config::verge().draft().enable_system_proxy;
let enable = enable.unwrap_or(false);
tauri::async_runtime::spawn(async move {
match patch_verge(IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
})
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
// 切换tun模式
pub fn toggle_tun_mode() {
let enable = Config::verge().data().enable_tun_mode;
let enable = enable.unwrap_or(false);
tauri::async_runtime::spawn(async move {
match patch_verge(IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
})
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
/// 修改clash的订阅
pub async fn patch_clash(patch: Mapping) -> Result<()> {
Config::clash().draft().patch_config(patch.clone());
match {
let redir_port = patch.get("redir-port");
let tproxy_port = patch.get("tproxy-port");
let mixed_port = patch.get("mixed-port");
let socks_port = patch.get("socks-port");
let port = patch.get("port");
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
if mixed_port.is_some() && !enable_random_port {
let changed = mixed_port.unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用
if changed {
if let Some(port) = mixed_port.unwrap().as_u64() {
if !port_scanner::local_port_available(port as u16) {
Config::clash().discard();
bail!("port already in use");
}
}
}
};
// 激活订阅
if redir_port.is_some()
|| tproxy_port.is_some()
|| mixed_port.is_some()
|| socks_port.is_some()
|| port.is_some()
|| patch.get("secret").is_some()
|| patch.get("external-controller").is_some()
{
Config::generate()?;
CoreManager::global().run_core().await?;
handle::Handle::refresh_clash();
}
// 更新系统代理
if mixed_port.is_some() {
log_err!(sysopt::Sysopt::global().init_sysproxy());
}
if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part());
}
Config::runtime().latest().patch_config(patch);
<Result<()>>::Ok(())
} {
Ok(()) => {
Config::clash().apply();
Config::clash().data().save_config()?;
Ok(())
}
Err(err) => {
Config::clash().discard();
Err(err)
}
}
}
/// 修改verge的订阅
/// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
let auto_launch = patch.enable_auto_launch;
let system_proxy = patch.enable_system_proxy;
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let port = patch.verge_mixed_port;
let common_tray_icon = patch.common_tray_icon;
let sysproxy_tray_icon = patch.sysproxy_tray_icon;
let tun_tray_icon = patch.tun_tray_icon;
match {
let service_mode = patch.enable_service_mode;
if service_mode.is_some() {
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
Config::generate()?;
CoreManager::global().run_core().await?;
} else if tun_mode.is_some() {
update_core_config().await?;
}
if auto_launch.is_some() {
sysopt::Sysopt::global().update_launch()?;
}
if system_proxy.is_some() || proxy_bypass.is_some() || port.is_some() {
sysopt::Sysopt::global().update_sysproxy()?;
sysopt::Sysopt::global().guard_proxy();
}
if let Some(true) = patch.enable_proxy_guard {
sysopt::Sysopt::global().guard_proxy();
}
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys)?;
}
if language.is_some() {
handle::Handle::update_systray()?;
} else if system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
{
handle::Handle::update_systray_part()?;
}
<Result<()>>::Ok(())
} {
Ok(()) => {
Config::verge().apply();
Config::verge().data().save_file()?;
Ok(())
}
Err(err) => {
Config::verge().discard();
Err(err)
}
}
}
/// 更新某个profile
/// 如果更新当前订阅就激活订阅
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
let url_opt = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = profiles.get_item(&uid)?;
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
if !is_remote {
None // 直接更新
} else if item.url.is_none() {
bail!("failed to get the profile item url");
} else {
Some((item.url.clone().unwrap(), item.option.clone()))
}
};
let should_update = match url_opt {
Some((url, opt)) => {
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
Some(uid) == profiles.get_current()
}
None => true,
};
if should_update {
update_core_config().await?;
}
Ok(())
}
/// 更新订阅
async fn update_core_config() -> Result<()> {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
handle::Handle::notice_message("set_config::ok", "ok");
Ok(())
}
Err(err) => {
handle::Handle::notice_message("set_config::error", format!("{err}"));
Err(err)
}
}
}
/// copy env variable
pub fn copy_clash_env(app_handle: &AppHandle) {
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://127.0.0.1:{}", port);
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_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 mut cliboard = app_handle.clipboard_manager();
let env_type = { Config::verge().latest().env_type.clone() };
let env_type = match env_type {
Some(env_type) => env_type,
None => {
#[cfg(not(target_os = "windows"))]
let default = "bash";
#[cfg(target_os = "windows")]
let default = "powershell";
default.to_string()
}
};
match env_type.as_str() {
"bash" => cliboard.write_text(sh).unwrap_or_default(),
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
};
}
pub async fn test_delay(url: String) -> Result<u32> {
use tokio::time::{Duration, Instant};
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
let tun_mode = Config::verge().latest().enable_tun_mode.unwrap_or(false);
let proxy_scheme = format!("http://127.0.0.1:{port}");
if !tun_mode {
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
builder = builder.proxy(proxy);
}
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
builder = builder.proxy(proxy);
}
}
let request = builder
.timeout(Duration::from_millis(10000))
.build()?
.get(url).header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
let start = Instant::now();
let response = request.send().await?;
if response.status().is_success() {
let delay = start.elapsed().as_millis() as u32;
Ok(delay)
} else {
Ok(10000u32)
}
}

View File

@@ -1,163 +1,140 @@
#![cfg_attr( #![cfg_attr(
all(not(debug_assertions), target_os = "windows"), all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
mod cmds; mod cmds;
mod config;
mod core; mod core;
mod states; mod enhance;
mod feat;
mod utils; mod utils;
use crate::{ use crate::utils::{init, resolve, server};
core::VergeConfig, use tauri::{api, SystemTray};
utils::{resolve, server},
};
use tauri::{
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
};
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
if server::check_singleton().is_err() { // 单例检测
println!("app exists"); if server::check_singleton().is_err() {
return Ok(()); println!("app exists");
} return Ok(());
}
let tray_menu = SystemTrayMenu::new() #[cfg(target_os = "linux")]
.add_item(CustomMenuItem::new("open_window", "Show")) std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"));
#[allow(unused_mut)] crate::log_err!(init::init_config());
let mut builder = tauri::Builder::default()
.manage(states::VergeState::default()) #[allow(unused_mut)]
.manage(states::ClashState::default()) let mut builder = tauri::Builder::default()
.manage(states::ProfilesState::default()) .system_tray(SystemTray::new())
.setup(|app| Ok(resolve::resolve_setup(app))) .setup(|app| {
.system_tray(SystemTray::new().with_menu(tray_menu)) resolve::resolve_setup(app);
.on_system_tray_event(move |app_handle, event| match event { Ok(())
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { })
"open_window" => { .on_system_tray_event(core::tray::Tray::on_system_tray_event)
let window = app_handle.get_window("main").unwrap(); .invoke_handler(tauri::generate_handler![
window.unminimize().unwrap(); // common
window.show().unwrap(); cmds::get_sys_proxy,
window.set_focus().unwrap(); cmds::open_app_dir,
cmds::open_logs_dir,
cmds::open_web_url,
cmds::open_core_dir,
cmds::get_portable_flag,
// cmds::kill_sidecar,
cmds::restart_sidecar,
cmds::grant_permission,
// clash
cmds::get_clash_info,
cmds::get_clash_logs,
cmds::patch_clash_config,
cmds::change_clash_core,
cmds::get_runtime_config,
cmds::get_runtime_yaml,
cmds::get_runtime_exists,
cmds::get_runtime_logs,
cmds::uwp::invoke_uwp_tool,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
cmds::test_delay,
cmds::get_app_dir,
cmds::copy_icon_file,
cmds::download_icon_cache,
cmds::open_devtools,
cmds::exit_app,
// cmds::update_hotkeys,
// profile
cmds::get_profiles,
cmds::enhance_profiles,
cmds::patch_profiles_config,
cmds::view_profile,
cmds::patch_profile,
cmds::create_profile,
cmds::import_profile,
cmds::reorder_profile,
cmds::update_profile,
cmds::delete_profile,
cmds::read_profile_file,
cmds::save_profile_file,
// service mode
cmds::service::check_service,
cmds::service::install_service,
cmds::service::uninstall_service,
// clash api
cmds::clash_api_get_proxy_delay
]);
#[cfg(target_os = "macos")]
{
use tauri::{Menu, MenuItem, Submenu};
builder = builder.menu(
Menu::new().add_submenu(Submenu::new(
"Edit",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::CloseWindow)
.add_native_item(MenuItem::Quit),
)),
);
}
let app = builder
.build(tauri::generate_context!())
.expect("error while running tauri application");
app.run(|app_handle, e| match e {
tauri::RunEvent::ExitRequested { api, .. } => {
api.prevent_exit();
} }
"system_proxy" => { tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
let verge_state = app_handle.state::<states::VergeState>(); resolve::resolve_reset();
let mut verge = verge_state.0.lock().unwrap(); api::process::kill_children();
}
let old_value = verge.config.enable_system_proxy.clone().unwrap_or(false); tauri::RunEvent::WindowEvent { label, event, .. } => {
let new_value = !old_value; if label == "main" {
match event {
match verge.patch_config(VergeConfig { tauri::WindowEvent::Destroyed => {
enable_system_proxy: Some(new_value), let _ = resolve::save_window_size_position(app_handle, true);
..VergeConfig::default() }
}) { tauri::WindowEvent::CloseRequested { .. } => {
Ok(_) => { let _ = resolve::save_window_size_position(app_handle, true);
app_handle }
.tray_handle() tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
.get_item(id.as_str()) let _ = resolve::save_window_size_position(app_handle, false);
.set_selected(new_value) }
.unwrap(); _ => {}
}
// update verge config
let window = app_handle.get_window("main").unwrap();
window.emit("verge://refresh-verge-config", "yes").unwrap();
} }
Err(err) => log::error!("{err}"),
}
}
"restart_clash" => {
let clash_state = app_handle.state::<states::ClashState>();
let profiles_state = app_handle.state::<states::ProfilesState>();
let mut clash = clash_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap();
crate::log_if_err!(clash.restart_sidecar(&mut profiles));
}
"quit" => {
resolve::resolve_reset(app_handle);
api::process::kill_children();
std::process::exit(0);
} }
_ => {} _ => {}
},
#[cfg(target_os = "windows")]
SystemTrayEvent::LeftClick { .. } => {
let window = app_handle.get_window("main").unwrap();
window.unminimize().unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
_ => {}
})
.invoke_handler(tauri::generate_handler![
// common
cmds::restart_sidecar,
cmds::get_sys_proxy,
cmds::get_cur_proxy,
cmds::kill_sidecars,
cmds::open_app_dir,
cmds::open_logs_dir,
// clash
cmds::get_clash_info,
cmds::patch_clash_config,
// verge
cmds::get_verge_config,
cmds::patch_verge_config,
// profile
cmds::view_profile,
cmds::patch_profile,
cmds::create_profile,
cmds::import_profile,
cmds::update_profile,
cmds::delete_profile,
cmds::select_profile,
cmds::get_profiles,
cmds::sync_profiles,
cmds::enhance_profiles,
cmds::change_profile_chain,
cmds::read_profile_file,
cmds::save_profile_file
]);
#[cfg(target_os = "macos")]
{
use tauri::{Menu, MenuItem, Submenu};
let submenu_file = Submenu::new(
"File",
Menu::new()
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::SelectAll),
);
builder = builder.menu(Menu::new().add_submenu(submenu_file));
}
builder
.build(tauri::generate_context!())
.expect("error while running tauri application")
.run(|app_handle, e| match e {
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
let app_handle = app_handle.clone();
api.prevent_close();
app_handle.get_window(&label).unwrap().hide().unwrap();
}
_ => {}
},
tauri::RunEvent::ExitRequested { .. } => {
resolve::resolve_reset(app_handle);
api::process::kill_children();
}
_ => {}
}); });
Ok(()) Ok(())
} }

View File

@@ -1,11 +0,0 @@
use crate::core::{Clash, Profiles, Verge};
use std::sync::{Arc, Mutex};
#[derive(Default)]
pub struct ProfilesState(pub Arc<Mutex<Profiles>>);
#[derive(Default)]
pub struct ClashState(pub Arc<Mutex<Clash>>);
#[derive(Default)]
pub struct VergeState(pub Arc<Mutex<Verge>>);

View File

@@ -1,23 +0,0 @@
use anyhow::{Context, Result};
use serde::{de::DeserializeOwned, Serialize};
use std::{fs, path::PathBuf};
/// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
let yaml_str = fs::read_to_string(path).unwrap_or("".into());
serde_yaml::from_str::<T>(&yaml_str).unwrap_or(T::default())
}
/// save the data to the file
/// can set `prefix` string to add some comments
pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
let data_str = serde_yaml::to_string(data)?;
let yaml_str = match prefix {
Some(prefix) => format!("{prefix}{data_str}"),
None => data_str,
};
let path_str = path.as_os_str().to_string_lossy().to_string();
fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
}

View File

@@ -1,61 +1,125 @@
use std::env::temp_dir; use crate::core::handle;
use std::path::{Path, PathBuf}; use anyhow::Result;
use once_cell::sync::OnceCell;
use std::path::PathBuf;
use tauri::{ use tauri::{
api::path::{home_dir, resource_dir}, api::path::{data_dir, resource_dir},
Env, PackageInfo, Env,
}; };
#[cfg(not(feature = "verge-dev"))] #[cfg(not(feature = "verge-dev"))]
static APP_DIR: &str = "clash-verge"; pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
#[cfg(feature = "verge-dev")] #[cfg(feature = "verge-dev")]
static APP_DIR: &str = "clash-verge-dev"; pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
pub static PORTABLE_FLAG: OnceCell<bool> = OnceCell::new();
static CLASH_CONFIG: &str = "config.yaml"; static CLASH_CONFIG: &str = "config.yaml";
static VERGE_CONFIG: &str = "verge.yaml"; static VERGE_CONFIG: &str = "verge.yaml";
static PROFILE_YAML: &str = "profiles.yaml"; static PROFILE_YAML: &str = "profiles.yaml";
static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
/// init portable flag
pub fn init_portable_flag() -> Result<()> {
use tauri::utils::platform::current_exe;
let app_exe = current_exe()?;
if let Some(dir) = app_exe.parent() {
let dir = PathBuf::from(dir).join(".config/PORTABLE");
if dir.exists() {
PORTABLE_FLAG.get_or_init(|| true);
}
}
PORTABLE_FLAG.get_or_init(|| false);
Ok(())
}
/// get the verge app home dir /// get the verge app home dir
pub fn app_home_dir() -> PathBuf { pub fn app_home_dir() -> Result<PathBuf> {
home_dir() use tauri::utils::platform::current_exe;
.unwrap()
.join(Path::new(".config")) let flag = PORTABLE_FLAG.get().unwrap_or(&false);
.join(Path::new(APP_DIR)) if *flag {
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_dir = app_exe
.parent()
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
return Ok(PathBuf::from(app_dir).join(".config").join(APP_ID));
}
Ok(data_dir()
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
.join(APP_ID))
} }
/// get the resources dir /// get the resources dir
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf { pub fn app_resources_dir() -> Result<PathBuf> {
resource_dir(package_info, &Env::default()) let handle = handle::Handle::global();
.unwrap() let app_handle = handle.app_handle.lock();
.join("resources") if let Some(app_handle) = app_handle.as_ref() {
let res_dir = resource_dir(app_handle.package_info(), &Env::default())
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
.join("resources");
return Ok(res_dir);
};
Err(anyhow::anyhow!("failed to get the resource dir"))
} }
/// profiles dir /// profiles dir
pub fn app_profiles_dir() -> PathBuf { pub fn app_profiles_dir() -> Result<PathBuf> {
app_home_dir().join("profiles") Ok(app_home_dir()?.join("profiles"))
} }
/// logs dir /// logs dir
pub fn app_logs_dir() -> PathBuf { pub fn app_logs_dir() -> Result<PathBuf> {
app_home_dir().join("logs") Ok(app_home_dir()?.join("logs"))
} }
pub fn clash_path() -> PathBuf { pub fn clash_path() -> Result<PathBuf> {
app_home_dir().join(CLASH_CONFIG) Ok(app_home_dir()?.join(CLASH_CONFIG))
} }
pub fn verge_path() -> PathBuf { pub fn verge_path() -> Result<PathBuf> {
app_home_dir().join(VERGE_CONFIG) Ok(app_home_dir()?.join(VERGE_CONFIG))
} }
pub fn profiles_path() -> PathBuf { pub fn profiles_path() -> Result<PathBuf> {
app_home_dir().join(PROFILE_YAML) Ok(app_home_dir()?.join(PROFILE_YAML))
} }
pub fn profiles_temp_path() -> PathBuf { pub fn clash_pid_path() -> Result<PathBuf> {
#[cfg(not(feature = "debug-yml"))] Ok(app_home_dir()?.join("clash.pid"))
return temp_dir().join(PROFILE_TEMP); }
#[cfg(feature = "debug-yml")] #[cfg(not(target_os = "windows"))]
return app_home_dir().join(PROFILE_TEMP); pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service"))
}
#[cfg(windows)]
pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service.exe"))
}
pub fn service_log_file() -> Result<PathBuf> {
use chrono::Local;
let log_dir = app_logs_dir()?.join("service");
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file);
let _ = std::fs::create_dir_all(&log_dir);
Ok(log_file)
}
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
let path_str = path
.as_os_str()
.to_str()
.ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
Ok(path_str)
} }

View File

@@ -1,94 +1,188 @@
use anyhow::{anyhow, bail, Context, Result};
use nanoid::nanoid; use nanoid::nanoid;
use std::str::FromStr; use serde::{de::DeserializeOwned, Serialize};
use std::time::{SystemTime, UNIX_EPOCH}; use serde_yaml::{Mapping, Value};
use std::{fs, path::PathBuf, str::FromStr};
use tauri::{
api::shell::{open, Program},
Manager,
};
pub fn get_now() -> usize { /// read data from yaml as struct T
SystemTime::now() pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
.duration_since(UNIX_EPOCH) if !path.exists() {
.unwrap() bail!("file not found \"{}\"", path.display());
.as_secs() as _ }
let yaml_str = fs::read_to_string(path)
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
format!(
"failed to read the file with yaml format \"{}\"",
path.display()
)
})
}
/// read mapping from yaml fix #165
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
let mut val: Value = read_yaml(path)?;
val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
Ok(val
.as_mapping()
.ok_or(anyhow!(
"failed to transform to yaml mapping \"{}\"",
path.display()
))?
.to_owned())
}
/// save the data to the file
/// can set `prefix` string to add some comments
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
let data_str = serde_yaml::to_string(data)?;
let yaml_str = match prefix {
Some(prefix) => format!("{prefix}\n\n{data_str}"),
None => data_str,
};
let path_str = path.as_os_str().to_string_lossy().to_string();
fs::write(path, yaml_str.as_bytes())
.with_context(|| format!("failed to save file \"{path_str}\""))
} }
const ALPHABET: [char; 62] = [ const ALPHABET: [char; 62] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z', 'V', 'W', 'X', 'Y', 'Z',
]; ];
/// generate the uid /// generate the uid
pub fn get_uid(prefix: &str) -> String { pub fn get_uid(prefix: &str) -> String {
let id = nanoid!(11, &ALPHABET); let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}") format!("{prefix}{id}")
} }
/// parse the string /// parse the string
/// xxx=123123; => 123123 /// xxx=123123; => 123123
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> { pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
match target.find(key) { target.split(';').map(str::trim).find_map(|s| {
Some(idx) => { let mut parts = s.splitn(2, '=');
let idx = idx + key.len(); match (parts.next(), parts.next()) {
let value = &target[idx..]; (Some(k), Some(v)) if k == key => v.parse::<T>().ok(),
match match value.split(';').nth(0) { _ => None,
Some(value) => value.trim().parse(), }
None => value.trim().parse(), })
} { }
Ok(r) => Some(r),
Err(_) => None, /// get the last part of the url, if not found, return empty string
} pub fn get_last_part_and_decode(url: &str) -> Option<String> {
} let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
None => None, let segments: Vec<&str> = path.split('/').collect();
} let last_segment = segments.last()?;
Some(
percent_encoding::percent_decode_str(last_segment)
.decode_utf8_lossy()
.to_string(),
)
}
/// open file
/// use vscode by default
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
#[cfg(target_os = "macos")]
let code = "Visual Studio Code";
#[cfg(not(target_os = "macos"))]
let code = "code";
let _ = match Program::from_str(code) {
Ok(code) => open(&app.shell_scope(), path.to_string_lossy(), Some(code)),
Err(err) => {
log::error!(target: "app", "Can't find VScode `{err}`");
// default open
open(&app.shell_scope(), path.to_string_lossy(), None)
}
};
Ok(())
} }
#[macro_export] #[macro_export]
macro_rules! log_if_err { macro_rules! error {
($result: expr) => { ($result: expr) => {
if let Err(err) = $result { log::error!(target: "app", "{}", $result);
log::error!("{err}"); };
}
#[macro_export]
macro_rules! log_err {
($result: expr) => {
if let Err(err) = $result {
log::error!(target: "app", "{err}");
}
};
($result: expr, $err_str: expr) => {
if let Err(_) = $result {
log::error!(target: "app", "{}", $err_str);
}
};
}
#[macro_export]
macro_rules! trace_err {
($result: expr, $err_str: expr) => {
if let Err(err) = $result {
log::trace!(target: "app", "{}, err {}", $err_str, err);
}
} }
};
} }
/// wrap the anyhow error /// wrap the anyhow error
/// transform the error to String /// transform the error to String
#[macro_export] #[macro_export]
macro_rules! wrap_err { macro_rules! wrap_err {
($stat: expr) => { ($stat: expr) => {
match $stat { match $stat {
Ok(a) => Ok(a), Ok(a) => Ok(a),
Err(err) => { Err(err) => {
log::error!("{}", err.to_string()); log::error!(target: "app", "{}", err.to_string());
Err(format!("{}", err.to_string())) Err(format!("{}", err.to_string()))
} }
} }
}; };
} }
/// return the string literal error /// return the string literal error
#[macro_export] #[macro_export]
macro_rules! ret_err { macro_rules! ret_err {
($str: expr) => { ($str: expr) => {
return Err($str.into()) return Err($str.into())
}; };
} }
#[test] #[test]
fn test_parse_value() { fn test_parse_value() {
let test_1 = "upload=111; download=2222; total=3333; expire=444"; let test_1 = "upload=111; download=2222; total=3333; expire=444";
let test_2 = "attachment; filename=Clash.yaml"; let test_2 = "attachment; filename=Clash.yaml";
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111); assert_eq!(parse_str::<usize>(test_1, "upload").unwrap(), 111);
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222); assert_eq!(parse_str::<usize>(test_1, "download").unwrap(), 2222);
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333); assert_eq!(parse_str::<usize>(test_1, "total").unwrap(), 3333);
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444); assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444);
assert_eq!( assert_eq!(
parse_str::<String>(test_2, "filename=").unwrap(), parse_str::<String>(test_2, "filename").unwrap(),
format!("Clash.yaml") format!("Clash.yaml")
); );
assert_eq!(parse_str::<usize>(test_1, "aaa="), None); assert_eq!(parse_str::<usize>(test_1, "aaa"), None);
assert_eq!(parse_str::<usize>(test_1, "upload1="), None); assert_eq!(parse_str::<usize>(test_1, "upload1"), None);
assert_eq!(parse_str::<usize>(test_1, "expire1="), None); assert_eq!(parse_str::<usize>(test_1, "expire1"), None);
assert_eq!(parse_str::<usize>(test_2, "attachment="), None); assert_eq!(parse_str::<usize>(test_2, "attachment"), None);
} }

View File

@@ -1,100 +1,330 @@
use crate::utils::{dirs, tmpl}; use crate::config::*;
use chrono::Local; use crate::utils::{dirs, help};
use anyhow::Result;
use chrono::{Local, TimeZone};
use log::LevelFilter; use log::LevelFilter;
use log4rs::append::console::ConsoleAppender; use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender; use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Root}; use log4rs::config::{Appender, Logger, Root};
use log4rs::encode::pattern::PatternEncoder; use log4rs::encode::pattern::PatternEncoder;
use std::fs; use std::fs::{self, DirEntry};
use std::io::Write;
use std::path::PathBuf; use std::path::PathBuf;
use tauri::PackageInfo; use std::str::FromStr;
use tauri::api::process::Command;
/// initialize this instance's log file /// initialize this instance's log file
fn init_log(log_dir: &PathBuf) { fn init_log() -> Result<()> {
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string(); let log_dir = dirs::app_logs_dir()?;
let log_file = format!("{}.log", local_time); if !log_dir.exists() {
let log_file = log_dir.join(log_file); let _ = fs::create_dir_all(&log_dir);
let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}";
let stdout = ConsoleAppender::builder()
.encoder(Box::new(PatternEncoder::new(time_format)))
.build();
let tofile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(time_format)))
.build(log_file)
.unwrap();
let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile)))
.build(
Root::builder()
.appenders(["stdout", "file"])
.build(LevelFilter::Info),
)
.unwrap();
log4rs::init_config(config).unwrap();
}
/// Initialize all the files from resources
fn init_config(app_dir: &PathBuf) -> std::io::Result<()> {
// target path
let clash_path = app_dir.join("config.yaml");
let verge_path = app_dir.join("verge.yaml");
let profile_path = app_dir.join("profiles.yaml");
if !clash_path.exists() {
fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?;
}
if !verge_path.exists() {
fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?;
}
if !profile_path.exists() {
fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?;
}
Ok(())
}
/// initialize app
pub fn init_app(package_info: &PackageInfo) {
// create app dir
let app_dir = dirs::app_home_dir();
let log_dir = dirs::app_logs_dir();
let profiles_dir = dirs::app_profiles_dir();
let res_dir = dirs::app_resources_dir(package_info);
if !app_dir.exists() {
fs::create_dir_all(&app_dir).unwrap();
}
if !log_dir.exists() {
fs::create_dir_all(&log_dir).unwrap();
}
if !profiles_dir.exists() {
fs::create_dir_all(&profiles_dir).unwrap();
}
init_log(&log_dir);
if let Err(err) = init_config(&app_dir) {
log::error!("{err}");
}
// copy the resource file
let mmdb_path = app_dir.join("Country.mmdb");
let mmdb_tmpl = res_dir.join("Country.mmdb");
if !mmdb_path.exists() && mmdb_tmpl.exists() {
fs::copy(mmdb_tmpl, mmdb_path).unwrap();
}
// copy the wintun.dll
#[cfg(target_os = "windows")]
{
let wintun_path = app_dir.join("wintun.dll");
let wintun_tmpl = res_dir.join("wintun.dll");
if !wintun_path.exists() && wintun_tmpl.exists() {
fs::copy(wintun_tmpl, wintun_path).unwrap();
} }
}
let log_level = Config::verge().data().get_log_level();
if log_level == LevelFilter::Off {
return Ok(());
}
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file);
let log_pattern = match log_level {
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
};
let encode = Box::new(PatternEncoder::new(log_pattern));
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
let mut logger_builder = Logger::builder();
let mut root_builder = Root::builder();
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
#[cfg(feature = "verge-dev")]
{
logger_builder = logger_builder.appenders(["file", "stdout"]);
if log_more {
root_builder = root_builder.appenders(["file", "stdout"]);
} else {
root_builder = root_builder.appenders(["stdout"]);
}
}
#[cfg(not(feature = "verge-dev"))]
{
logger_builder = logger_builder.appenders(["file"]);
if log_more {
root_builder = root_builder.appenders(["file"]);
}
}
let (config, _) = log4rs::config::Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(tofile)))
.logger(logger_builder.additive(false).build("app", log_level))
.build_lossy(root_builder.build(log_level));
log4rs::init_config(config)?;
Ok(())
}
/// 删除log文件
pub fn delete_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
if !log_dir.exists() {
return Ok(());
}
let auto_log_clean = {
let verge = Config::verge();
let verge = verge.data();
verge.auto_log_clean.unwrap_or(0)
};
let day = match auto_log_clean {
1 => 7,
2 => 30,
3 => 90,
_ => return Ok(()),
};
log::debug!(target: "app", "try to delete log files, day: {day}");
// %Y-%m-%d to NaiveDateTime
let parse_time_str = |s: &str| {
let sa: Vec<&str> = s.split('-').collect();
if sa.len() != 4 {
return Err(anyhow::anyhow!("invalid time str"));
}
let year = i32::from_str(sa[0])?;
let month = u32::from_str(sa[1])?;
let day = u32::from_str(sa[2])?;
let time = chrono::NaiveDate::from_ymd_opt(year, month, day)
.ok_or(anyhow::anyhow!("invalid time str"))?
.and_hms_opt(0, 0, 0)
.ok_or(anyhow::anyhow!("invalid time str"))?;
Ok(time)
};
let process_file = |file: DirEntry| -> Result<()> {
let file_name = file.file_name();
let file_name = file_name.to_str().unwrap_or_default();
if file_name.ends_with(".log") {
let now = Local::now();
let created_time = parse_time_str(&file_name[0..file_name.len() - 4])?;
let file_time = Local
.from_local_datetime(&created_time)
.single()
.ok_or(anyhow::anyhow!("invalid local datetime"))?;
let duration = now.signed_duration_since(file_time);
if duration.num_days() > day {
let file_path = file.path();
let _ = fs::remove_file(file_path);
log::info!(target: "app", "delete log file: {file_name}");
}
}
Ok(())
};
for file in fs::read_dir(&log_dir)?.flatten() {
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(())
}
/// Initialize all the config files
/// before tauri setup
pub fn init_config() -> Result<()> {
let _ = dirs::init_portable_flag();
let _ = init_log();
let _ = delete_log();
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir);
}
}));
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {
if !profiles_dir.exists() {
let _ = fs::create_dir_all(&profiles_dir);
}
}));
crate::log_err!(dirs::clash_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Vergeasu"))?;
}
<Result<()>>::Ok(())
}));
crate::log_err!(dirs::verge_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IVerge::template(), Some("# Clash Verge"))?;
}
<Result<()>>::Ok(())
}));
crate::log_err!(dirs::profiles_path().map(|path| {
if !path.exists() {
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Verge"))?;
}
<Result<()>>::Ok(())
}));
Ok(())
}
/// initialize app resources
/// after tauri setup
pub fn init_resources() -> Result<()> {
let app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir()?;
if !app_dir.exists() {
let _ = fs::create_dir_all(&app_dir);
}
if !res_dir.exists() {
let _ = fs::create_dir_all(&res_dir);
}
#[cfg(target_os = "windows")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(not(target_os = "windows"))]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
// copy the resource file
// if the source file is newer than the destination file, copy it over
for file in file_list.iter() {
let src_path = res_dir.join(file);
let dest_path = app_dir.join(file);
let handle_copy = || {
match fs::copy(&src_path, &dest_path) {
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
Err(err) => {
log::error!(target: "app", "failed to copy resources '{file}', {err}")
}
};
};
if src_path.exists() && !dest_path.exists() {
handle_copy();
continue;
}
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
match (src_modified, dest_modified) {
(Ok(src_modified), Ok(dest_modified)) => {
if src_modified > dest_modified {
handle_copy();
} else {
log::debug!(target: "app", "skipping resource copy '{file}'");
}
}
_ => {
log::debug!(target: "app", "failed to get modified '{file}'");
handle_copy();
}
};
}
Ok(())
}
/// initialize url scheme
#[cfg(target_os = "windows")]
pub fn init_scheme() -> Result<()> {
use tauri::utils::platform::current_exe;
use winreg::enums::*;
use winreg::RegKey;
let app_exe = current_exe()?;
let app_exe = dunce::canonicalize(app_exe)?;
let app_exe = app_exe.to_string_lossy().into_owned();
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?;
clash.set_value("", &"Clash Verge")?;
clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?;
let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?;
default_icon.set_value("", &app_exe)?;
let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?;
command.set_value("", &format!("{app_exe} \"%1\""))?;
Ok(())
}
#[cfg(target_os = "linux")]
pub fn init_scheme() -> Result<()> {
let output = std::process::Command::new("xdg-mime")
.arg("default")
.arg("clash-verge.desktop")
.arg("x-scheme-handler/clash")
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"failed to set clash scheme, {}",
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
#[cfg(target_os = "macos")]
pub fn init_scheme() -> Result<()> {
Ok(())
}
pub fn startup_script() -> Result<()> {
let path = {
let verge = Config::verge();
let verge = verge.latest();
verge.startup_script.clone().unwrap_or("".to_string())
};
if !path.is_empty() {
let mut shell = "";
if path.ends_with(".sh") {
shell = "bash";
}
if path.ends_with(".ps1") {
shell = "powershell";
}
if path.ends_with(".bat") {
shell = "powershell";
}
if shell.is_empty() {
return Err(anyhow::anyhow!("unsupported script: {path}"));
}
let current_dir = PathBuf::from(path.clone());
if !current_dir.exists() {
return Err(anyhow::anyhow!("script not found: {path}"));
}
let current_dir = current_dir.parent();
match current_dir {
Some(dir) => {
let _ = Command::new(shell)
.current_dir(dir.to_path_buf())
.args(&[path])
.output()?;
}
None => {
let _ = Command::new(shell).args(&[path]).output()?;
}
}
}
Ok(())
} }

View File

@@ -1,8 +1,7 @@
pub mod config;
pub mod dirs; pub mod dirs;
pub mod help; pub mod help;
pub mod init; pub mod init;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;
pub mod sysopt;
pub mod tmpl; pub mod tmpl;
pub mod unix_helper;

View File

@@ -1,88 +1,266 @@
use super::{init, server}; use crate::config::{IVerge, PrfOption};
use crate::{core::Profiles, log_if_err, states}; use crate::{
config::{Config, PrfItem},
core::*,
utils::init,
utils::server,
};
use crate::{log_err, trace_err};
use anyhow::Result;
use once_cell::sync::OnceCell;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::api::notification;
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new();
pub fn find_unused_port() -> Result<u16> {
match TcpListener::bind("127.0.0.1:0") {
Ok(listener) => {
let port = listener.local_addr()?.port();
Ok(port)
}
Err(_) => {
let port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
log::warn!(target: "app", "use default port: {}", port);
Ok(port)
}
}
}
/// handle something when start app /// handle something when start app
pub fn resolve_setup(app: &App) { pub fn resolve_setup(app: &mut App) {
// setup a simple http server for singleton #[cfg(target_os = "macos")]
server::embed_server(&app.handle()); app.set_activation_policy(tauri::ActivationPolicy::Accessory);
let version = app.package_info().version.to_string();
handle::Handle::global().init(app.app_handle());
VERSION.get_or_init(|| version.clone());
// init app config log_err!(init::init_resources());
init::init_app(app.package_info()); log_err!(init::init_scheme());
log_err!(init::startup_script());
// 处理随机端口
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
// init states let mut port = Config::verge()
let clash_state = app.state::<states::ClashState>(); .latest()
let verge_state = app.state::<states::VergeState>(); .verge_mixed_port
let profiles_state = app.state::<states::ProfilesState>(); .unwrap_or(Config::clash().data().get_mixed_port());
let mut clash = clash_state.0.lock().unwrap(); if enable_random_port {
let mut verge = verge_state.0.lock().unwrap(); port = find_unused_port().unwrap_or(
let mut profiles = profiles_state.0.lock().unwrap(); Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port()),
);
}
log_if_err!(clash.run_sidecar()); Config::verge().data().patch_config(IVerge {
verge_mixed_port: Some(port),
..IVerge::default()
});
let _ = Config::verge().data().save_file();
let mut mapping = Mapping::new();
mapping.insert("mixed-port".into(), port.into());
Config::clash().data().patch_config(mapping);
let _ = Config::clash().data().save_config();
*profiles = Profiles::read_file(); // 启动核心
log::trace!("init config");
log_err!(Config::init_config());
clash.set_window(app.get_window("main")); log::trace!("launch core");
log_if_err!(clash.activate(&profiles)); log_err!(CoreManager::global().init());
log_if_err!(clash.activate_enhanced(&profiles, true, true));
verge.init_sysproxy(clash.info.port.clone()); // setup a simple http server for singleton
log::trace!("launch embed server");
server::embed_server(app.app_handle());
log_if_err!(verge.init_launch()); log::trace!("init system tray");
log_err!(tray::Tray::update_systray(&app.app_handle()));
verge.config.enable_system_proxy.map(|enable| { let silent_start = { Config::verge().data().enable_silent_start };
log_if_err!(app if !silent_start.unwrap_or(false) {
.tray_handle() create_window(&app.app_handle());
.get_item("system_proxy") }
.set_selected(enable));
});
resolve_window(app, verge.config.enable_silent_start.clone()); log_err!(sysopt::Sysopt::global().init_launch());
log_err!(sysopt::Sysopt::global().init_sysproxy());
log_err!(handle::Handle::update_systray_part());
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
log_err!(timer::Timer::global().init());
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
tauri::async_runtime::block_on(async {
resolve_scheme(argvs[1].to_owned()).await;
});
}
} }
/// reset system proxy /// reset system proxy
pub fn resolve_reset(app_handle: &AppHandle) { pub fn resolve_reset() {
let verge_state = app_handle.state::<states::VergeState>(); log_err!(sysopt::Sysopt::global().reset_sysproxy());
let mut verge = verge_state.0.lock().unwrap(); log_err!(CoreManager::global().stop_core());
verge.reset_sysproxy();
} }
/// customize the window theme /// create main window
fn resolve_window(app: &App, hide: Option<bool>) { pub fn create_window(app_handle: &AppHandle) {
let window = app.get_window("main").unwrap(); if let Some(window) = app_handle.get_window("main") {
trace_err!(window.unminimize(), "set win unminimize");
// silent start trace_err!(window.show(), "set win visible");
hide.map(|hide| { trace_err!(window.set_focus(), "set win focus");
if hide { return;
window.hide().unwrap();
} }
});
#[cfg(target_os = "windows")] let mut builder = tauri::window::WindowBuilder::new(
{ app_handle,
use window_shadows::set_shadow; "main".to_string(),
use window_vibrancy::apply_blur; tauri::WindowUrl::App("index.html".into()),
)
.title("Clash Verge")
.visible(false)
.fullscreen(false)
.min_inner_size(600.0, 520.0);
window.set_decorations(false).unwrap(); match Config::verge().latest().window_size_position.clone() {
set_shadow(&window, true).unwrap(); Some(size_pos) if size_pos.len() == 4 => {
apply_blur(&window, None).unwrap(); let size = (size_pos[0], size_pos[1]);
} let pos = (size_pos[2], size_pos[3]);
let w = size.0.clamp(600.0, f64::INFINITY);
let h = size.1.clamp(520.0, f64::INFINITY);
builder = builder.inner_size(w, h).position(pos.0, pos.1);
}
_ => {
#[cfg(target_os = "windows")]
{
builder = builder.inner_size(800.0, 636.0).center();
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
{ {
use tauri::LogicalSize; builder = builder.inner_size(800.0, 642.0).center();
use tauri::Size::Logical; }
window.set_decorations(true).unwrap();
window #[cfg(target_os = "linux")]
.set_size(Logical(LogicalSize { {
width: 800.0, builder = builder.inner_size(800.0, 642.0).center();
height: 610.0, }
})) }
.unwrap(); };
// use tauri_plugin_vibrancy::MacOSVibrancy; #[cfg(target_os = "windows")]
// #[allow(deprecated)] let window = builder
// window.apply_vibrancy(MacOSVibrancy::AppearanceBased); .decorations(false)
} .additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.visible(false)
.build();
#[cfg(target_os = "macos")]
let window = builder
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build();
#[cfg(target_os = "linux")]
let window = builder.decorations(false).transparent(true).build();
match window {
Ok(win) => {
let is_maximized = Config::verge()
.latest()
.window_is_maximized
.unwrap_or(false);
log::trace!("try to calculate the monitor size");
let center = (|| -> Result<bool> {
let mut center = false;
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
let size = monitor.size();
let pos = win.outer_position()?;
if pos.x < -400
|| pos.x > (size.width - 200) as i32
|| pos.y < -200
|| pos.y > (size.height - 200) as i32
{
center = true;
}
Ok(center)
})();
if center.unwrap_or(true) {
trace_err!(win.center(), "set win center");
}
trace_err!(set_shadow(&win, true), "set win shadow");
if is_maximized {
trace_err!(win.maximize(), "set win maximize");
}
}
Err(_) => {
log::error!("failed to create window");
return;
}
}
}
/// save window size and position
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
let verge = Config::verge();
let mut verge = verge.latest();
if save_to_file {
verge.save_file()?;
}
let win = app_handle
.get_window("main")
.ok_or(anyhow::anyhow!("failed to get window"))?;
let scale = win.scale_factor()?;
let size = win.inner_size()?;
let size = size.to_logical::<f64>(scale);
let pos = win.outer_position()?;
let pos = pos.to_logical::<f64>(scale);
let is_maximized = win.is_maximized()?;
verge.window_is_maximized = Some(is_maximized);
if !is_maximized && size.width >= 600.0 && size.height >= 520.0 {
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
}
Ok(())
}
pub async fn resolve_scheme(param: String) {
let url = param
.trim_start_matches("clash://install-config/?url=")
.trim_start_matches("clash://install-config?url=");
let option = PrfOption {
user_agent: None,
with_proxy: Some(true),
self_proxy: None,
danger_accept_invalid_certs: None,
update_interval: None,
};
if let Ok(item) = PrfItem::from_url(url, None, None, Some(option)).await {
if Config::profiles().data().append_item(item).is_ok() {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile success")
.show()
.unwrap();
};
} else {
notification::Notification::new(crate::utils::dirs::APP_ID)
.title("Clash Verge")
.body("Import profile failed")
.show()
.unwrap();
log::error!("failed to parse url: {}", url);
}
} }

View File

@@ -1,41 +1,78 @@
extern crate warp; extern crate warp;
use super::resolve;
use crate::config::IVerge;
use anyhow::{bail, Result};
use port_scanner::local_port_available; use port_scanner::local_port_available;
use tauri::{AppHandle, Manager}; use std::convert::Infallible;
use tauri::AppHandle;
use warp::Filter; use warp::Filter;
#[cfg(not(feature = "verge-dev"))] #[derive(serde::Deserialize, Debug)]
const SERVER_PORT: u16 = 33333; struct QueryParam {
#[cfg(feature = "verge-dev")] param: String,
const SERVER_PORT: u16 = 11233; }
/// check whether there is already exists /// check whether there is already exists
pub fn check_singleton() -> Result<(), ()> { pub fn check_singleton() -> Result<()> {
if !local_port_available(SERVER_PORT) { let port = IVerge::get_singleton_port();
tauri::async_runtime::block_on(async {
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT); if !local_port_available(port) {
reqwest::get(url).await.unwrap(); tauri::async_runtime::block_on(async {
Err(()) let resp = reqwest::get(format!("http://127.0.0.1:{port}/commands/ping"))
}) .await?
} else { .text()
Ok(()) .await?;
}
if &resp == "ok" {
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
let param = argvs[1].as_str();
reqwest::get(format!(
"http://127.0.0.1:{port}/commands/scheme?param={param}"
))
.await?
.text()
.await?;
} else {
reqwest::get(format!("http://127.0.0.1:{port}/commands/visible"))
.await?
.text()
.await?;
}
bail!("app exists");
}
log::error!("failed to setup singleton listen server");
Ok(())
})
} else {
Ok(())
}
} }
/// The embed server only be used to implement singleton process /// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later /// maybe it can be used as pac server later
pub fn embed_server(app: &AppHandle) { pub fn embed_server(app_handle: AppHandle) {
let window = app.get_window("main").unwrap(); let port = IVerge::get_singleton_port();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
let commands = warp::path!("commands" / "visible").map(move || { let ping = warp::path!("commands" / "ping").map(move || "ok");
window.show().unwrap();
window.set_focus().unwrap(); let visible = warp::path!("commands" / "visible").map(move || {
return format!("ok"); resolve::create_window(&app_handle);
"ok"
});
let scheme = warp::path!("commands" / "scheme")
.and(warp::query::<QueryParam>())
.and_then(scheme_handler);
async fn scheme_handler(query: QueryParam) -> Result<impl warp::Reply, Infallible> {
resolve::resolve_scheme(query.param).await;
Ok("ok")
}
let commands = ping.or(visible).or(scheme);
warp::serve(commands).run(([127, 0, 0, 1], port)).await;
}); });
warp::serve(commands)
.bind(([127, 0, 0, 1], SERVER_PORT))
.await;
});
} }

View File

@@ -1,366 +0,0 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[cfg(target_os = "windows")]
static DEFAULT_BYPASS: &str = "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>";
#[cfg(target_os = "linux")]
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
#[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str =
"192.168.0.0/16\n10.0.0.0/8\n172.16.0.0/12\n127.0.0.1\nlocalhost\n*.local\ntimestamp.apple.com\n";
#[cfg(target_os = "macos")]
static MACOS_SERVICE: &str = "Wi-Fi";
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct SysProxyConfig {
pub enable: bool,
pub server: String,
pub bypass: String,
}
impl Default for SysProxyConfig {
fn default() -> Self {
SysProxyConfig {
enable: false,
server: String::from(""),
bypass: String::from(""),
}
}
}
impl SysProxyConfig {
pub fn new(enable: bool, port: String, bypass: Option<String>) -> Self {
SysProxyConfig {
enable,
server: format!("127.0.0.1:{}", port),
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
}
}
}
#[cfg(target_os = "windows")]
impl SysProxyConfig {
/// Get the windows system proxy config
pub fn get_sys() -> Result<Self> {
use winreg::enums::*;
use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let cur_var = hkcu.open_subkey_with_flags(
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
KEY_READ,
)?;
Ok(SysProxyConfig {
enable: cur_var.get_value::<u32, _>("ProxyEnable")? == 1u32,
server: cur_var.get_value("ProxyServer")?,
bypass: cur_var.get_value("ProxyOverride")?,
})
}
/// Set the windows system proxy config
pub fn set_sys(&self) -> Result<()> {
use winreg::enums::*;
use winreg::RegKey;
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let cur_var = hkcu.open_subkey_with_flags(
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
KEY_SET_VALUE,
)?;
let enable: u32 = if self.enable { 1u32 } else { 0u32 };
cur_var.set_value("ProxyEnable", &enable)?;
cur_var.set_value("ProxyServer", &self.server)?;
cur_var.set_value("ProxyOverride", &self.bypass)?;
Ok(())
}
}
#[cfg(target_os = "macos")]
impl SysProxyConfig {
/// Get the macos system proxy config
pub fn get_sys() -> Result<Self> {
use std::process::Command;
let http = macproxy::get_proxy(&["-getwebproxy", MACOS_SERVICE])?;
let https = macproxy::get_proxy(&["-getsecurewebproxy", MACOS_SERVICE])?;
let sock = macproxy::get_proxy(&["-getsocksfirewallproxy", MACOS_SERVICE])?;
let mut enable = false;
let mut server = "".into();
if sock.0 == "Yes" {
enable = true;
server = sock.1;
}
if https.0 == "Yes" {
enable = true;
server = https.1;
}
if http.0 == "Yes" || !enable {
enable = http.0 == "Yes";
server = http.1;
}
let bypass_output = Command::new("networksetup")
.args(["-getproxybypassdomains", MACOS_SERVICE])
.output()?;
// change the format to xxx,xxx
let bypass = std::str::from_utf8(&bypass_output.stdout)
.unwrap_or(DEFAULT_BYPASS)
.to_string()
.split('\n')
.collect::<Vec<_>>()
.join(",");
Ok(SysProxyConfig {
enable,
server,
bypass,
})
}
/// Set the macos system proxy config
pub fn set_sys(&self) -> Result<()> {
use std::process::Command;
let enable = self.enable;
let server = self.server.as_str();
let bypass = self.bypass.clone();
macproxy::set_proxy("-setwebproxy", MACOS_SERVICE, enable, server)?;
macproxy::set_proxy("-setsecurewebproxy", MACOS_SERVICE, enable, server)?;
macproxy::set_proxy("-setsocksfirewallproxy", MACOS_SERVICE, enable, server)?;
let domains = bypass.split(",").collect::<Vec<_>>();
Command::new("networksetup")
.args([["-setproxybypassdomains", MACOS_SERVICE].to_vec(), domains].concat())
.status()?;
Ok(())
}
}
#[cfg(target_os = "macos")]
mod macproxy {
use super::*;
use anyhow::bail;
use std::process::Command;
/// use networksetup
/// get the target proxy config
pub(super) fn get_proxy(args: &[&str; 2]) -> Result<(String, String)> {
let output = Command::new("networksetup").args(args).output()?;
let stdout = std::str::from_utf8(&output.stdout)?;
let enable = parse(stdout, "Enabled:");
let server = parse(stdout, "Server:");
let port = parse(stdout, "Port:");
let server = format!("{server}:{port}");
Ok((enable.into(), server))
}
/// use networksetup
/// set the target proxy config
pub(super) fn set_proxy(
target: &str, // like: -setwebproxy
device: &str,
enable: bool,
server: &str,
) -> Result<()> {
let mut split = server.split(":");
let host = split.next();
let port = split.next();
// can not parse the field
if host.is_none() || port.is_none() {
bail!("failed to parse the server into host:port");
}
let args = vec![target, device, host.unwrap(), port.unwrap()];
Command::new("networksetup").args(&args).status()?;
let target_state = String::from(target) + "state";
let enable = if enable { "on" } else { "off" };
let args = vec![target_state.as_str(), device, enable];
Command::new("networksetup").args(&args).status()?;
Ok(())
}
/// parse the networksetup output
fn parse<'a>(target: &'a str, key: &'a str) -> &'a str {
match target.find(key) {
Some(idx) => {
let idx = idx + key.len();
let value = &target[idx..];
let value = match value.find("\n") {
Some(end) => &value[..end],
None => value,
};
value.trim()
}
None => "",
}
}
#[test]
fn test_get() {
use std::process::Command;
let output = Command::new("networksetup")
.args(["-getwebproxy", "Wi-Fi"])
.output();
let output = output.unwrap();
let stdout = std::str::from_utf8(&output.stdout).unwrap();
let enable = parse(stdout, "Enabled:");
let server = parse(stdout, "Server:");
let port = parse(stdout, "Port:");
println!("enable: {}, server: {}, port: {}", enable, server, port);
dbg!(SysProxyConfig::get_sys().unwrap());
}
#[test]
fn test_set() {
let sysproxy = SysProxyConfig::new(true, "7890".into(), None);
dbg!(sysproxy.set_sys().unwrap());
}
}
///
/// Linux Desktop System Proxy Supports
/// by using `gsettings`
#[cfg(target_os = "linux")]
impl SysProxyConfig {
/// Get the system proxy config [http/https/socks]
pub fn get_sys() -> Result<Self> {
use std::process::Command;
let schema = "org.gnome.system.proxy";
// get enable
let mode = Command::new("gsettings")
.args(["get", schema, "mode"])
.output()?;
let mode = std::str::from_utf8(&mode.stdout)?;
let enable = mode == "manual";
// get bypass
// Todo: parse the ignore-hosts
// ['aaa', 'bbb'] -> aaa,bbb
let ignore = Command::new("gsettings")
.args(["get", schema, "ignore-hosts"])
.output()?;
let ignore = std::str::from_utf8(&ignore.stdout)?;
let bypass = ignore.to_string();
let http = Self::get_proxy("http")?;
let https = Self::get_proxy("https")?;
let socks = Self::get_proxy("socks")?;
let mut server = "".into();
if socks.len() > 0 {
server = socks;
}
if https.len() > 0 {
server = https;
}
if http.len() > 0 {
server = http;
}
Ok(SysProxyConfig {
enable,
server,
bypass,
})
}
/// Get the system proxy config [http/https/socks]
pub fn set_sys(&self) -> Result<()> {
use anyhow::bail;
use std::process::Command;
let enable = self.enable;
let server = self.server.as_str();
let bypass = self.bypass.clone();
let schema = "org.gnome.system.proxy";
if enable {
let mut split = server.split(":");
let host = split.next();
let port = split.next();
if port.is_none() {
bail!("failed to parse the port");
}
let host = format!("'{}'", host.unwrap_or("127.0.0.1"));
let host = host.as_str();
let port = port.unwrap();
let http = format!("{schema}.http");
Command::new("gsettings")
.args(["set", http.as_str(), "host", host])
.status()?;
Command::new("gsettings")
.args(["set", http.as_str(), "port", port])
.status()?;
let https = format!("{schema}.https");
Command::new("gsettings")
.args(["set", https.as_str(), "host", host])
.status()?;
Command::new("gsettings")
.args(["set", https.as_str(), "port", port])
.status()?;
let socks = format!("{schema}.socks");
Command::new("gsettings")
.args(["set", socks.as_str(), "host", host])
.status()?;
Command::new("gsettings")
.args(["set", socks.as_str(), "port", port])
.status()?;
// set bypass
// Todo: parse the ignore-hosts
// aaa,bbb,cccc -> ['aaa', 'bbb', 'ccc']
Command::new("gsettings")
.args(["set", schema, "ignore-hosts", bypass.as_str()]) // todo
.status()?;
}
let mode = if enable { "'manual'" } else { "'none'" };
Command::new("gsettings")
.args(["set", schema, "mode", mode])
.status()?;
Ok(())
}
/// help function
fn get_proxy(typ: &str) -> Result<String> {
use std::process::Command;
let schema = format!("org.gnome.system.proxy.{typ}");
let schema = schema.as_str();
let host = Command::new("gsettings")
.args(["get", schema, "host"])
.output()?;
let host = std::str::from_utf8(&host.stdout)?;
let port = Command::new("gsettings")
.args(["get", schema, "port"])
.output()?;
let port = std::str::from_utf8(&port.stdout)?;
Ok(format!("{host}:{port}"))
}
}

View File

@@ -1,70 +1,43 @@
///! Some config file template //! Some config file template
/// template for clash core `config.yaml`
pub const CLASH_CONFIG: &[u8] = br#"# Default Config For Clash Core
mixed-port: 7890
log-level: info
allow-lan: false
external-controller: 127.0.0.1:9090
mode: rule
secret: ""
"#;
/// template for `profiles.yaml`
pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge
current: ~
items: ~
";
/// template for `verge.yaml`
pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge
language: en
theme_mode: light
theme_blur: false
traffic_graph: true
enable_self_startup: false
enable_system_proxy: false
enable_proxy_guard: false
proxy_guard_duration: 10
system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
";
/// 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-proxies: prepend-rule-providers: {}
prepend-proxy-groups: prepend-proxies: []
append-rules: prepend-proxy-providers: {}
append-proxies: prepend-proxy-groups: []
append-proxy-groups: append-rules: []
append-rule-providers: {}
append-proxies: []
append-proxy-providers: {}
append-proxy-groups: []
"; ";
/// enhanced profile /// enhanced profile
pub const ITEM_SCRIPT: &str = "// Should define the `main` function pub const ITEM_SCRIPT: &str = "// Define main function (script entry)
// The argument to this function is the clash config
// or the result of the previous handler function main(config) {
// so you should return the config after processing return config;
function main(params) {
return params;
} }
"; ";

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",
}
}

View File

@@ -1,62 +1,41 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"package": { "package": {
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "0.0.27" "version": "1.6.1"
}, },
"build": { "build": {
"distDir": "../dist", "distDir": "../dist",
"devPath": "http://localhost:3000/", "devPath": "http://localhost:3000/",
"beforeDevCommand": "yarn run web:dev", "beforeDevCommand": "pnpm run web:dev",
"beforeBuildCommand": "yarn run web:build" "beforeBuildCommand": "pnpm run web:build"
}, },
"tauri": { "tauri": {
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"bundle": { "bundle": {
"active": true, "active": true,
"targets": "all", "identifier": "io.github.clash-verge-rev.clash-verge-rev",
"identifier": "top.gydi.clashverge",
"icon": [ "icon": [
"icons/32x32.png", "icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon-new.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": ["resources"], "resources": ["resources"],
"externalBin": ["sidecar/clash"], "externalBin": ["sidecar/clash-meta", "sidecar/clash-meta-alpha"],
"copyright": "© 2022 zzzgydi All Rights Reserved", "copyright": "© 2022 zzzgydi All Rights Reserved",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "A Clash GUI based on tauri.", "shortDescription": "A Clash Meta GUI based on tauri.",
"longDescription": "A Clash GUI based on tauri.", "longDescription": "A Clash Meta GUI based on tauri."
"deb": {
"depends": [],
"useBootstrapper": false
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
}, },
"updater": { "updater": {
"active": true, "active": true,
"endpoints": [ "endpoints": [
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json", "https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
], ],
"dialog": false, "dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK" "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
}, },
"allowlist": { "allowlist": {
"shell": { "shell": {
@@ -67,23 +46,36 @@
}, },
"process": { "process": {
"all": true "all": true
},
"globalShortcut": {
"all": true
},
"clipboard": {
"all": true
},
"notification": {
"all": true
},
"dialog": {
"all": false,
"open": true
},
"protocol": {
"asset": true,
"assetScope": ["$APPDATA/**", "$RESOURCE/../**"]
},
"path": {
"all": true
},
"fs": {
"exists": true,
"readFile": true,
"scope": ["$APPDATA/**", "$RESOURCE/../**", "**"]
} }
}, },
"windows": [ "windows": [],
{
"title": "Clash Verge",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true,
"minWidth": 600,
"minHeight": 520
}
],
"security": { "security": {
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';" "csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src asset: http: https: data: 'self';"
} }
} }
} }

View File

@@ -0,0 +1,16 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["deb", "appimage", "updater"],
"deb": {
"depends": ["openssl"],
"desktopTemplate": "./template/clash-verge.desktop"
}
}
}
}

View File

@@ -0,0 +1,20 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/mac-tray-icon.png",
"iconAsTemplate": true
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["app", "dmg", "updater"],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "10.15",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
}
}
}
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"targets": ["nsis", "updater"],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": "",
"webviewInstallMode": {
"type": "embedBootstrapper",
"silent": true
},
"nsis": {
"displayLanguageSelector": true,
"installerIcon": "icons/icon.ico",
"languages": ["SimpChinese", "English"],
"license": "../LICENSE",
"installMode": "perMachine",
"template": "./template/installer.nsi"
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
[Desktop Entry]
Categories={{{categories}}}
Comment={{{comment}}}
Exec={{{exec}}} %u
Icon={{{icon}}}
Name={{{name}}}
Terminal=false
Type=Application
MimeType=x-scheme-handler/clash;

View File

@@ -0,0 +1,852 @@
; This file is copied from https://github.com/tauri-apps/tauri/blob/tauri-v1.5/tooling/bundler/src/bundle/windows/templates/installer.nsi
; and edit to fit the needs of the project. the latest tauri 2.x has a different base nsi script.
Unicode true
; Set the compression algorithm. Default is LZMA.
!if "{{compression}}" == ""
SetCompressor /SOLID lzma
!else
SetCompressor /SOLID "{{compression}}"
!endif
!include MUI2.nsh
!include FileFunc.nsh
!include x64.nsh
!include WordFunc.nsh
!include "LogicLib.nsh"
!include "StrFunc.nsh"
!addplugindir "$%AppData%\Local\NSIS\"
${StrCase}
${StrLoc}
!define MANUFACTURER "{{manufacturer}}"
!define PRODUCTNAME "{{product_name}}"
!define VERSION "{{version}}"
!define VERSIONWITHBUILD "{{version_with_build}}"
!define SHORTDESCRIPTION "{{short_description}}"
!define INSTALLMODE "{{install_mode}}"
!define LICENSE "{{license}}"
!define INSTALLERICON "{{installer_icon}}"
!define SIDEBARIMAGE "{{sidebar_image}}"
!define HEADERIMAGE "{{header_image}}"
!define MAINBINARYNAME "{{main_binary_name}}"
!define MAINBINARYSRCPATH "{{main_binary_path}}"
!define BUNDLEID "{{bundle_id}}"
!define COPYRIGHT "{{copyright}}"
!define OUTFILE "{{out_file}}"
!define ARCH "{{arch}}"
!define PLUGINSPATH "{{additional_plugins_path}}"
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}"
!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}"
!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}"
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
!define MANUPRODUCTKEY "Software\${MANUFACTURER}\${PRODUCTNAME}"
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
!define ESTIMATEDSIZE "{{estimated_size}}"
Name "${PRODUCTNAME}"
BrandingText "${COPYRIGHT}"
OutFile "${OUTFILE}"
VIProductVersion "${VERSIONWITHBUILD}"
VIAddVersionKey "ProductName" "${PRODUCTNAME}"
VIAddVersionKey "FileDescription" "${SHORTDESCRIPTION}"
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"
; Plugins path, currently exists for linux only
!if "${PLUGINSPATH}" != ""
!addplugindir "${PLUGINSPATH}"
!endif
!if "${UNINSTALLERSIGNCOMMAND}" != ""
!uninstfinalize '${UNINSTALLERSIGNCOMMAND}'
!endif
; Handle install mode, `perUser`, `perMachine` or `both`
!if "${INSTALLMODE}" == "perMachine"
RequestExecutionLevel highest
!endif
!if "${INSTALLMODE}" == "currentUser"
RequestExecutionLevel user
!endif
!if "${INSTALLMODE}" == "both"
!define MULTIUSER_MUI
!define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}"
!define MULTIUSER_INSTALLMODE_COMMANDLINE
!if "${ARCH}" == "x64"
!define MULTIUSER_USE_PROGRAMFILES64
!else if "${ARCH}" == "arm64"
!define MULTIUSER_USE_PROGRAMFILES64
!endif
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}"
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser"
!define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME
!define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation
!define MULTIUSER_EXECUTIONLEVEL Highest
!include MultiUser.nsh
!endif
; installer icon
!if "${INSTALLERICON}" != ""
!define MUI_ICON "${INSTALLERICON}"
!endif
; installer sidebar image
!if "${SIDEBARIMAGE}" != ""
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
!endif
; installer header image
!if "${HEADERIMAGE}" != ""
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
!endif
; Define registry key to store installer language
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}"
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
; Installer pages, must be ordered as they appear
; 1. Welcome Page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_WELCOME
; 2. License Page (if defined)
!if "${LICENSE}" != ""
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_LICENSE "${LICENSE}"
!endif
; 3. Install mode (if it is set to `both`)
!if "${INSTALLMODE}" == "both"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MULTIUSER_PAGE_INSTALLMODE
!endif
; 4. Custom page to ask user if he wants to reinstall/uninstall
; only if a previous installtion was detected
Var ReinstallPageCheck
Page custom PageReinstall PageLeaveReinstall
Function PageReinstall
; Uninstall previous WiX installation if exists.
;
; A WiX installer stores the isntallation info in registry
; using a UUID and so we have to loop through all keys under
; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`
; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER}
;
; This has a potentional issue that there maybe another installation that matches
; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer,
; however, this should be fine since the user will have to confirm the uninstallation
; and they can chose to abort it if doesn't make sense.
StrCpy $0 0
wix_loop:
EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0
StrCmp $1 "" wix_done ; Exit loop if there is no more keys to loop on
IntOp $0 $0 + 1
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName"
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher"
StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
${StrCase} $R1 $R0 "L"
${StrLoc} $R0 $R1 "msiexec" ">"
StrCmp $R0 0 0 wix_done
StrCpy $R7 "wix"
StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1"
Goto compare_version
wix_done:
; Check if there is an existing installation, if not, abort the reinstall page
ReadRegStr $R0 SHCTX "${UNINSTKEY}" ""
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
${IfThen} "$R0$R1" == "" ${|} Abort ${|}
; Compare this installar version with the existing installation
; and modify the messages presented to the user accordingly
compare_version:
StrCpy $R4 "$(older)"
${If} $R7 == "wix"
ReadRegStr $R0 HKLM "$R6" "DisplayVersion"
${Else}
ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion"
${EndIf}
${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|}
nsis_tauri_utils::SemverCompare "${VERSION}" $R0
Pop $R0
; Reinstalling the same version
${If} $R0 == 0
StrCpy $R1 "$(alreadyInstalledLong)"
StrCpy $R2 "$(addOrReinstall)"
StrCpy $R3 "$(uninstallApp)"
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)"
StrCpy $R5 "2"
; Upgrading
${ElseIf} $R0 == 1
StrCpy $R1 "$(olderOrUnknownVersionInstalled)"
StrCpy $R2 "$(uninstallBeforeInstalling)"
StrCpy $R3 "$(dontUninstall)"
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
StrCpy $R5 "1"
; Downgrading
${ElseIf} $R0 == -1
StrCpy $R1 "$(newerVersionInstalled)"
StrCpy $R2 "$(uninstallBeforeInstalling)"
!if "${ALLOWDOWNGRADES}" == "true"
StrCpy $R3 "$(dontUninstall)"
!else
StrCpy $R3 "$(dontUninstallDowngrade)"
!endif
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
StrCpy $R5 "1"
${Else}
Abort
${EndIf}
Call SkipIfPassive
nsDialogs::Create 1018
Pop $R4
${IfThen} $(^RTL) == 1 ${|} nsDialogs::SetRTL $(^RTL) ${|}
${NSD_CreateLabel} 0 0 100% 24u $R1
Pop $R1
${NSD_CreateRadioButton} 30u 50u -30u 8u $R2
Pop $R2
${NSD_OnClick} $R2 PageReinstallUpdateSelection
${NSD_CreateRadioButton} 30u 70u -30u 8u $R3
Pop $R3
; disable this radio button if downgrading and downgrades are disabled
!if "${ALLOWDOWNGRADES}" == "false"
${IfThen} $R0 == -1 ${|} EnableWindow $R3 0 ${|}
!endif
${NSD_OnClick} $R3 PageReinstallUpdateSelection
; Check the first radio button if this the first time
; we enter this page or if the second button wasn't
; selected the last time we were on this page
${If} $ReinstallPageCheck != 2
SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0
${Else}
SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0
${EndIf}
${NSD_SetFocus} $R2
nsDialogs::Show
FunctionEnd
Function PageReinstallUpdateSelection
${NSD_GetState} $R2 $R1
${If} $R1 == ${BST_CHECKED}
StrCpy $ReinstallPageCheck 1
${Else}
StrCpy $ReinstallPageCheck 2
${EndIf}
FunctionEnd
Function PageLeaveReinstall
${NSD_GetState} $R2 $R1
; $R5 holds whether we are reinstalling the same version or not
; $R5 == "1" -> different versions
; $R5 == "2" -> same version
;
; $R1 holds the radio buttons state. its meaning is dependant on the context
StrCmp $R5 "1" 0 +2 ; Existing install is not the same version?
StrCmp $R1 "1" reinst_uninstall reinst_done ; $R1 == "1", then user chose to uninstall existing version, otherwise skip uninstalling
StrCmp $R1 "1" reinst_done ; Same version? skip uninstalling
reinst_uninstall:
HideWindow
ClearErrors
${If} $R7 == "wix"
ReadRegStr $R1 HKLM "$R6" "UninstallString"
ExecWait '$R1' $0
${Else}
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
ExecWait '$R1 /P _?=$4' $0
${EndIf}
BringToFront
${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code
${If} $0 <> 0
${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe"
${If} $0 = 1 ; User aborted uninstaller?
StrCmp $R5 "2" 0 +2 ; Is the existing install the same version?
Quit ; ...yes, already installed, we are done
Abort
${EndIf}
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
Abort
${Else}
StrCpy $0 $R1 1
${IfThen} $0 == '"' ${|} StrCpy $R1 $R1 -1 1 ${|} ; Strip quotes from UninstallString
Delete $R1
RMDir $INSTDIR
${EndIf}
reinst_done:
FunctionEnd
; 5. Choose install directoy page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_DIRECTORY
; 6. Start menu shortcut page
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
Var AppStartMenuFolder
!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder
; 7. Installation page
!insertmacro MUI_PAGE_INSTFILES
; 8. Finish page
;
; Don't auto jump to finish page after installation page,
; because the installation page has useful info that can be used debug any issues with the installer.
!define MUI_FINISHPAGE_NOAUTOCLOSE
; Use show readme button in the finish page as a button create a desktop shortcut
!define MUI_FINISHPAGE_SHOWREADME
!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)"
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut
; Show run app after installation.
!define MUI_FINISHPAGE_RUN "$INSTDIR\${MAINBINARYNAME}.exe"
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
!insertmacro MUI_PAGE_FINISH
; Uninstaller Pages
; 1. Confirm uninstall page
Var DeleteAppDataCheckbox
Var DeleteAppDataCheckboxState
!define /ifndef WS_EX_LAYOUTRTL 0x00400000
!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow
Function un.ConfirmShow
FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog
${If} $(^RTL) == 1
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE}|${WS_EX_LAYOUTRTL},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 50,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
${Else}
System::Call 'USER32::CreateWindowEx(i${__NSD_CheckBox_EXSTYLE},t"${__NSD_CheckBox_CLASS}",t "$(deleteAppData)",i${__NSD_CheckBox_STYLE},i 0,i 100,i 400, i 25,i$1,i0,i0,i0)i.s'
${EndIf}
Pop $DeleteAppDataCheckbox
SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1
SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1
FunctionEnd
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave
Function un.ConfirmLeave
SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState
FunctionEnd
!insertmacro MUI_UNPAGE_CONFIRM
; 2. Uninstalling Page
!insertmacro MUI_UNPAGE_INSTFILES
;Languages
{{#each languages}}
!insertmacro MUI_LANGUAGE "{{this}}"
{{/each}}
!insertmacro MUI_RESERVEFILE_LANGDLL
{{#each language_files}}
!include "{{this}}"
{{/each}}
!macro SetContext
!if "${INSTALLMODE}" == "currentUser"
SetShellVarContext current
!else if "${INSTALLMODE}" == "perMachine"
SetShellVarContext all
!endif
${If} ${RunningX64}
!if "${ARCH}" == "x64"
SetRegView 64
!else if "${ARCH}" == "arm64"
SetRegView 64
!else
SetRegView 32
!endif
${EndIf}
!macroend
Var PassiveMode
Function .onInit
${GetOptions} $CMDLINE "/P" $PassiveMode
IfErrors +2 0
StrCpy $PassiveMode 1
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
!insertmacro MUI_LANGDLL_DISPLAY
!endif
!insertmacro SetContext
${If} $INSTDIR == ""
; Set default install location
!if "${INSTALLMODE}" == "perMachine"
${If} ${RunningX64}
!if "${ARCH}" == "x64"
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
!else if "${ARCH}" == "arm64"
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
!else
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
!endif
${Else}
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
${EndIf}
!else if "${INSTALLMODE}" == "currentUser"
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
!endif
Call RestorePreviousInstallLocation
${EndIf}
!if "${INSTALLMODE}" == "both"
!insertmacro MULTIUSER_INIT
!endif
FunctionEnd
!macro CheckAllVergeProcesses
; Check if Clash Verge.exe is running
nsis_tauri_utils::FindProcess "Clash Verge.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill Clash Verge.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "Clash Verge.exe"
!else
nsis_tauri_utils::KillProcess "Clash Verge.exe"
!endif
${EndIf}
; Check if clash-verge-service.exe is running
nsis_tauri_utils::FindProcess "clash-verge-service.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-verge-service.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-verge-service.exe"
!else
nsis_tauri_utils::KillProcess "clash-verge-service.exe"
!endif
${EndIf}
; Check if clash-meta-alpha.exe is running
nsis_tauri_utils::FindProcess "clash-meta-alpha.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-meta-alpha.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-meta-alpha.exe"
!else
nsis_tauri_utils::KillProcess "clash-meta-alpha.exe"
!endif
${EndIf}
; Check if clash-meta.exe is running
nsis_tauri_utils::FindProcess "clash-meta.exe"
${If} $R0 != 0
; Kill the process
DetailPrint "Kill clash-meta.exe..."
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "clash-meta.exe"
!else
nsis_tauri_utils::KillProcess "clash-meta.exe"
!endif
${EndIf}
!macroend
!macro StartVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 0
DetailPrint "Restart Clash Verge Service..."
SimpleSC::StartService "clash_verge_service" "" 30
${EndIf}
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
${EndIf}
${EndIf}
!macroend
!macro RemoveVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
Pop $0 ; 0service existsother: service not exists
; Service exists
${If} $0 == 0
Push $0
; Check if the service is running
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
Pop $1 ; returns 1 (service is running) - returns 0 (service is not running)
${If} $0 == 0
Push $0
${If} $1 == 1
DetailPrint "Stop Clash Verge Service..."
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0 ; returns an errorcode (<>0) otherwise success (0)
${If} $0 == 0
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Clash Verge Service Stop Error ($0)"
${EndIf}
${ElseIf} $1 == 0
DetailPrint "Removing Clash Verge Service..."
SimpleSC::RemoveService "clash_verge_service"
${EndIf}
${ElseIf} $0 != 0
Push $0
SimpleSC::GetErrorMessage
Pop $0
MessageBox MB_OK|MB_ICONSTOP "Check Service Status Error ($0)"
${EndIf}
${EndIf}
!macroend
Section EarlyChecks
; Abort silent installer if downgrades is disabled
!if "${ALLOWDOWNGRADES}" == "false"
IfSilent 0 silent_downgrades_done
; If downgrading
${If} $R0 == -1
System::Call 'kernel32::AttachConsole(i -1)i.r0'
${If} $0 != 0
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
FileWrite $0 "$(silentDowngrades)"
${EndIf}
Abort
${EndIf}
silent_downgrades_done:
!endif
SectionEnd
Section WebView2
; Check if Webview2 is already installed and skip this section
${If} ${RunningX64}
ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${Else}
ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${EndIf}
ReadRegStr $5 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
StrCmp $4 "" 0 webview2_done
StrCmp $5 "" 0 webview2_done
; Webview2 install modes
!if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper"
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
DetailPrint "$(webview2Downloading)"
nsis_tauri_utils::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe"
Pop $0
${If} $0 == 0
DetailPrint "$(webview2DownloadSuccess)"
${Else}
DetailPrint "$(webview2DownloadError)"
Abort "$(webview2AbortError)"
${EndIf}
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
Goto install_webview2
!endif
!if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper"
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}"
DetailPrint "$(installingWebview2)"
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
Goto install_webview2
!endif
!if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller"
Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}"
DetailPrint "$(installingWebview2)"
StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
Goto install_webview2
!endif
Goto webview2_done
install_webview2:
DetailPrint "$(installingWebview2)"
; $6 holds the path to the webview2 installer
ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1
${If} $1 == 0
DetailPrint "$(webview2InstallSuccess)"
${Else}
DetailPrint "$(webview2InstallError)"
Abort "$(webview2AbortError)"
${EndIf}
webview2_done:
SectionEnd
!macro CheckIfAppIsRunning
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::FindProcessCurrentUser "${MAINBINARYNAME}.exe"
!else
nsis_tauri_utils::FindProcess "${MAINBINARYNAME}.exe"
!endif
Pop $R0
${If} $R0 = 0
IfSilent kill 0
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL "$(appRunningOkKill)" IDOK kill IDCANCEL cancel ${|}
kill:
!if "${INSTALLMODE}" == "currentUser"
nsis_tauri_utils::KillProcessCurrentUser "${MAINBINARYNAME}.exe"
!else
nsis_tauri_utils::KillProcess "${MAINBINARYNAME}.exe"
!endif
Pop $R0
Sleep 500
${If} $R0 = 0
Goto app_check_done
${Else}
IfSilent silent ui
silent:
System::Call 'kernel32::AttachConsole(i -1)i.r0'
${If} $0 != 0
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
FileWrite $0 "$(appRunning)$\n"
${EndIf}
Abort
ui:
Abort "$(failedToKillApp)"
${EndIf}
cancel:
Abort "$(appRunning)"
${EndIf}
app_check_done:
!macroend
Section Install
SetOutPath $INSTDIR
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; Copy main executable
File "${MAINBINARYSRCPATH}"
; Copy resources
{{#each resources_dirs}}
CreateDirectory "$INSTDIR\\{{this}}"
{{/each}}
{{#each resources}}
File /a "/oname={{this.[1]}}" "{{@key}}"
{{/each}}
; Copy external binaries
{{#each binaries}}
File /a "/oname={{this}}" "{{@key}}"
{{/each}}
!insertmacro StartVergeService
; Create uninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
; Save $INSTDIR in registry for future installations
WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR
!if "${INSTALLMODE}" == "both"
; Save install mode to be selected by default for the next installation such as updating
; or when uninstalling
WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1
!endif
; Registry information for add/remove programs
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}"
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\""
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}"
WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\""
WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1"
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1"
WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "${ESTIMATEDSIZE}"
; Create start menu shortcut (GUI)
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
Call CreateStartMenuShortcut
!insertmacro MUI_STARTMENU_WRITE_END
; Create shortcuts for silent and passive installers, which
; can be disabled by passing `/NS` flag
; GUI installer has buttons for users to control creating them
IfSilent check_ns_flag 0
${IfThen} $PassiveMode == 1 ${|} Goto check_ns_flag ${|}
Goto shortcuts_done
check_ns_flag:
${GetOptions} $CMDLINE "/NS" $R0
IfErrors 0 shortcuts_done
Call CreateDesktopShortcut
Call CreateStartMenuShortcut
shortcuts_done:
; Auto close this page for passive mode
${IfThen} $PassiveMode == 1 ${|} SetAutoClose true ${|}
SectionEnd
Function .onInstSuccess
; Check for `/R` flag only in silent and passive installers because
; GUI installer has a toggle for the user to (re)start the app
IfSilent check_r_flag 0
${IfThen} $PassiveMode == 1 ${|} Goto check_r_flag ${|}
Goto run_done
check_r_flag:
${GetOptions} $CMDLINE "/R" $R0
IfErrors run_done 0
Exec '"$INSTDIR\${MAINBINARYNAME}.exe"'
run_done:
FunctionEnd
Function un.onInit
!insertmacro SetContext
!if "${INSTALLMODE}" == "both"
!insertmacro MULTIUSER_UNINIT
!endif
!insertmacro MUI_UNGETLANGUAGE
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
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; Delete the app directory and its content from disk
; Copy main executable
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
; Delete resources
{{#each resources}}
Delete "$INSTDIR\\{{this.[1]}}"
{{/each}}
Delete "$INSTDIR\resources"
; Delete external binaries
{{#each binaries}}
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"
; Remove InstallDir
Push "$INSTDIR"
Call un.isDirectoryEmpty
Pop $0
${If} $0 == 1
RMDir /R /REBOOTOK "$INSTDIR"
${Else}
MessageBox MB_OK "Install Directory is not Empty, Please remove it manually."
${EndIf}
; Remove start menu shortcut
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
Delete "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk"
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
; Remove desktop shortcuts
Delete "$DESKTOP\${MAINBINARYNAME}.lnk"
; Remove registry information for add/remove programs
!if "${INSTALLMODE}" == "both"
DeleteRegKey SHCTX "${UNINSTKEY}"
!else if "${INSTALLMODE}" == "perMachine"
DeleteRegKey HKLM "${UNINSTKEY}"
!else
DeleteRegKey HKCU "${UNINSTKEY}"
!endif
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
; Delete app data
${If} $DeleteAppDataCheckboxState == 1
SetShellVarContext current
RmDir /r "$APPDATA\${BUNDLEID}"
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
${EndIf}
${GetOptions} $CMDLINE "/P" $R0
IfErrors +2 0
SetAutoClose true
SectionEnd
Function RestorePreviousInstallLocation
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
StrCmp $4 "" +2 0
StrCpy $INSTDIR $4
FunctionEnd
Function SkipIfPassive
${IfThen} $PassiveMode == 1 ${|} Abort ${|}
FunctionEnd
Function CreateDesktopShortcut
CreateShortcut "$DESKTOP\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
ApplicationID::Set "$DESKTOP\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
FunctionEnd
Function CreateStartMenuShortcut
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
ApplicationID::Set "$SMPROGRAMS\$AppStartMenuFolder\${MAINBINARYNAME}.lnk" "${BUNDLEID}"
FunctionEnd

Binary file not shown.

View File

@@ -0,0 +1,10 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" rx="18" fill="url(#paint0_linear_971_118)"/>
<path d="M17.9917 9.66675C13.3917 9.66675 9.66669 13.4001 9.66669 18.0001C9.66669 22.6001 13.3917 26.3334 17.9917 26.3334C22.6 26.3334 26.3334 22.6001 26.3334 18.0001C26.3334 13.4001 22.6 9.66675 17.9917 9.66675ZM23.7667 14.6667H21.3084C21.0417 13.6251 20.6584 12.6251 20.1584 11.7001C21.6917 12.2251 22.9667 13.2917 23.7667 14.6667ZM18 11.3667C18.6917 12.3667 19.2334 13.4751 19.5917 14.6667H16.4084C16.7667 13.4751 17.3084 12.3667 18 11.3667ZM11.55 19.6667C11.4167 19.1334 11.3334 18.5751 11.3334 18.0001C11.3334 17.4251 11.4167 16.8667 11.55 16.3334H14.3667C14.3 16.8834 14.25 17.4334 14.25 18.0001C14.25 18.5667 14.3 19.1167 14.3667 19.6667H11.55ZM12.2334 21.3334H14.6917C14.9584 22.3751 15.3417 23.3751 15.8417 24.3001C14.3084 23.7751 13.0334 22.7167 12.2334 21.3334ZM14.6917 14.6667H12.2334C13.0334 13.2834 14.3084 12.2251 15.8417 11.7001C15.3417 12.6251 14.9584 13.6251 14.6917 14.6667ZM18 24.6334C17.3084 23.6334 16.7667 22.5251 16.4084 21.3334H19.5917C19.2334 22.5251 18.6917 23.6334 18 24.6334ZM19.95 19.6667H16.05C15.975 19.1167 15.9167 18.5667 15.9167 18.0001C15.9167 17.4334 15.975 16.8751 16.05 16.3334H19.95C20.025 16.8751 20.0834 17.4334 20.0834 18.0001C20.0834 18.5667 20.025 19.1167 19.95 19.6667ZM20.1584 24.3001C20.6584 23.3751 21.0417 22.3751 21.3084 21.3334H23.7667C22.9667 22.7084 21.6917 23.7751 20.1584 24.3001ZM21.6334 19.6667C21.7 19.1167 21.75 18.5667 21.75 18.0001C21.75 17.4334 21.7 16.8834 21.6334 16.3334H24.45C24.5834 16.8667 24.6667 17.4251 24.6667 18.0001C24.6667 18.5751 24.5834 19.1334 24.45 19.6667H21.6334Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_971_118" x1="31" y1="27.5" x2="6.5" y2="7" gradientUnits="userSpaceOnUse">
<stop stop-color="#009038"/>
<stop offset="1" stop-color="#1CA350"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,10 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" rx="18" fill="url(#paint0_linear_971_127)"/>
<path d="M18.8334 22.1667H12.1667C11.7084 22.1667 11.3334 22.5417 11.3334 23.0001C11.3334 23.4584 11.7084 23.8334 12.1667 23.8334H18.8334C19.2917 23.8334 19.6667 23.4584 19.6667 23.0001C19.6667 22.5417 19.2917 22.1667 18.8334 22.1667ZM23.8334 15.5001H12.1667C11.7084 15.5001 11.3334 15.8751 11.3334 16.3334C11.3334 16.7917 11.7084 17.1667 12.1667 17.1667H23.8334C24.2917 17.1667 24.6667 16.7917 24.6667 16.3334C24.6667 15.8751 24.2917 15.5001 23.8334 15.5001ZM12.1667 20.5001H23.8334C24.2917 20.5001 24.6667 20.1251 24.6667 19.6667C24.6667 19.2084 24.2917 18.8334 23.8334 18.8334H12.1667C11.7084 18.8334 11.3334 19.2084 11.3334 19.6667C11.3334 20.1251 11.7084 20.5001 12.1667 20.5001ZM11.3334 13.0001C11.3334 13.4584 11.7084 13.8334 12.1667 13.8334H23.8334C24.2917 13.8334 24.6667 13.4584 24.6667 13.0001C24.6667 12.5417 24.2917 12.1667 23.8334 12.1667H12.1667C11.7084 12.1667 11.3334 12.5417 11.3334 13.0001Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_971_127" x1="6" y1="6.5" x2="29.5" y2="30.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#E96038"/>
<stop offset="1" stop-color="#E1451D"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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