Compare commits

...
840 Commits
Author SHA1 Message Date
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
GyDi e80be8e7b6 v0.0.27 2022-04-06 01:21:33 +08:00
GyDi fe0ad0f5cb chore: update log 2022-04-06 01:19:13 +08:00
GyDi cb8e162f4e refactor: update profile menu 2022-04-06 01:13:06 +08:00
GyDi ae8c30fe57 chore: add updater script 2022-04-05 23:24:52 +08:00
GyDi ab82db9e22 chore: updater json supports arch field 2022-04-05 23:19:54 +08:00
GyDi cb9d3098de chore: add ci 2022-04-05 20:21:53 +08:00
GyDi c927419c99 refactor: enhanced mode ui component 2022-04-03 01:41:48 +08:00
GyDi c009026961 feat: add text color 2022-04-02 17:18:38 +08:00
GyDi 51cf442fa5 fix: i18n add value 2022-04-02 13:49:48 +08:00
GyDi 954e3553ee fix: proxy page first render 2022-04-02 01:23:31 +08:00
GyDi cde17385b4 feat: control final tun config 2022-04-01 23:55:44 +08:00
GyDi 5b9e078061 chore: add debug feature 2022-04-01 23:43:35 +08:00
GyDi 0290d9ddfc feat: support css injection 2022-04-01 02:08:42 +08:00
GyDi eab671d102 fix: console warning 2022-04-01 01:16:23 +08:00
GyDi f9a96ff914 feat: support theme setting 2022-03-31 23:38:00 +08:00
GyDi 3ec2b46d28 feat: add text color 2022-03-31 23:34:36 +08:00
GyDi aec30b89e0 feat: add theme setting 2022-03-31 23:11:50 +08:00
GyDi 309c33e190 refactor: ui theme 2022-03-30 12:36:39 +08:00
GyDi 9c0276f97b chore: add api 2022-03-30 01:30:22 +08:00
GyDi 0a3402ff43 fix: icon button title 2022-03-30 01:29:25 +08:00
GyDi bd82308024 chore: update tauri 2022-03-29 23:05:32 +08:00
GyDi db3b634e62 fix: macOS transition flickers close #47 2022-03-29 01:39:54 +08:00
GyDi 40bcb22977 chore: update deps 2022-03-29 01:33:14 +08:00
GyDi 4bd94092f1 fix: csp image data 2022-03-28 23:17:11 +08:00
GyDi cac1ce6895 chore: readme 2022-03-28 01:33:58 +08:00
GyDi 5c3696123a v0.0.26 2022-03-28 01:30:54 +08:00
GyDi 4140fc86ee chore: update log 2022-03-28 01:18:18 +08:00
GyDi 2ad771e7fd fix: close dialog after save 2022-03-28 01:09:34 +08:00
GyDi 5a38468144 refactor: optimize enhance mode strategy 2022-03-28 01:07:14 +08:00
GyDi 0a9c81772f fix: change to deep copy 2022-03-28 00:56:48 +08:00
GyDi c9649ac501 feat: enhanced mode supports more fields 2022-03-27 20:36:13 +08:00
GyDi 6ea567742b chore: profile release 2022-03-27 01:38:40 +08:00
GyDi f31349eaa0 feat: supports edit profile file 2022-03-27 00:58:17 +08:00
GyDi 9d44668d5f feat: supports silent start 2022-03-26 18:56:16 +08:00
GyDi a12f58c1c7 feat: use crate open 2022-03-23 23:00:14 +08:00
GyDi b5283eaaed fix: window style close #45 2022-03-23 21:43:13 +08:00
GyDi 4b6189af5f feat: enhance connections display order 2022-03-23 14:02:08 +08:00
GyDi 5d0ffbe453 chore: adjust style 2022-03-23 13:58:57 +08:00
GyDi 502706931e fix: manage global proxy correctly 2022-03-23 12:49:34 +08:00
GyDi 57c411288f fix: tauri csp 2022-03-23 01:47:35 +08:00
GyDi b09b7b11a1 feat: save global selected 2022-03-22 12:38:59 +08:00
200 changed files with 22008 additions and 9496 deletions
+6
View File
@@ -5,3 +5,9 @@ charset = utf-8
end_of_line = lf
indent_size = 2
insert_final_newline = true
[*.rs]
charset = utf-8
end_of_line = lf
indent_size = 4
insert_final_newline = true
+45
View File
@@ -0,0 +1,45 @@
name: Bug report
description: Create a report to help us improve
title: "[BUG]"
labels: ["bug"]
body:
- type: textarea
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: Platform
options:
- Windows
- Linux
- MacOS
validations:
required: true
- type: input
attributes:
label: System Version
placeholder: "e.g. macOS 10.15.7"
validations:
required: true
- type: input
attributes:
label: Software Version
placeholder: "e.g. 1.4.3"
validations:
required: true
- type: textarea
attributes:
label: Log
description: "Log file content or screenshot"
- type: textarea
attributes:
label: Additional Information
@@ -0,0 +1,27 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature]"
labels: ["enhancement"]
body:
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
+4
View File
@@ -0,0 +1,4 @@
FROM rust:buster
COPY entrypoint.sh /entrypoint.sh
RUN chmod a+x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
+14
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 }}
+9
View File
@@ -0,0 +1,9 @@
# pnpm install --resolution-only
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" ] || [ "$INPUT_TARGET" = "i686-unknown-linux-gnu" ]; then
pnpm build --target $INPUT_TARGET
else
pnpm build --target $INPUT_TARGET -b deb
fi
+47
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" = "i686-unknown-linux-gnu" ]; then
dpkg --add-architecture i386
apt-get update
apt-get install -y libstdc++6:i386 libgdk-pixbuf2.0-dev:i386 libatomic1:i386 gcc-multilib g++-multilib libwebkit2gtk-4.0-dev:i386 libssl-dev:i386 libgtk-3-dev:i386 librsvg2-dev:i386 patchelf:i386 libayatana-appindicator3-dev:i386
export PKG_CONFIG_PATH=/usr/lib/i386-linux-gnu/pkgconfig/:$PKG_CONFIG_PATH
export PKG_CONFIG_SYSROOT_DIR=/
elif [ "$INPUT_TARGET" = "aarch64-unknown-linux-gnu" ]; then
sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | tee /etc/apt/sources.list.d/ports.list
sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list
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 libssl-dev:arm64 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
sed 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch-=amd64,i386] http:\/\/ports.ubuntu.com\/ubuntu-ports\//g' /etc/apt/sources.list | tee /etc/apt/sources.list.d/ports.list
sed -i 's/http:\/\/\(.*\).ubuntu.com\/ubuntu\//[arch=amd64,i386] http:\/\/\1.archive.ubuntu.com\/ubuntu\//g' /etc/apt/sources.list
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 libssl-dev:armhf 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
else
echo "Unknown target: $INPUT_TARGET" && exit 1
fi
bash .github/build-for-linux/build.sh
-112
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 update.json
run: yarn run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+170
View File
@@ -0,0 +1,170 @@
name: Release Build
on:
workflow_dispatch:
push:
tags:
- v**
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: i686-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: Apply Patch
if: matrix.target == 'aarch64-pc-windows-msvc'
run: |
git config --global user.email "clash-verge-rev@github.io"
git config --global user.name "clash-verge-rev"
git am patches/support-windows-aarch64.patch
- name: Init Submodule
if: matrix.target == 'aarch64-pc-windows-msvc'
run: git submodule update --init --recursive
- name: Install Rust Stable
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@v2
name: Install pnpm
with:
version: 8
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 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 }}
VITE_WIN_PORTABLE: 1
release-for-linux:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-latest
target: i686-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') || startsWith(matrix.target, 'i686')
uses: softprops/action-gh-release@v1
with:
tag_name: v${{env.VERSION}}
name: "Clash Verge 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 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:
needs: [release]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
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 }}
+29
View File
@@ -0,0 +1,29 @@
name: Updater CI
on: workflow_dispatch
permissions: write-all
jobs:
release-update:
runs-on: ubuntu-latest
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 }}
+2
View File
@@ -4,3 +4,5 @@ dist
dist-ssr
*.local
update.json
scripts/_env.sh
.vscode
Regular → Executable
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn pretty-quick --staged
pnpm pretty-quick --staged
+62 -23
View File
@@ -1,69 +1,105 @@
<h1 align="center">
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
<br>
Clash Verge
Continuation of <a href="https://github.com/zzzgydi/clash-verge">Clash Verge</a>
<br>
</h1>
<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>
## Features
- Full `clash` config supported, Partial `clash premium` config supported.
- Profiles management and enhancement (by yaml and Javascript).
- 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://github.com/clash-verge-rev/clash-verge-rev/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
- Simple UI and supports custom theme color.
- Built-in support [Clash.Meta(mihomo)](https://github.com/MetaCubeX/mihomo) core.
- System proxy setting and guard.
#### 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
## Preview
![preview](./docs/preview.gif)
## Install
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
Download from [release](https://github.com/clash-verge-rev/clash-verge-rev/releases). Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
- [Windows x64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_x64-setup.exe)
- [Windows x86](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_x86-setup.exe)
- [Windows arm64](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_arm64-setup.exe)
- [macOS intel](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_x64.dmg)
- [macOS apple](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/Clash.Verge_1.4.4_aarch64.dmg)
- [Linux x64 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_amd64.AppImage)
- [Linux x64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_amd64.deb)
- [Linux x86 AppImage](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_i386.AppImage)
- [Linux x86 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_i386.deb)
- [Linux arm64 deb](https://github.com/clash-verge-rev/clash-verge-rev/releases/download/v1.4.4/clash-verge_1.4.4_arm64.deb)
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
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.
### FAQ
#### 1. **macOS** "Clash Verge" is damaged and can't be opened
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Verge.app`
## Development
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages.
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
```shell
yarn install
pnpm i
```
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).
Then download the clash binary... Or you can download it from [clash meta release](https://github.com/MetaCubeX/Clash.Meta/releases) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
```shell
yarn run check
# force update to latest version
# pnpm run check --force
pnpm run check
```
Then run
```shell
yarn dev
pnpm dev
# run it in another way if app instance exists
pnpm dev:diff
```
Or you can build it
```shell
yarn build
pnpm 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.
@@ -74,10 +110,13 @@ Issue and PR welcome!
## 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:
- [keiko233/clash-nyanpasu](https://github.com/keiko233/clash-nyanpasu): A Clash Verge variant.
- [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.
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
- [MetaCubeX/Clash.Meta](https://github.com/MetaCubeX/Clash.Meta): 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.
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
+516
View File
@@ -1,3 +1,519 @@
## 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
### Features
- supports custom theme color
- tun mode setting control the final config
### Bug Fixes
- fix transition flickers (macOS)
- reduce proxy page render
---
## v0.0.26
### Features
- silent start
- profile editor
- profile enhance mode supports more fields
- optimize profile enhance mode strategy
### Bug Fixes
- fix csp restriction on macOS
- window controllers on Linux
---
## v0.0.25
### Features
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

+53 -33
View File
@@ -1,55 +1,75 @@
{
"name": "clash-verge",
"version": "0.0.25",
"version": "1.4.4",
"license": "GPL-3.0",
"scripts": {
"dev": "tauri dev",
"dev:diff": "tauri dev -f verge-dev",
"build": "tauri build",
"tauri": "tauri",
"web:dev": "vite",
"web:build": "tsc && vite build",
"web:serve": "vite preview",
"check": "node scripts/check.mjs",
"green": "node scripts/green.mjs",
"publish": "node scripts/publish.mjs",
"release": "node scripts/release.mjs",
"updater": "node scripts/updater.mjs",
"portable": "node scripts/portable.mjs",
"prepare": "husky install"
},
"dependencies": {
"@emotion/react": "^11.8.1",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.4.4",
"@mui/material": "^5.4.4",
"@tauri-apps/api": "^1.0.0-rc.2",
"ahooks": "^3.1.13",
"axios": "^0.26.0",
"dayjs": "^1.10.8",
"i18next": "^21.6.14",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-i18next": "^11.15.6",
"react-router-dom": "^6.2.2",
"react-virtuoso": "^2.7.0",
"recoil": "^0.6.1",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^5.14.19",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^5.14.19",
"@mui/x-data-grid": "^6.18.2",
"@tauri-apps/api": "^1.5.1",
"ahooks": "^3.7.8",
"axios": "^1.6.2",
"dayjs": "1.11.5",
"i18next": "^23.7.7",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.34.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.48.2",
"react-i18next": "^13.5.0",
"react-router-dom": "^6.20.0",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.6.2",
"recoil": "^0.7.7",
"snarkdown": "^2.0.0",
"swr": "^1.2.1"
"swr": "^1.3.0",
"tar": "^6.2.0"
},
"devDependencies": {
"@actions/github": "^5.0.0",
"@tauri-apps/cli": "^1.0.0-rc.7",
"@actions/github": "^5.1.1",
"@tauri-apps/cli": "^1.5.6",
"@types/fs-extra": "^9.0.13",
"@types/js-cookie": "^3.0.1",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@vitejs/plugin-react": "^1.2.0",
"adm-zip": "^0.5.9",
"fs-extra": "^10.0.0",
"husky": "^7.0.0",
"node-fetch": "^3.2.0",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.14.202",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.17",
"@types/react-transition-group": "^4.4.9",
"@vitejs/plugin-react": "^4.2.0",
"adm-zip": "^0.5.10",
"cross-env": "^7.0.3",
"fs-extra": "^11.2.0",
"https-proxy-agent": "^5.0.1",
"husky": "^7.0.4",
"node-fetch": "^3.3.2",
"prettier": "^2.8.8",
"pretty-quick": "^3.1.3",
"sass": "^1.49.7",
"typescript": "^4.5.5",
"vite": "^2.8.6"
"sass": "^1.69.5",
"typescript": "^5.3.2",
"vite": "^4.5.0",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.2.0"
},
"prettier": {
"tabWidth": 2,
+195
View File
@@ -0,0 +1,195 @@
From 8b085aea2f11e64f433244eda092c178a2bb50bc Mon Sep 17 00:00:00 2001
From: MystiPanda <mystipanda@proton.me>
Date: Sun, 10 Dec 2023 19:47:45 +0800
Subject: [PATCH] feat: Support windows aarch64
---
.gitmodules | 3 +
src-tauri/Cargo.toml | 2 +-
src-tauri/quick-rs | 1 +
src-tauri/src/enhance/script.rs | 130 +++++++++++++++++++-------------
4 files changed, 81 insertions(+), 55 deletions(-)
create mode 100644 .gitmodules
create mode 160000 src-tauri/quick-rs
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2eda7e4
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "src-tauri/quick-rs"]
+ path = src-tauri/quick-rs
+ url = https://github.com/clash-verge-rev/quick-rs.git
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 4c6dde5..5fd9ad8 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -25,7 +25,6 @@ log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.29"
-rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
auto-launch = "0.5"
@@ -34,6 +33,7 @@ port_scanner = "0.1.5"
delay_timer = "0.11"
parking_lot = "0.12"
percent-encoding = "2.3.1"
+quick-rs = { path = "quick-rs" }
window-shadows = { version = "0.2" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
diff --git a/src-tauri/quick-rs b/src-tauri/quick-rs
new file mode 160000
index 0000000..78277c4
--- /dev/null
+++ b/src-tauri/quick-rs
@@ -0,0 +1 @@
+Subproject commit 78277c4509c64f18c0fc5c9f2b84671de7c83343
diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs
index 6c207d9..d47dc33 100644
--- a/src-tauri/src/enhance/script.rs
+++ b/src-tauri/src/enhance/script.rs
@@ -3,61 +3,83 @@ use anyhow::Result;
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
- use rquickjs::{function::Func, Context, Runtime};
- use std::sync::{Arc, Mutex};
-
- let runtime = Runtime::new().unwrap();
- let context = Context::full(&runtime).unwrap();
- let outputs = Arc::new(Mutex::new(vec![]));
-
- let copy_outputs = outputs.clone();
- let result = context.with(|ctx| -> Result<Mapping> {
- ctx.globals().set(
- "__verge_log__",
- Func::from(move |level: String, data: String| {
- let mut out = copy_outputs.lock().unwrap();
- out.push((level, data));
- }),
- )?;
-
- ctx.eval(
- r#"var console = Object.freeze({
- log(data){__verge_log__("log",JSON.stringify(data))},
- info(data){__verge_log__("info",JSON.stringify(data))},
- error(data){__verge_log__("error",JSON.stringify(data))},
- debug(data){__verge_log__("debug",JSON.stringify(data))},
- });"#,
- )?;
-
- let config = use_lowercase(config.clone());
- let config_str = serde_json::to_string(&config)?;
-
- let code = format!(
- r#"try{{
+ use quick_rs::{context::Context, function::Function, module::Module, runtime::Runtime};
+
+ let config = use_lowercase(config.clone());
+ let config_str = serde_json::to_string(&config)?;
+
+ let runtime = Runtime::new();
+ let context = Context::from(&runtime);
+
+ let code = format!(
+ r#"
+ let output = [];
+
+ function __verge_log__(type, data) {{
+ output.push([type, data]);
+ }}
+
+ var console = Object.freeze({{
+ log(data) {{ __verge_log__("log", JSON.stringify(data)) }},
+ info(data) {{ __verge_log__("info", JSON.stringify(data)) }},
+ error(data) {{ __verge_log__("error", JSON.stringify(data)) }},
+ debug(data) {{ __verge_log__("debug", JSON.stringify(data)) }},
+ }});
+
{script};
- JSON.stringify(main({config_str})||'')
- }} catch(err) {{
- `__error_flag__ ${{err.toString()}}`
- }}"#
- );
- let result: String = ctx.eval(code.as_str())?;
- if result.starts_with("__error_flag__") {
- anyhow::bail!(result[15..].to_owned());
- }
- if result == "\"\"" {
- anyhow::bail!("main function should return object");
- }
- return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
- });
-
- let mut out = outputs.lock().unwrap();
- match result {
- Ok(config) => Ok((use_lowercase(config), out.to_vec())),
- Err(err) => {
- out.push(("exception".into(), err.to_string()));
- Ok((config, out.to_vec()))
- }
- }
+
+ export function _main(){{
+ try{{
+ let result = JSON.stringify(main({config_str})||"");
+ return JSON.stringify({{result, output}});
+ }} catch(err) {{
+ output.push(["exception", err.toString()]);
+ return JSON.stringify({{result: "__error__", output}});
+ }}
+ }}
+ "#
+ );
+ let value = context.eval_module(&code, "_main")?;
+ let module = Module::new(value)?;
+ let value = module.get("_main")?;
+ let function = Function::new(value)?;
+ let value = function.call(vec![])?;
+ let result = serde_json::from_str::<serde_json::Value>(&value.to_string()?)?;
+ result
+ .as_object()
+ .map(|obj| {
+ let result = obj.get("result").unwrap().as_str().unwrap();
+ let output = obj.get("output").unwrap();
+
+ let mut out = output
+ .as_array()
+ .unwrap()
+ .iter()
+ .map(|item| {
+ let item = item.as_array().unwrap();
+ (
+ item[0].as_str().unwrap().into(),
+ item[1].as_str().unwrap().into(),
+ )
+ })
+ .collect::<Vec<_>>();
+ if result.is_empty() {
+ anyhow::bail!("main function should return object");
+ }
+ if result == "__error__" {
+ return Ok((config, out.to_vec()));
+ }
+ let result = serde_json::from_str::<Mapping>(result);
+
+ match result {
+ Ok(config) => Ok((use_lowercase(config), out.to_vec())),
+ Err(err) => {
+ out.push(("exception".into(), err.to_string()));
+ Ok((config, out.to_vec()))
+ }
+ }
+ })
+ .unwrap_or_else(|| anyhow::bail!("Unknown result"))
}
#[test]
--
2.43.0.windows.1
+4092
View File
File diff suppressed because it is too large Load Diff
+303 -112
View File
@@ -1,159 +1,266 @@
import fs from "fs-extra";
import zlib from "zlib";
import tar from "tar";
import path from "path";
import AdmZip from "adm-zip";
import fetch from "node-fetch";
import proxyAgent from "https-proxy-agent";
import { execSync } from "child_process";
const cwd = process.cwd();
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
const FORCE = process.argv.includes("--force");
/**
* get the correct clash release infomation
*/
function resolveClash() {
const { platform, arch } = process;
const PLATFORM_MAP = {
"x86_64-pc-windows-msvc": "win32",
"i686-pc-windows-msvc": "win32",
"aarch64-pc-windows-msvc": "win32",
"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",
};
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",
};
const CLASH_URL_PREFIX =
"https://github.com/Dreamacro/clash/releases/download/premium/";
const CLASH_LATEST_DATE = "2022.03.21";
const arg1 = process.argv.slice(2)[0];
const arg2 = process.argv.slice(2)[1];
const target = arg1 === "--force" ? arg2 : arg1;
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process;
// todo
const map = {
"win32-x64": "clash-windows-amd64",
"darwin-x64": "clash-darwin-amd64",
"darwin-arm64": "clash-darwin-arm64",
"linux-x64": "clash-linux-amd64",
};
const SIDECAR_HOST = target
? target
: execSync("rustc -vV")
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0];
const name = map[`${platform}-${arch}`];
/* ======= clash meta alpha======= */
const 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) {
throw new Error(`unsupport platform "${platform}-${arch}"`);
const META_ALPHA_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",
};
// Fetch the latest release version from the version.txt file
async function getLatestVersion() {
try {
const response = await fetch(VERSION_URL, { method: "GET" });
let v = await response.text();
META_ALPHA_VERSION = v.trim(); // Trim to remove extra whitespaces
console.log(`Latest release version: ${META_ALPHA_VERSION}`);
} catch (error) {
console.error("Error fetching latest release version:", error.message);
process.exit(1);
}
}
const isWin = platform === "win32";
const zip = isWin ? "zip" : "gz";
const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`;
const exefile = `${name}${isWin ? ".exe" : ""}`;
const zipfile = `${name}.${zip}`;
/* ======= clash meta stable ======= */
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`;
let META_VERSION = "v1.17.0";
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",
};
/*
* 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() {
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
function clashMetaAlpha() {
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();
const ext = process.platform === "win32" ? ".exe" : "";
const sidecarFile = `clash-${host}${ext}`;
const sidecarPath = path.join(sidecarDir, sidecarFile);
return {
name: "clash-meta-alpha",
targetFile: `clash-meta-alpha-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
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);
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
// download sidecar
const binInfo = resolveClash();
const tempDir = path.join(cwd, "pre-dev-temp");
const tempZip = path.join(tempDir, binInfo.zipfile);
const tempExe = path.join(tempDir, binInfo.exefile);
const tempDir = path.join(TEMP_DIR, name);
const tempZip = path.join(tempDir, zipFile);
const tempExe = path.join(tempDir, exeFile);
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") {
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log("[INFO]: entry name", entry.entryName);
});
zip.extractAllTo(tempDir, true);
// save as sidecar
await fs.rename(tempExe, sidecarPath);
console.log(`[INFO]: unzip finished`);
} else {
// gz
const readStream = fs.createReadStream(tempZip);
const writeStream = fs.createWriteStream(sidecarPath);
readStream
.pipe(zlib.createGunzip())
.pipe(writeStream)
.on("finish", () => {
console.log(`[INFO]: gunzip finished`);
if (zipFile.endsWith(".zip")) {
const zip = new AdmZip(tempZip);
zip.getEntries().forEach((entry) => {
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
});
zip.extractAllTo(tempDir, true);
await fs.rename(tempExe, sidecarPath);
console.log(`[INFO]: "${name}" unzip finished`);
} else if (zipFile.endsWith(".tgz")) {
// tgz
await fs.mkdirp(tempDir);
await tar.extract({
cwd: tempDir,
file: tempZip,
//strip: 1, // 可能需要根据实际的 .tgz 文件结构调整
});
const files = await fs.readdir(tempDir);
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}`);
console.log(`[INFO]: chmod binary finished`);
})
.on("error", (error) => console.error(error));
console.log(`[INFO]: "${name}" chmod binary finished`);
} else {
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
* get the wintun.dll (not required)
* download the file to the resources dir
*/
async function resolveWintun() {
const { platform } = process;
async function resolveResource(binInfo) {
const { file, downloadURL } = binInfo;
if (platform !== "win32") return;
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");
const resDir = path.join(cwd, "src-tauri/resources");
const targetPath = path.join(resDir, file);
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 downloadFile(url, resPath);
await downloadFile(downloadURL, targetPath);
console.log(`[INFO]: ${file} finished`);
}
/**
* download file and save to `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, {
...options,
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
@@ -163,7 +270,91 @@ async function downloadFile(url, path) {
console.log(`[INFO]: download finished "${url}"`);
}
/// main
resolveSidecar().catch(console.error);
resolveWintun().catch(console.error);
resolveMmdb().catch(console.error);
/**
* main
*/
const SERVICE_URL =
"https://github.com/zzzgydi/clash-verge-service/releases/download/latest";
const resolveService = () =>
resolveResource({
file: "clash-verge-service.exe",
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
});
const resolveInstall = () =>
resolveResource({
file: "install-service.exe",
downloadURL: `${SERVICE_URL}/install-service.exe`,
});
const resolveUninstall = () =>
resolveResource({
file: "uninstall-service.exe",
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
});
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: () => getLatestVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: "clash-meta",
func: () => resolveSidecar(clashMeta()),
retry: 5,
},
// { name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true },
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
{ 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,
},
];
async function runTask() {
const task = tasks.shift();
if (!task) return;
if (task.winOnly && process.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();
+25 -7
View File
@@ -4,31 +4,47 @@ import AdmZip from "adm-zip";
import { createRequire } from "module";
import { getOctokit, context } from "@actions/github";
const target = process.argv.slice(2)[0];
const ARCH_MAP = {
"i686-pc-windows-msvc": "x86",
"x86_64-pc-windows-msvc": "x64",
"aarch64-pc-windows-msvc": "arm64",
};
/// Script for ci
/// 打包绿色版/便携版 (only Windows)
async function resolveGreen() {
async function resolvePortable() {
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))) {
throw new Error("could not found the release dir");
}
await fs.mkdir(configDir);
await fs.createFile(path.join(configDir, "PORTABLE"));
const zip = new AdmZip();
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(configDir, ".config");
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");
const { version } = packageJson;
const zipFile = `Clash.Verge_${version}_x64_green.zip`;
const zipFile = `Clash.Verge_${version}_${ARCH_MAP[target]}_portable.zip`;
zip.writeZip(zipFile);
console.log("[INFO]: create green zip successfully");
console.log("[INFO]: create portable zip successfully");
// push release assets
if (process.env.GITHUB_TOKEN === undefined) {
@@ -38,9 +54,11 @@ async function resolveGreen() {
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
const { data: release } = await github.rest.repos.getReleaseByTag({
...options,
tag: `v${version}`,
tag: process.env.TAG_NAME || `v${version}`,
});
console.log(release.name);
@@ -53,4 +71,4 @@ async function resolveGreen() {
});
}
resolveGreen().catch(console.error);
resolvePortable().catch(console.error);
-53
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();
-164
View File
@@ -1,164 +0,0 @@
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";
const UPDATE_TAG_NAME = "updater";
const UPDATE_JSON_FILE = "update.json";
const UPDATE_JSON_PROXY = "update-proxy.json";
/// generate update.json
/// upload to update tag's release asset
async function resolveRelease() {
if (process.env.GITHUB_TOKEN === undefined) {
throw new Error("GITHUB_TOKEN is required");
}
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});
// get the latest publish tag
const tag = tags.find((t) => t.name.startsWith("v"));
console.log(tag);
console.log();
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});
const updateData = {
name: tag.name,
notes: await resolveUpdateLog(tag.name), // use updatelog.md
pub_date: new Date().toISOString(),
platforms: {
win64: { signature: "", url: "" },
linux: { signature: "", url: "" },
darwin: { signature: "", url: "" },
},
};
const promises = latestRelease.assets.map(async (asset) => {
const { name, browser_download_url } = asset;
// win64 url
if (/\.msi\.zip$/.test(name)) {
updateData.platforms.win64.url = browser_download_url;
}
// win64 signature
if (/\.msi\.zip\.sig$/.test(name)) {
updateData.platforms.win64.signature = await getSignature(
browser_download_url
);
}
// darwin url
if (/\.app\.tar\.gz$/.test(name)) {
updateData.platforms.darwin.url = browser_download_url;
}
// darwin signature
if (/\.app\.tar\.gz\.sig$/.test(name)) {
updateData.platforms.darwin.signature = await getSignature(
browser_download_url
);
}
// linux url
if (/\.AppImage\.tar\.gz$/.test(name)) {
updateData.platforms.linux.url = browser_download_url;
}
// linux signature
if (/\.AppImage\.tar\.gz\.sig$/.test(name)) {
updateData.platforms.linux.signature = await getSignature(
browser_download_url
);
}
});
await Promise.allSettled(promises);
console.log(updateData);
// maybe should test the signature as well
const { darwin, win64, linux } = updateData.platforms;
if (!darwin.url) {
console.log(`[Error]: failed to parse release for darwin`);
delete updateData.platforms.darwin;
}
if (!win64.url) {
console.log(`[Error]: failed to parse release for win64`);
delete updateData.platforms.win64;
}
if (!linux.url) {
console.log(`[Error]: failed to parse release for linux`);
delete updateData.platforms.linux;
}
// 生成一个代理github的更新文件
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
const updateDataNew = JSON.parse(JSON.stringify(updateData));
Object.keys(updateDataNew.platforms).forEach((key) => {
if (updateDataNew.platforms[key]) {
updateDataNew.platforms[key].url = updateDataNew.platforms[
key
].url.replace("https://github.com/", "https://hub.fastgit.xyz/");
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
}
});
// update the update.json
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: UPDATE_TAG_NAME,
});
// delete the old assets
for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: asset.id,
});
}
if (asset.name === UPDATE_JSON_PROXY) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error); // do not break the pipeline
}
}
// upload assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_FILE,
data: JSON.stringify(updateData, null, 2),
});
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_PROXY,
data: JSON.stringify(updateDataNew, null, 2),
});
}
// get the signature file content
async function getSignature(url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
return response.text();
}
resolveRelease().catch(console.error);
+218
View File
@@ -0,0 +1,218 @@
import fetch from "node-fetch";
import { getOctokit, context } from "@actions/github";
import { resolveUpdateLog } from "./updatelog.mjs";
const UPDATE_TAG_NAME = "updater";
const UPDATE_JSON_FILE = "update.json";
const UPDATE_JSON_PROXY = "update-proxy.json";
/// generate update.json
/// upload to update tag's release asset
async function resolveUpdater() {
if (process.env.GITHUB_TOKEN === undefined) {
throw new Error("GITHUB_TOKEN is required");
}
const options = { owner: context.repo.owner, repo: context.repo.repo };
const github = getOctokit(process.env.GITHUB_TOKEN);
const { data: tags } = await github.rest.repos.listTags({
...options,
per_page: 10,
page: 1,
});
// get the latest publish tag
const tag = tags.find((t) => t.name.startsWith("v"));
console.log(tag);
console.log();
const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: tag.name,
});
const updateData = {
name: tag.name,
notes: await resolveUpdateLog(tag.name), // use updatelog.md
pub_date: new Date().toISOString(),
platforms: {
win64: { signature: "", url: "" }, // compatible with older formats
linux: { signature: "", url: "" }, // compatible with older formats
darwin: { signature: "", url: "" }, // compatible with older formats
"darwin-aarch64": { signature: "", url: "" },
"darwin-intel": { signature: "", url: "" },
"darwin-x86_64": { signature: "", url: "" },
"linux-x86_64": { signature: "", url: "" },
"linux-i686": { signature: "", url: "" },
"linux-aarch64": { signature: "", url: "" },
"linux-armv7": { signature: "", url: "" },
"windows-x86_64": { signature: "", url: "" },
"windows-i686": { signature: "", url: "" },
"windows-aarch64": { signature: "", url: "" },
},
};
const promises = latestRelease.assets.map(async (asset) => {
const { name, browser_download_url } = asset;
// win64 url
if (name.endsWith("x64-setup.nsis.zip")) {
updateData.platforms.win64.url = browser_download_url;
updateData.platforms["windows-x86_64"].url = browser_download_url;
}
// win64 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms.win64.signature = sig;
updateData.platforms["windows-x86_64"].signature = sig;
}
// win32 url
if (name.endsWith("x86-setup.nsis.zip")) {
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x86-setup.nsis.zip.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-i686"].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)
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
updateData.platforms.darwin.url = browser_download_url;
updateData.platforms["darwin-intel"].url = browser_download_url;
updateData.platforms["darwin-x86_64"].url = browser_download_url;
}
// darwin signature (intel)
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
const sig = await getSignature(browser_download_url);
updateData.platforms.darwin.signature = sig;
updateData.platforms["darwin-intel"].signature = sig;
updateData.platforms["darwin-x86_64"].signature = sig;
}
// darwin url (aarch)
if (name.endsWith("aarch64.app.tar.gz")) {
updateData.platforms["darwin-aarch64"].url = browser_download_url;
}
// darwin signature (aarch)
if (name.endsWith("aarch64.app.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["darwin-aarch64"].signature = sig;
}
// linux x64 url
if (name.endsWith("amd64.AppImage.tar.gz")) {
updateData.platforms.linux.url = browser_download_url;
updateData.platforms["linux-x86_64"].url = browser_download_url;
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].url = browser_download_url;
updateData.platforms["linux-armv7"].url = browser_download_url;
}
// linux x64 signature
if (name.endsWith("amd64.AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms.linux.signature = sig;
updateData.platforms["linux-x86_64"].signature = sig;
// 暂时使用x64版本的url和sig,使得可以检查更新,但aarch64版本还不支持构建appimage
updateData.platforms["linux-aarch64"].signature = sig;
updateData.platforms["linux-armv7"].signature = sig;
}
// linux x86 url
if (name.endsWith("i386.AppImage.tar.gz")) {
updateData.platforms["linux-i686"].url = browser_download_url;
}
// linux x86 signature
if (name.endsWith("i386.AppImage.tar.gz.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["linux-i686"].signature = sig;
}
});
await Promise.allSettled(promises);
console.log(updateData);
// maybe should test the signature as well
// delete the null field
Object.entries(updateData.platforms).forEach(([key, value]) => {
if (!value.url) {
console.log(`[Error]: failed to parse release for "${key}"`);
delete updateData.platforms[key];
}
});
// 生成一个代理github的更新文件
// 使用 https://hub.fastgit.xyz/ 做github资源的加速
const updateDataNew = JSON.parse(JSON.stringify(updateData));
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
if (value.url) {
updateDataNew.platforms[key].url =
"https://mirror.ghproxy.com/" + value.url;
} else {
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
}
});
// update the update.json
const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
...options,
tag: UPDATE_TAG_NAME,
});
// delete the old assets
for (let asset of updateRelease.assets) {
if (asset.name === UPDATE_JSON_FILE) {
await github.rest.repos.deleteReleaseAsset({
...options,
asset_id: asset.id,
});
}
if (asset.name === UPDATE_JSON_PROXY) {
await github.rest.repos
.deleteReleaseAsset({ ...options, asset_id: asset.id })
.catch(console.error); // do not break the pipeline
}
}
// upload new assets
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_FILE,
data: JSON.stringify(updateData, null, 2),
});
await github.rest.repos.uploadReleaseAsset({
...options,
release_id: updateRelease.id,
name: UPDATE_JSON_PROXY,
data: JSON.stringify(updateDataNew, null, 2),
});
}
// get the signature file content
async function getSignature(url) {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/octet-stream" },
});
return response.text();
}
resolveUpdater().catch(console.error);
+2870 -1622
View File
File diff suppressed because it is too large Load Diff
+44 -25
View File
@@ -1,43 +1,62 @@
[package]
name = "clash-verge"
version = "0.1.0"
version = "1.4.4"
description = "clash verge"
authors = ["zzzgydi"]
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0"
repository = "https://github.com/zzzgydi/clash-verge.git"
repository = "https://github.com/clash-verge-rev/clash-verge-rev.git"
default-run = "clash-verge"
edition = "2021"
build = "build.rs"
[build-dependencies]
tauri-build = { version = "1.0.0-rc.4", features = [] }
tauri-build = { version = "1", features = [] }
[dependencies]
anyhow = "1.0"
dirs = "4.0.0"
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.4", 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"
which = "4.2.2"
auto-launch = "0.2"
which = "5.0.0"
anyhow = "1.0"
dirs = "5.0"
open = "5.0"
log = "0.4"
ctrlc = "3.4"
dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.29"
rquickjs = "0.3" # 高版本不支持 Linux aarch64
serde_json = "1.0"
serde_yaml = "0.9"
auto-launch = "0.5"
once_cell = "1.18"
port_scanner = "0.1.5"
delay_timer = "0.11"
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.11", features = ["json"] }
sysproxy = { git="https://github.com/zzzgydi/sysproxy-rs", branch = "main" }
tauri = { version = "1.5", features = ["icon-png", "clipboard-all", "global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
[target.'cfg(windows)'.dependencies]
winreg = { version = "0.10", features = ["transactions"] }
runas = "=1.0.0" # 高版本会返回错误 Status
deelevate = "0.2.0"
winreg = { version = "0.52", features = ["transactions"] }
windows-sys = { version = "0.52", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
[target.'cfg(target_os = "linux")'.dependencies]
#openssl
[features]
default = [ "custom-protocol" ]
custom-protocol = [ "tauri/custom-protocol" ]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
verge-dev = []
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
opt-level = "s"
+1 -1
View File
@@ -1,3 +1,3 @@
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.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 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

+1 -1
View File
@@ -1,6 +1,6 @@
max_width = 100
hard_tabs = false
tab_spaces = 2
tab_spaces = 4
newline_style = "Auto"
use_small_heuristics = "Default"
reorder_imports = true
+231 -313
View File
@@ -1,388 +1,306 @@
use crate::{
core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig},
states::{ClashState, ProfilesState, VergeState},
utils::{dirs, sysopt::SysProxyConfig},
config::*,
core::*,
feat,
utils::{dirs, help},
};
use crate::{ret_err, wrap_err};
use anyhow::Result;
use anyhow::{Context, Result};
use serde_yaml::Mapping;
use std::{path::PathBuf, process::Command};
use tauri::{api, Manager, State};
use std::collections::{HashMap, VecDeque};
use sysproxy::Sysproxy;
type CmdResult<T = ()> = Result<T, String>;
/// get all profiles from `profiles.yaml`
#[tauri::command]
pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> {
let profiles = profiles_state.0.lock().unwrap();
Ok(profiles.clone())
pub fn get_profiles() -> CmdResult<IProfiles> {
Ok(Config::profiles().data().clone())
}
/// synchronize data irregularly
#[tauri::command]
pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.sync_file())
pub async fn enhance_profiles() -> CmdResult {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
Ok(())
}
/// import the profile from url
/// and save to `profiles.yaml`
#[tauri::command]
pub async fn import_profile(
url: String,
option: Option<PrfOption>,
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))
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
wrap_err!(Config::profiles().data().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]
pub async fn create_profile(
item: PrfItem, // partial
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))
pub async fn reorder_profile(active_id: String, over_id: String) -> CmdResult {
wrap_err!(Config::profiles().data().reorder(active_id, over_id))
}
/// Update the profile
#[tauri::command]
pub async fn update_profile(
index: String,
option: Option<PrfOption>,
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let (url, opt) = {
// must release the lock here
let profiles = profiles_state.0.lock().unwrap();
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
wrap_err!(Config::profiles().data().append_item(item))
}
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
wrap_err!(feat::update_profile(index, option).await)
}
#[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();
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 data = wrap_err!(item.read_file())?;
Ok(data)
}
// check the profile type
if let Some(typ) = item.itype.as_ref() {
if *typ != "remote" {
ret_err!(format!("could not update the `{typ}` profile"));
}
#[tauri::command]
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if file_data.is_none() {
return Ok(());
}
if item.url.is_none() {
ret_err!("failed to get the item url");
}
(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(&profiles, false))?;
}
Ok(())
let profiles = Config::profiles();
let profiles = profiles.latest();
let item = wrap_err!(profiles.get_item(&index))?;
wrap_err!(item.save_file(file_data.unwrap()))
}
/// change the current profile
#[tauri::command]
pub fn select_profile(
index: String,
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(&profiles, false))
pub fn get_clash_info() -> CmdResult<ClashInfo> {
Ok(Config::clash().latest().get_client_info())
}
/// change the profile chain
#[tauri::command]
pub fn change_profile_chain(
chain: Option<Vec<String>>,
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))
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
Ok(Config::runtime().latest().config.clone())
}
/// manually exec enhanced profile
#[tauri::command]
pub fn enhance_profiles(
app_handle: tauri::AppHandle,
clash_state: State<'_, ClashState>,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut clash = clash_state.0.lock().unwrap();
let profiles = profiles_state.0.lock().unwrap();
clash.set_window(app_handle.get_window("main"));
wrap_err!(clash.activate_enhanced(&profiles, false))
pub fn get_runtime_yaml() -> CmdResult<String> {
let runtime = Config::runtime();
let runtime = runtime.latest();
let config = runtime.config.as_ref();
wrap_err!(config
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
.and_then(
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
))
}
/// delete profile item
#[tauri::command]
pub fn delete_profile(
index: String,
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(&profiles, false))?;
}
Ok(())
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
Ok(Config::runtime().latest().exists_keys.clone())
}
/// patch the profile config
#[tauri::command]
pub fn patch_profile(
index: String,
profile: PrfItem,
profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> {
let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.patch_item(index, profile))
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
Ok(Config::runtime().latest().chain_logs.clone())
}
/// run vscode command to edit the profile
#[tauri::command]
pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
let profiles = profiles_state.0.lock().unwrap();
let item = wrap_err!(profiles.get_item(&index))?;
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
wrap_err!(feat::patch_clash(payload).await)
}
let file = item.file.clone();
if file.is_none() {
ret_err!("the file is null");
}
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVerge> {
Ok(Config::verge().data().clone())
}
let path = dirs::app_profiles_dir().join(file.unwrap());
if !path.exists() {
ret_err!("the file not found");
}
#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
wrap_err!(feat::patch_verge(payload).await)
}
// 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!("{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!("{err}");
return Err("failed to open file by VScode".into());
}
return Ok(());
}
open_path_cmd(path, "failed to open file by `open`")
#[tauri::command]
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
wrap_err!(CoreManager::global().change_core(clash_core).await)
}
/// restart the sidecar
#[tauri::command]
pub fn restart_sidecar(
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();
wrap_err!(clash.restart_sidecar(&mut profiles))
pub async fn restart_sidecar() -> CmdResult {
wrap_err!(CoreManager::global().run_core().await)
}
/// get the clash core info from the state
/// the caller can also get the infomation by clash's api
#[tauri::command]
pub fn get_clash_info(clash_state: State<'_, ClashState>) -> Result<ClashInfo, String> {
let clash = clash_state.0.lock().unwrap();
Ok(clash.info.clone())
}
pub fn grant_permission(_core: String) -> CmdResult {
#[cfg(any(target_os = "macos", target_os = "linux"))]
return wrap_err!(manager::grant_permission(_core));
/// update the clash core config
/// after putting the change to the clash core
/// 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))
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
return Err("Unsupported target".into());
}
/// get the system proxy
#[tauri::command]
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
wrap_err!(SysProxyConfig::get_sys())
pub fn get_sys_proxy() -> CmdResult<Mapping> {
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]
pub fn get_cur_proxy(verge_state: State<'_, VergeState>) -> Result<Option<SysProxyConfig>, String> {
let verge = verge_state.0.lock().unwrap();
Ok(verge.cur_sysproxy.clone())
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
Ok(logger::Logger::global().get_log())
}
/// get the verge config
#[tauri::command]
pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result<VergeConfig, String> {
let verge = verge_state.0.lock().unwrap();
let mut config = verge.config.clone();
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)
pub fn open_app_dir() -> CmdResult<()> {
let app_dir = wrap_err!(dirs::app_home_dir())?;
wrap_err!(open::that(app_dir))
}
/// patch the verge config
/// this command only save the config and not responsible for other things
#[tauri::command]
pub fn patch_verge_config(
payload: VergeConfig,
app_handle: tauri::AppHandle,
clash_state: State<'_, ClashState>,
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();
// change tun mode
if tun_mode.is_some() {
let mut clash = clash_state.0.lock().unwrap();
let profiles = profiles_state.0.lock().unwrap();
wrap_err!(clash.tun_mode(tun_mode.unwrap()))?;
clash.update_config();
wrap_err!(clash.activate(&profiles, false))?;
}
let mut verge = verge_state.0.lock().unwrap();
wrap_err!(verge.patch_config(payload))?;
// change system tray
if system_proxy.is_some() {
app_handle
.tray_handle()
.get_item("system_proxy")
.set_selected(system_proxy.unwrap())
.unwrap();
}
Ok(())
pub fn open_core_dir() -> CmdResult<()> {
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?;
wrap_err!(open::that(core_dir))
}
/// kill all sidecars when update app
#[tauri::command]
pub fn kill_sidecars() {
api::process::kill_children();
pub fn open_logs_dir() -> CmdResult<()> {
let log_dir = wrap_err!(dirs::app_logs_dir())?;
wrap_err!(open::that(log_dir))
}
/// open app config dir
#[tauri::command]
pub fn open_app_dir() -> Result<(), String> {
let app_dir = dirs::app_home_dir();
open_path_cmd(app_dir, "failed to open app dir")
pub fn open_web_url(url: String) -> CmdResult<()> {
wrap_err!(open::that(url))
}
/// open logs dir
#[tauri::command]
pub fn open_logs_dir() -> Result<(), String> {
let log_dir = dirs::app_logs_dir();
open_path_cmd(log_dir, "failed to open logs dir")
}
#[cfg(windows)]
pub mod uwp {
use super::*;
use crate::core::win_uwp;
/// use the os default open command to open file or dir
fn open_path_cmd(path: PathBuf, err_str: &str) -> Result<(), String> {
let result;
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
result = Command::new("explorer")
.creation_flags(0x08000000)
.arg(&path)
.spawn();
}
#[cfg(target_os = "macos")]
{
result = Command::new("open").arg(&path).spawn();
}
#[cfg(target_os = "linux")]
{
result = Command::new("xdg-open").arg(&path).spawn();
}
match result {
Ok(child) => match child.wait_with_output() {
Ok(out) => {
// 退出码不为0 不一定没有调用成功
// 因此仅做warn log且不返回错误
if let Some(code) = out.status.code() {
if code != 0 {
log::warn!("failed to open {:?} (code {})", &path, code);
log::warn!(
"open cmd stdout: {}, stderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr),
);
}
}
}
Err(err) => {
log::error!("failed to open {:?} for {err}", &path);
return Err(err_str.into());
}
},
Err(err) => {
log::error!("failed to open {:?} for {err}", &path);
return Err(err_str.into());
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
wrap_err!(win_uwp::invoke_uwptools().await)
}
}
#[tauri::command]
pub async fn clash_api_get_proxy_delay(
name: String,
url: Option<String>,
) -> CmdResult<clash_api::DelayRes> {
match clash_api::get_proxy_delay(name, url).await {
Ok(res) => Ok(res),
Err(err) => Err(format!("{}", err.to_string())),
}
}
#[cfg(windows)]
pub mod service {
use super::*;
use crate::core::win_service;
#[tauri::command]
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
wrap_err!(win_service::check_service().await)
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
wrap_err!(win_service::install_service().await)
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
wrap_err!(win_service::uninstall_service().await)
}
}
#[cfg(not(windows))]
pub mod service {
use super::*;
#[tauri::command]
pub async fn check_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
Ok(())
}
}
#[cfg(not(windows))]
pub mod uwp {
use super::*;
#[tauri::command]
pub async fn invoke_uwp_tool() -> CmdResult {
Ok(())
}
}
return Ok(());
}
+250
View File
@@ -0,0 +1,250 @@
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 {
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
Ok(map) => Self(Self::guard(map)),
Err(err) => {
log::error!(target: "app", "{err}");
Self::template()
}
}
}
pub fn template() -> Self {
let mut map = Mapping::new();
map.insert("mixed-port".into(), 7897.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());
Self(map)
}
fn guard(mut config: Mapping) -> Mapping {
let port = Self::guard_mixed_port(&config);
let ctrl = Self::guard_server_ctrl(&config);
config.insert("mixed-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)
}
pub fn get_client_info(&self) -> ClashInfo {
let config = &self.0;
ClashInfo {
port: Self::guard_mixed_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,
}),
}
}
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_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 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 {
port,
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>>,
}
+103
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,
}
+127
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));
}
+15
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::*;
+387
View File
@@ -0,0 +1,387 @@
use crate::utils::{dirs, help, 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)]
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>,
/// 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, 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>,
}
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.update_interval = b.update_interval.or(a.update_interval);
Some(a)
}
t @ _ => t.0.or(t.1),
}
}
}
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 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,
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 user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
let mut builder = reqwest::ClientBuilder::new().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 {
match Sysproxy::get_system_proxy() {
Ok(p @ Sysproxy { enable: true, .. }) => {
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 = unsafe { dirs::APP_VERSION };
let version = format!("clash-verge/{version}");
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 = value.to_str().unwrap_or("");
match help::parse_str::<String>(filename, "filename=") {
Some(filename) => Some(filename),
None => 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 => None,
},
}
}
None => None,
};
// parse the profile-update-interval
let option = 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 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,
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,
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,
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")
}
}
+304
View File
@@ -0,0 +1,304 @@
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>>,
/// record valid fields for clash
pub valid: 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
profiles.items.as_mut().map(|items| {
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 {
valid: Some(vec!["dns".into()]),
items: Some(vec![]),
..Self::default()
}
}
pub fn save_file(&self) -> Result<()> {
help::save_yaml(
&dirs::profiles_path()?,
self,
Some("# Profiles Config for Clash Verge"),
)
}
/// 只修改current,valid和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);
}
if let Some(valid) = patch.valid {
self.valid = Some(valid);
}
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![]);
}
self.items.as_mut().map(|items| 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(vec![]);
let mut old_index = None;
let mut new_index = None;
for i in 0..items.len() {
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(vec![]);
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;
// 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(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 _ = 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.len() > 0 {
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 Ok(help::read_merge_mapping(&file_path)?);
}
bail!("failed to find the current profile \"uid:{current}\"");
}
_ => Ok(Mapping::new()),
}
}
}
+31
View File
@@ -0,0 +1,31 @@
use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
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
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());
}
});
}
}
}
+240
View File
@@ -0,0 +1,240 @@
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>,
/// enable blur mode
/// maybe be able to set the alpha
pub theme_blur: Option<bool>,
/// tray click event
pub tray_event: Option<String>,
/// copy env type
pub env_type: 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>,
/// 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 default_latency_test: Option<String>,
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为关闭
pub enable_clash_fields: Option<bool>,
/// 是否使用内部的脚本支持,默认为真
pub enable_builtin_enhanced: Option<bool>,
/// proxy 页面布局 列数
pub proxy_layout_column: Option<i32>,
/// 日志清理
/// 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>>,
/// 是否启用随机端口
pub enable_random_port: Option<bool>,
/// verge mixed port 用于覆盖 clash 的 mixed port
pub verge_mixed_port: Option<u16>,
}
#[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()),
theme_blur: Some(false),
traffic_graph: Some(true),
enable_memory_usage: Some(true),
enable_auto_launch: Some(false),
enable_silent_start: Some(false),
enable_system_proxy: Some(false),
enable_random_port: Some(false),
verge_mixed_port: Some(7897),
enable_proxy_guard: Some(false),
proxy_guard_duration: Some(30),
auto_close_connection: Some(true),
enable_builtin_enhanced: Some(true),
enable_clash_fields: Some(false),
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!(theme_blur);
patch!(tray_event);
patch!(env_type);
patch!(traffic_graph);
patch!(enable_memory_usage);
patch!(enable_tun_mode);
patch!(enable_service_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_random_port);
patch!(verge_mixed_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!(default_latency_test);
patch!(enable_builtin_enhanced);
patch!(proxy_layout_column);
patch!(enable_clash_fields);
patch!(auto_log_clean);
patch!(window_size_position);
}
/// 在初始化前尝试拿到单例端口的值
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
}
}
}
-455
View File
@@ -1,455 +0,0 @@
use super::{PrfEnhancedResult, Profiles, Verge};
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, false)
}
/// 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(())
}
/// enable tun mode
/// only revise the config and restart the
pub fn tun_mode(&mut self, enable: bool) -> Result<()> {
// Windows 需要wintun.dll文件
#[cfg(target_os = "windows")]
if enable {
let wintun_dll = dirs::app_home_dir().join("wintun.dll");
if !wintun_dll.exists() {
bail!("failed to enable TUN for missing `wintun.dll`");
}
}
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 = self.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);
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!(self.config, "tun", new_tun);
// dns config
let dns_val = self.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);
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!(self.config, "dns", new_dns);
self.save_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 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
/// only change the enhanced profiles
pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool) -> Result<()> {
if self.window.is_none() {
bail!("failed to get the main window");
}
let win = self.window.clone().unwrap();
let event_name = help::get_uid("e");
let event_name = format!("enhanced-cb-{event_name}");
let info = self.info.clone();
let mut config = self.config.clone();
// generate the payload
let payload = profiles.gen_enhanced(event_name.clone())?;
let window = self.window.clone();
win.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 {
// all of these can not be revised by script
// 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",
];
for (key, value) in data.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) {
config.insert(Value::String(key_str), value);
}
});
}
log::info!("profile enhanced status {}", result.status);
Self::_activate(info, config, window).unwrap();
}
if let Some(error) = result.error {
log::error!("{error}");
}
}
});
tauri::async_runtime::spawn(async move {
// wait the window setup during resolve app
if delay {
sleep(Duration::from_secs(2)).await;
}
win.emit("script-handler", payload).unwrap();
});
Ok(())
}
/// activate the profile
/// auto activate enhanced profile
pub fn activate(&self, profiles: &Profiles, delay: bool) -> Result<()> {
let gen_map = profiles.gen_activate()?;
let info = self.info.clone();
let mut config = self.config.clone();
for (key, value) in gen_map.into_iter() {
config.insert(key, value);
}
Self::_activate(info, config, self.window.clone())?;
self.activate_enhanced(profiles, delay)
}
}
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}");
}
}
}
+141
View File
@@ -0,0 +1,141 @@
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>) -> 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", "10000"), ("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();
}
return 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);
}
+327
View File
@@ -0,0 +1,327 @@
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, PidExt, ProcessExt, System, SystemExt};
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();
system.process(Pid::from_u32(pid)).map(|proc| {
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.len() > 0 {
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,
};
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(super::win_service::stop_core_by_service().await);
should_kill = true;
}
// 这里得等一会儿
if should_kill {
sleep(Duration::from_millis(500)).await;
}
#[cfg(target_os = "windows")]
{
use super::win_service;
// 服务模式
let enable = { Config::verge().latest().enable_service_mode.clone() };
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 {
win_service::check_service().await?;
win_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)?;
// fix #212
let args = match clash_core.as_str() {
"clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
"clash-meta-alpha" => vec!["-m", "-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<()> {
// 服务模式不管
#[cfg(target_os = "windows")]
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<()> {
#[cfg(target_os = "windows")]
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move {
log_err!(super::win_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; 3] = ["clash", "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(())
}
}
+77
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()
.map_or(None, |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(())
}
}
+165
View File
@@ -0,0 +1,165 @@
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_dashboard" => || feat::open_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()),
"clash_mode_script" => || feat::change_clash_mode("script".into()),
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"enable_system_proxy" => || feat::enable_system_proxy(),
"disable_system_proxy" => || feat::disable_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(),
"enable_tun_mode" => || feat::enable_tun_mode(),
"disable_tun_mode" => || feat::disable_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<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a 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();
}
}
}
+36
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();
}
}
+82
View File
@@ -0,0 +1,82 @@
use std::borrow::Cow;
/// 给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 = {
// the path of clash /Applications/Clash Verge.app/Contents/MacOS/clash
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
// let path = escape(&path);
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=+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}");
}
}
#[allow(unused)]
pub fn escape<'a>(text: &'a str) -> Cow<'a, str> {
let bytes = text.as_bytes();
let mut owned = None;
for pos in 0..bytes.len() {
let special = match bytes[pos] {
b' ' => Some(b' '),
_ => None,
};
if let Some(s) = special {
if owned.is_none() {
owned = Some(bytes[0..pos].to_owned());
}
owned.as_mut().unwrap().push(b'\\');
owned.as_mut().unwrap().push(b'\\');
owned.as_mut().unwrap().push(s);
} else if let Some(owned) = owned.as_mut() {
owned.push(bytes[pos]);
}
}
if let Some(owned) = owned {
unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) }
} else {
unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) }
}
}
+12 -6
View File
@@ -1,7 +1,13 @@
mod clash;
mod profiles;
mod verge;
pub mod clash_api;
mod core;
pub mod handle;
pub mod hotkey;
pub mod logger;
pub mod manager;
pub mod sysopt;
pub mod timer;
pub mod tray;
pub mod win_service;
pub mod win_uwp;
pub use self::clash::*;
pub use self::profiles::*;
pub use self::verge::*;
pub use self::core::*;
-670
View File
@@ -1,670 +0,0 @@
use crate::utils::{config, dirs, help, tmpl};
use anyhow::{bail, Context, Result};
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Value};
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()),
})
}
}
///
/// ## 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());
}
let mut new_config = Mapping::new();
let def_config = config::read_yaml::<Mapping>(file_path.clone());
// Only the following fields are allowed:
// proxies/proxy-providers/proxy-groups/rule-providers/rules
let valid_keys = vec![
"proxies",
"proxy-providers",
"proxy-groups",
"rule-providers",
"rules",
];
for (key, value) in def_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();
if valid_keys.contains(&&*key_str) {
new_config.insert(Value::String(key_str), value);
}
});
}
return Ok(new_config);
}
}
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 {
current: Mapping,
chain: Vec<PrfData>,
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,
}
}
}
+313
View File
@@ -0,0 +1,313 @@
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::sync::Arc;
use sysproxy::Sysproxy;
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
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.clone().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().map_or(None, |p| Some(p));
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.clone().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());
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 enable = { Config::verge().latest().enable_auto_launch.clone() };
let enable = enable.unwrap_or(false);
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()?;
// 避免在开发时将自启动关了
#[cfg(feature = "verge-dev")]
if !enable {
return Ok(());
}
#[cfg(target_os = "macos")]
{
if enable && !auto.is_enabled().unwrap_or(false) {
// 避免重复设置登录项
let _ = auto.disable();
auto.enable()?;
} else if !enable {
let _ = auto.disable();
}
}
#[cfg(not(target_os = "macos"))]
if enable {
auto.enable()?;
}
*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.clone() };
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.clone().unwrap_or(false),
verge.enable_proxy_guard.clone().unwrap_or(false),
verge.proxy_guard_duration.clone().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
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();
Config::profiles().latest().get_items().map(|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),
}
+219
View File
@@ -0,0 +1,219 @@
use crate::{cmds, config::Config, feat, utils::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");
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 mut indication_icon = if *system_proxy {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-sys.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-sys.png").to_vec();
icon
} else {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/icon.png").to_vec();
icon
};
if *tun_mode {
#[cfg(not(target_os = "macos"))]
let icon = include_bytes!("../../icons/tray-icon-tun.png").to_vec();
#[cfg(target_os = "macos")]
let icon = include_bytes!("../../icons/mac-tray-icon-tun.png").to_vec();
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);
let switch_map = {
let mut map = std::collections::HashMap::new();
map.insert(true, "on");
map.insert(false, "off");
map
};
let _ = tray.set_tooltip(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}",
t!("System Proxy", "系统代理"),
switch_map[system_proxy],
t!("TUN Mode", "Tun 模式"),
switch_map[tun_mode]
));
Ok(())
}
pub fn on_left_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 {
SystemTrayEvent::LeftClick { .. } => Tray::on_left_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" => {
let _ = resolve::save_window_size_position(app_handle, true);
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
std::process::exit(0);
}
_ => {}
},
_ => {}
}
}
}
-326
View File
@@ -1,326 +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>,
/// 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>,
}
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;
}
// 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;
});
}
}
+178
View File
@@ -0,0 +1,178 @@
#![cfg(target_os = "windows")]
use crate::config::Config;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep;
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弹窗阻塞主线程
pub async fn install_service() -> Result<()> {
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(())
}
/// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
pub async fn uninstall_service() -> Result<()> {
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(())
}
/// 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 clash_bin = format!("{clash_core}.exe");
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(())
}
+27
View File
@@ -0,0 +1,27 @@
#![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 binary_path = dirs::service_path()?;
let tool_path = binary_path.with_file_name("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(())
}
@@ -0,0 +1,6 @@
function main(params) {
if (params.mode === "script") {
params.mode = "rule";
}
return params;
}
@@ -0,0 +1,10 @@
function main(params) {
if (Array.isArray(params.proxies)) {
params.proxies.forEach((p, i) => {
if (p.type === "hysteria" && typeof p.alpn === "string") {
params.proxies[i].alpn = [p.alpn];
}
});
}
return params;
}
+101
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,
}
}
}
+156
View File
@@ -0,0 +1,156 @@
use serde_yaml::{Mapping, Value};
use std::collections::HashSet;
pub const HANDLE_FIELDS: [&str; 9] = [
"mode",
"port",
"socks-port",
"mixed-port",
"allow-lan",
"log-level",
"ipv6",
"secret",
"external-controller",
];
pub const DEFAULT_FIELDS: [&str; 5] = [
"proxies",
"proxy-groups",
"proxy-providers",
"rules",
"rule-providers",
];
pub const OTHERS_FIELDS: [&str; 31] = [
"dns",
"tun",
"ebpf",
"hosts",
"script",
"profile",
"payload",
"tunnels",
"auto-redir",
"experimental",
"interface-name",
"routing-mark",
"redir-port",
"tproxy-port",
"iptables",
"external-ui",
"bind-address",
"authentication",
"tls", // meta
"sniffer", // meta
"geox-url", // meta
"listeners", // meta
"sub-rules", // meta
"geodata-mode", // meta
"unified-delay", // meta
"tcp-concurrent", // meta
"enable-process", // meta
"find-process-mode", // meta
"skip-auth-prefixes", // meta
"external-controller-tls", // meta
"global-client-fingerprint", // meta
];
pub fn use_clash_fields() -> Vec<String> {
DEFAULT_FIELDS
.into_iter()
.chain(HANDLE_FIELDS)
.chain(OTHERS_FIELDS)
.map(|s| s.to_string())
.collect()
}
pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
let others = Vec::from(OTHERS_FIELDS);
valid.iter_mut().for_each(|s| s.make_ascii_lowercase());
valid
.into_iter()
.filter(|s| others.contains(&s.as_str()))
.chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))
.collect()
}
pub fn use_filter(config: Mapping, filter: &Vec<String>, enable: bool) -> Mapping {
if !enable {
return config;
}
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, enable_filter: bool) -> Mapping {
let mut ret = Mapping::new();
HANDLE_FIELDS
.into_iter()
.chain(OTHERS_FIELDS)
.chain(DEFAULT_FIELDS)
.for_each(|key| {
let key = Value::from(key);
config.get(&key).map(|value| {
ret.insert(key, value.clone());
});
});
if !enable_filter {
let supported_keys: HashSet<&str> = HANDLE_FIELDS
.into_iter()
.chain(OTHERS_FIELDS)
.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);
config.get(&key).map(|value| {
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();
return s;
})
.collect()
}
+92
View File
@@ -0,0 +1,92 @@
use super::{use_filter, use_lowercase};
use serde_yaml::{self, Mapping, Sequence, Value};
const MERGE_FIELDS: [&str; 6] = [
"prepend-rules",
"append-rules",
"prepend-proxies",
"append-proxies",
"prepend-proxy-groups",
"append-proxy-groups",
];
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
// 直接覆盖原字段
use_lowercase(merge.clone())
.into_iter()
.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(), true);
["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());
}
}
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(())
}
+126
View File
@@ -0,0 +1,126 @@
mod chain;
mod field;
mod merge;
mod script;
mod tun;
pub(self) use self::field::*;
use self::chain::*;
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, enable_filter) = {
let verge = Config::verge();
let verge = verge.latest();
(
verge.clash_core.clone(),
verge.enable_tun_mode.clone().unwrap_or(false),
verge.enable_builtin_enhanced.clone().unwrap_or(true),
verge.enable_clash_fields.clone().unwrap_or(true),
)
};
// 从profiles里拿东西
let (mut config, chain, valid) = {
let profiles = Config::profiles();
let profiles = profiles.latest();
let current = profiles.current_mapping().unwrap_or(Mapping::new());
let chain = match profiles.chain.as_ref() {
Some(chain) => chain
.iter()
.filter_map(|uid| profiles.get_item(uid).ok())
.filter_map(|item| <Option<ChainItem>>::from(item))
.collect::<Vec<ChainItem>>(),
None => vec![],
};
let valid = profiles.valid.clone().unwrap_or(vec![]);
(current, chain, valid)
};
let mut result_map = HashMap::new(); // 保存脚本日志
let mut exists_keys = use_keys(&config); // 保存出现过的keys
let valid = use_valid_fields(valid);
config = use_filter(config, &valid, enable_filter);
// 处理用户的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());
config = use_filter(config.to_owned(), &valid, enable_filter);
}
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 = use_filter(res_config, &valid, enable_filter);
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() {
config.insert(key, value);
}
let clash_fields = use_clash_fields();
// 内建脚本最后跑
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 = use_filter(res_config, &clash_fields, enable_filter);
}
Err(err) => {
log::error!(target: "app", "builtin script error `{err}`");
}
},
_ => {}
}
});
}
config = use_filter(config, &clash_fields, enable_filter);
config = use_tun(config, enable_tun);
config = use_sort(config, enable_filter);
let mut exists_set = HashSet::new();
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
exists_keys = exists_set.into_iter().collect();
(config, exists_keys, result_map)
}
+94
View File
@@ -0,0 +1,94 @@
use super::use_lowercase;
use anyhow::Result;
use serde_yaml::Mapping;
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
use rquickjs::{function::Func, Context, Runtime};
use std::sync::{Arc, Mutex};
let runtime = Runtime::new().unwrap();
let context = Context::full(&runtime).unwrap();
let outputs = Arc::new(Mutex::new(vec![]));
let copy_outputs = outputs.clone();
let result = context.with(|ctx| -> Result<Mapping> {
ctx.globals().set(
"__verge_log__",
Func::from(move |level: String, data: String| {
let mut out = copy_outputs.lock().unwrap();
out.push((level, data));
}),
)?;
ctx.eval(
r#"var console = Object.freeze({
log(data){__verge_log__("log",JSON.stringify(data))},
info(data){__verge_log__("info",JSON.stringify(data))},
error(data){__verge_log__("error",JSON.stringify(data))},
debug(data){__verge_log__("debug",JSON.stringify(data))},
});"#,
)?;
let config = use_lowercase(config.clone());
let config_str = serde_json::to_string(&config)?;
let code = format!(
r#"try{{
{script};
JSON.stringify(main({config_str})||'')
}} catch(err) {{
`__error_flag__ ${{err.toString()}}`
}}"#
);
let result: String = ctx.eval(code.as_str())?;
if result.starts_with("__error_flag__") {
anyhow::bail!(result[15..].to_owned());
}
if result == "\"\"" {
anyhow::bail!("main function should return object");
}
return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
});
let mut out = outputs.lock().unwrap();
match result {
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
Err(err) => {
out.push(("exception".into(), err.to_string()));
Ok((config, out.to_vec()))
}
}
}
#[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);
}
+81
View File
@@ -0,0 +1,81 @@
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);
if enable {
append!(tun_val, "stack", "gvisor");
append!(tun_val, "dns-hijack", vec!["any:53"]);
append!(tun_val, "auto-route", true);
append!(tun_val, "auto-detect-interface", true);
}
revise!(config, "tun", tun_val);
if enable {
use_dns_for_tun(config)
} else {
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
}
+370
View File
@@ -0,0 +1,370 @@
//!
//! 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};
// 打开面板
pub fn open_dashboard() {
let handle = handle::Handle::global();
let app_handle = handle.app_handle.lock();
if let Some(app_handle) = app_handle.as_ref() {
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.clone();
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}"),
}
});
}
// 打开系统代理
pub fn enable_system_proxy() {
tauri::async_runtime::spawn(async {
match patch_verge(IVerge {
enable_system_proxy: Some(true),
..IVerge::default()
})
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
// 关闭系统代理
pub fn disable_system_proxy() {
tauri::async_runtime::spawn(async {
match patch_verge(IVerge {
enable_system_proxy: Some(false),
..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.clone();
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}"),
}
});
}
// 打开tun模式
pub fn enable_tun_mode() {
tauri::async_runtime::spawn(async {
match patch_verge(IVerge {
enable_tun_mode: Some(true),
..IVerge::default()
})
.await
{
Ok(_) => handle::Handle::refresh_verge(),
Err(err) => log::error!(target: "app", "{err}"),
}
});
}
// 关闭tun模式
pub fn disable_tun_mode() {
tauri::async_runtime::spawn(async {
match patch_verge(IVerge {
enable_tun_mode: Some(false),
..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 mixed_port = patch.get("mixed-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.clone().unwrap()
!= Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
// 检查端口占用
if changed {
if let Some(port) = mixed_port.clone().unwrap().as_u64() {
if !port_scanner::local_port_available(port as u16) {
Config::clash().discard();
bail!("port already in use");
}
}
}
};
// 激活订阅
if mixed_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;
match {
#[cfg(target_os = "windows")]
{
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?;
}
}
#[cfg(not(target_os = "windows"))]
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() {
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.or(tun_mode).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} \n set 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}"),
};
}
+127 -137
View File
@@ -1,158 +1,148 @@
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod cmds;
mod config;
mod core;
mod states;
mod enhance;
mod feat;
mod utils;
use crate::{
core::VergeConfig,
utils::{resolve, server},
};
use tauri::{
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
};
use crate::utils::{init, resolve, server};
use tauri::{api, SystemTray};
fn main() -> std::io::Result<()> {
if server::check_singleton().is_err() {
println!("app exists");
return Ok(());
}
// 单例检测
if server::check_singleton().is_err() {
println!("app exists");
return Ok(());
}
let tray_menu = SystemTrayMenu::new()
.add_item(CustomMenuItem::new("open_window", "Show"))
.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"));
crate::log_err!(init::init_config());
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.manage(states::VergeState::default())
.manage(states::ClashState::default())
.manage(states::ProfilesState::default())
.setup(|app| Ok(resolve::resolve_setup(app)))
.system_tray(SystemTray::new().with_menu(tray_menu))
.on_system_tray_event(move |app_handle, event| match event {
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
"open_window" => {
let window = app_handle.get_window("main").unwrap();
window.unminimize().unwrap();
window.show().unwrap();
window.set_focus().unwrap();
#[allow(unused_mut)]
let mut builder = tauri::Builder::default()
.system_tray(SystemTray::new())
.setup(|app| Ok(resolve::resolve_setup(app)))
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
.invoke_handler(tauri::generate_handler![
// common
cmds::get_sys_proxy,
cmds::open_app_dir,
cmds::open_logs_dir,
cmds::open_web_url,
cmds::open_core_dir,
// 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::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" => {
let verge_state = app_handle.state::<states::VergeState>();
let mut verge = verge_state.0.lock().unwrap();
tauri::RunEvent::Exit => {
resolve::resolve_reset();
api::process::kill_children();
app_handle.exit(0);
}
tauri::RunEvent::Updater(tauri::UpdaterEvent::Downloaded) => {
resolve::resolve_reset();
api::process::kill_children();
}
#[cfg(target_os = "macos")]
tauri::RunEvent::WindowEvent { label, event, .. } => {
use tauri::Manager;
let old_value = verge.config.enable_system_proxy.clone().unwrap_or(false);
let new_value = !old_value;
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
api.prevent_close();
let _ = resolve::save_window_size_position(&app_handle, true);
match verge.patch_config(VergeConfig {
enable_system_proxy: Some(new_value),
..VergeConfig::default()
}) {
Ok(_) => {
app_handle
.tray_handle()
.get_item(id.as_str())
.set_selected(new_value)
.unwrap();
// update verge config
let window = app_handle.get_window("main").unwrap();
window.emit("verge://refresh-verge-config", "yes").unwrap();
app_handle.get_window("main").map(|win| {
let _ = win.hide();
});
}
_ => {}
}
}
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(not(target_os = "macos"))]
tauri::RunEvent::WindowEvent { label, event, .. } => {
if label == "main" {
match event {
tauri::WindowEvent::CloseRequested { .. } => {
let _ = resolve::save_window_size_position(&app_handle, true);
}
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
let _ = resolve::save_window_size_position(&app_handle, false);
}
_ => {}
}
}
}
_ => {}
},
#[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
]);
#[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::CloseRequested { label, 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(())
}
-11
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>>);
-23
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}\""))
}
+128 -28
View File
@@ -1,57 +1,157 @@
use std::env::temp_dir;
use std::path::{Path, PathBuf};
use anyhow::Result;
use std::path::PathBuf;
use tauri::{
api::path::{home_dir, resource_dir},
Env, PackageInfo,
api::path::{data_dir, resource_dir},
Env, PackageInfo,
};
#[cfg(not(feature = "verge-dev"))]
static APP_DIR: &str = "clash-verge";
static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev";
#[cfg(feature = "verge-dev")]
static APP_DIR: &str = "clash-verge-dev";
static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev";
static CLASH_CONFIG: &str = "config.yaml";
static VERGE_CONFIG: &str = "verge.yaml";
static PROFILE_YAML: &str = "profiles.yaml";
static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
static mut RESOURCE_DIR: Option<PathBuf> = None;
static mut APP_HOME_DIR: Option<PathBuf> = None;
/// portable flag
#[allow(unused)]
static mut PORTABLE_FLAG: bool = false;
pub static mut APP_VERSION: &str = "v1.2.0";
/// initialize portable flag
#[cfg(target_os = "windows")]
pub unsafe fn init_portable_flag() -> Result<()> {
use tauri::utils::platform::current_exe;
let exe = current_exe()?;
if let Some(dir) = exe.parent() {
let dir = PathBuf::from(dir).join(".config/PORTABLE");
if dir.exists() {
PORTABLE_FLAG = true;
}
}
Ok(())
}
/// get the verge app home dir
pub fn app_home_dir() -> PathBuf {
home_dir()
.unwrap()
.join(Path::new(".config"))
.join(Path::new(APP_DIR))
pub fn app_home_dir() -> Result<PathBuf> {
use tauri::utils::platform::current_exe;
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"))?;
let portable_home_dir = PathBuf::from(app_dir).join(".config").join(APP_ID);
let home_dir = data_dir()
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
.join(APP_ID);
unsafe {
if PORTABLE_FLAG {
APP_HOME_DIR = Some(portable_home_dir.clone());
Ok(portable_home_dir)
} else {
APP_HOME_DIR = Some(home_dir.clone());
Ok(home_dir)
}
}
}
/// get the resources dir
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
resource_dir(package_info, &Env::default())
.unwrap()
.join("resources")
pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
let res_dir = resource_dir(package_info, &Env::default())
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
.join("resources");
unsafe {
RESOURCE_DIR = Some(res_dir.clone());
let ver = package_info.version.to_string();
let ver_str = format!("v{ver}");
APP_VERSION = Box::leak(Box::new(ver_str));
}
Ok(res_dir)
}
/// profiles dir
pub fn app_profiles_dir() -> PathBuf {
app_home_dir().join("profiles")
pub fn app_profiles_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("profiles"))
}
/// logs dir
pub fn app_logs_dir() -> PathBuf {
app_home_dir().join("logs")
pub fn app_logs_dir() -> Result<PathBuf> {
Ok(app_home_dir()?.join("logs"))
}
pub fn clash_path() -> PathBuf {
app_home_dir().join(CLASH_CONFIG)
pub fn clash_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join(CLASH_CONFIG))
}
pub fn verge_path() -> PathBuf {
app_home_dir().join(VERGE_CONFIG)
pub fn verge_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join(VERGE_CONFIG))
}
pub fn profiles_path() -> PathBuf {
app_home_dir().join(PROFILE_YAML)
pub fn profiles_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join(PROFILE_YAML))
}
pub fn profiles_temp_path() -> PathBuf {
temp_dir().join(PROFILE_TEMP)
#[allow(unused)]
pub fn app_res_dir() -> Result<PathBuf> {
unsafe {
Ok(RESOURCE_DIR
.clone()
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?)
}
}
pub fn clash_pid_path() -> Result<PathBuf> {
unsafe {
Ok(APP_HOME_DIR
.clone()
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
.join("clash.pid"))
}
}
#[cfg(windows)]
pub fn service_path() -> Result<PathBuf> {
unsafe {
let res_dir = RESOURCE_DIR
.clone()
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?;
Ok(res_dir.join("clash-verge-service.exe"))
}
}
#[cfg(windows)]
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)
}
+142 -58
View File
@@ -1,94 +1,178 @@
use anyhow::{anyhow, bail, Context, Result};
use nanoid::nanoid;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{de::DeserializeOwned, Serialize};
use serde_yaml::{Mapping, Value};
use std::{fs, path::PathBuf, str::FromStr};
use tauri::{
api::shell::{open, Program},
Manager,
};
pub fn get_now() -> usize {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as _
/// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
if !path.exists() {
bail!("file not found \"{}\"", path.display());
}
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] = [
'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',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z',
'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',
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z',
];
/// generate the uid
pub fn get_uid(prefix: &str) -> String {
let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}")
let id = nanoid!(11, &ALPHABET);
format!("{prefix}{id}")
}
/// parse the string
/// xxx=123123; => 123123
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
match target.find(key) {
Some(idx) => {
let idx = idx + key.len();
let value = &target[idx..];
match match value.split(';').nth(0) {
Some(value) => value.trim().parse(),
None => value.trim().parse(),
} {
Ok(r) => Some(r),
Err(_) => None,
}
}
None => None,
}
target.find(key).and_then(|idx| {
let idx = idx + key.len();
let value = &target[idx..];
match value.split(';').nth(0) {
Some(value) => value.trim().parse(),
None => value.trim().parse(),
}
.ok()
})
}
/// 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_rules! log_if_err {
($result: expr) => {
if let Err(err) = $result {
log::error!("{err}");
macro_rules! error {
($result: expr) => {
log::error!(target: "app", "{}", $result);
};
}
#[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
/// transform the error to String
#[macro_export]
macro_rules! wrap_err {
($stat: expr) => {
match $stat {
Ok(a) => Ok(a),
Err(err) => {
log::error!("{}", err.to_string());
Err(format!("{}", err.to_string()))
}
}
};
($stat: expr) => {
match $stat {
Ok(a) => Ok(a),
Err(err) => {
log::error!(target: "app", "{}", err.to_string());
Err(format!("{}", err.to_string()))
}
}
};
}
/// return the string literal error
#[macro_export]
macro_rules! ret_err {
($str: expr) => {
return Err($str.into())
};
($str: expr) => {
return Err($str.into())
};
}
#[test]
fn test_parse_value() {
let test_1 = "upload=111; download=2222; total=3333; expire=444";
let test_2 = "attachment; filename=Clash.yaml";
let test_1 = "upload=111; download=2222; total=3333; expire=444";
let test_2 = "attachment; filename=Clash.yaml";
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, "total=").unwrap(), 3333);
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
assert_eq!(
parse_str::<String>(test_2, "filename=").unwrap(),
format!("Clash.yaml")
);
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, "total=").unwrap(), 3333);
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
assert_eq!(
parse_str::<String>(test_2, "filename=").unwrap(),
format!("Clash.yaml")
);
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, "expire1="), None);
assert_eq!(parse_str::<usize>(test_2, "attachment="), 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, "expire1="), None);
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
}
+234 -91
View File
@@ -1,100 +1,243 @@
use crate::utils::{dirs, tmpl};
use chrono::Local;
use crate::config::*;
use crate::utils::{dirs, help};
use anyhow::Result;
use chrono::{Local, TimeZone};
use log::LevelFilter;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Root};
use log4rs::config::{Appender, Logger, Root};
use log4rs::encode::pattern::PatternEncoder;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::fs::{self, DirEntry};
use std::str::FromStr;
use tauri::PackageInfo;
/// initialize this instance's log file
fn init_log(log_dir: &PathBuf) {
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
let log_file = format!("{}.log", local_time);
let log_file = log_dir.join(log_file);
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();
fn init_log() -> Result<()> {
let log_dir = dirs::app_logs_dir()?;
if !log_dir.exists() {
let _ = fs::create_dir_all(&log_dir);
}
}
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.clone().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)? {
if let Ok(file) = file {
let _ = process_file(file);
}
}
Ok(())
}
/// Initialize all the config files
/// before tauri setup
pub fn init_config() -> Result<()> {
#[cfg(target_os = "windows")]
unsafe {
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(package_info: &PackageInfo) -> Result<()> {
let app_dir = dirs::app_home_dir()?;
let res_dir = dirs::app_resources_dir(package_info)?;
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(())
}
+1 -2
View File
@@ -1,8 +1,7 @@
pub mod config;
pub mod dirs;
pub mod help;
pub mod init;
pub mod resolve;
pub mod server;
pub mod sysopt;
pub mod tmpl;
// mod winhelp;
+195 -66
View File
@@ -1,88 +1,217 @@
use super::{init, server};
use crate::{core::Profiles, log_if_err, states};
use crate::config::IVerge;
use crate::{config::Config, core::*, utils::init, utils::server};
use crate::{log_err, trace_err};
use anyhow::Result;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::{App, AppHandle, Manager};
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
pub fn resolve_setup(app: &App) {
resolve_window(app);
pub fn resolve_setup(app: &mut App) {
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
// setup a simple http server for singleton
server::embed_server(&app.handle());
handle::Handle::global().init(app.app_handle());
// init app config
init::init_app(app.package_info());
log_err!(init::init_resources(app.package_info()));
// init states
let clash_state = app.state::<states::ClashState>();
let verge_state = app.state::<states::VergeState>();
let profiles_state = app.state::<states::ProfilesState>();
// 处理随机端口
let enable_random_port = Config::verge().latest().enable_random_port.unwrap_or(false);
let mut clash = clash_state.0.lock().unwrap();
let mut verge = verge_state.0.lock().unwrap();
let mut profiles = profiles_state.0.lock().unwrap();
let mut port = Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port());
log_if_err!(clash.run_sidecar());
if enable_random_port {
port = find_unused_port().unwrap_or(
Config::verge()
.latest()
.verge_mixed_port
.unwrap_or(Config::clash().data().get_mixed_port()),
);
}
*profiles = Profiles::read_file();
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();
clash.set_window(app.get_window("main"));
log_if_err!(clash.activate(&profiles, true));
// 启动核心
log::trace!("init config");
log_err!(Config::init_config());
verge.init_sysproxy(clash.info.port.clone());
// enable tun mode
if verge.config.enable_tun_mode.clone().unwrap_or(false)
&& verge.cur_sysproxy.is_some()
&& verge.cur_sysproxy.as_ref().unwrap().enable
{
log::info!("enable tun mode");
clash.tun_mode(true).unwrap();
}
log::trace!("launch core");
log_err!(CoreManager::global().init());
log_if_err!(verge.init_launch());
// setup a simple http server for singleton
log::trace!("launch embed server");
server::embed_server(app.app_handle());
verge.config.enable_system_proxy.map(|enable| {
log_if_err!(app
.tray_handle()
.get_item("system_proxy")
.set_selected(enable));
});
log::trace!("init system tray");
log_err!(tray::Tray::update_systray(&app.app_handle()));
let silent_start = { Config::verge().data().enable_silent_start.clone() };
if !silent_start.unwrap_or(false) {
create_window(&app.app_handle());
}
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());
}
/// reset system proxy
pub fn resolve_reset(app_handle: &AppHandle) {
let verge_state = app_handle.state::<states::VergeState>();
let mut verge = verge_state.0.lock().unwrap();
verge.reset_sysproxy();
pub fn resolve_reset() {
log_err!(sysopt::Sysopt::global().reset_sysproxy());
log_err!(CoreManager::global().stop_core());
}
/// customize the window theme
fn resolve_window(app: &App) {
let window = app.get_window("main").unwrap();
/// create main window
pub fn create_window(app_handle: &AppHandle) {
if let Some(window) = app_handle.get_window("main") {
trace_err!(window.unminimize(), "set win unminimize");
trace_err!(window.show(), "set win visible");
trace_err!(window.set_focus(), "set win focus");
return;
}
#[cfg(target_os = "windows")]
{
use window_shadows::set_shadow;
use window_vibrancy::apply_blur;
let mut builder = tauri::window::WindowBuilder::new(
app_handle,
"main".to_string(),
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();
set_shadow(&window, true).unwrap();
apply_blur(&window, None).unwrap();
}
match Config::verge().latest().window_size_position.clone() {
Some(size_pos) if size_pos.len() == 4 => {
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")]
{
use tauri::LogicalSize;
use tauri::Size::Logical;
window.set_decorations(true).unwrap();
window
.set_size(Logical(LogicalSize {
width: 800.0,
height: 610.0,
}))
.unwrap();
// use tauri_plugin_vibrancy::MacOSVibrancy;
// #[allow(deprecated)]
// window.apply_vibrancy(MacOSVibrancy::AppearanceBased);
}
#[cfg(target_os = "macos")]
{
builder = builder.inner_size(800.0, 642.0).center();
}
#[cfg(target_os = "linux")]
{
builder = builder.inner_size(800.0, 642.0).center();
}
}
};
#[cfg(target_os = "windows")]
{
use window_shadows::set_shadow;
match builder
.decorations(false)
.transparent(true)
.visible(false)
.build()
{
Ok(win) => {
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).try_into()?
|| pos.y < -200
|| pos.y > (size.height - 200).try_into()?
{
center = true;
}
Ok(center)
})();
if center.unwrap_or(true) {
trace_err!(win.center(), "set win center");
}
log::trace!("try to create window");
let app_handle = app_handle.clone();
if let Some(window) = app_handle.get_window("main") {
trace_err!(set_shadow(&window, true), "set win shadow");
} else {
log::error!(target: "app", "failed to create window, get_window is None")
}
}
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
}
}
#[cfg(target_os = "macos")]
crate::log_err!(builder
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build());
#[cfg(target_os = "linux")]
crate::log_err!(builder.decorations(true).transparent(false).build());
}
/// save window size and position
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
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 verge = Config::verge();
let mut verge = verge.latest();
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
if save_to_file {
verge.save_file()?;
}
Ok(())
}
+31 -28
View File
@@ -1,41 +1,44 @@
extern crate warp;
use super::resolve;
use crate::config::IVerge;
use anyhow::{bail, Result};
use port_scanner::local_port_available;
use tauri::{AppHandle, Manager};
use tauri::AppHandle;
use warp::Filter;
#[cfg(not(feature = "verge-dev"))]
const SERVER_PORT: u16 = 33333;
#[cfg(feature = "verge-dev")]
const SERVER_PORT: u16 = 11233;
/// check whether there is already exists
pub fn check_singleton() -> Result<(), ()> {
if !local_port_available(SERVER_PORT) {
tauri::async_runtime::block_on(async {
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT);
reqwest::get(url).await.unwrap();
Err(())
})
} else {
Ok(())
}
pub fn check_singleton() -> Result<()> {
let port = IVerge::get_singleton_port();
if !local_port_available(port) {
tauri::async_runtime::block_on(async {
let url = format!("http://127.0.0.1:{port}/commands/visible");
let resp = reqwest::get(url).await?.text().await?;
if &resp == "ok" {
bail!("app exists");
}
log::error!("failed to setup singleton listen server");
Ok(())
})
} else {
Ok(())
}
}
/// The embed server only be used to implement singleton process
/// maybe it can be used as pac server later
pub fn embed_server(app: &AppHandle) {
let window = app.get_window("main").unwrap();
pub fn embed_server(app_handle: AppHandle) {
let port = IVerge::get_singleton_port();
tauri::async_runtime::spawn(async move {
let commands = warp::path!("commands" / "visible").map(move || {
window.show().unwrap();
window.set_focus().unwrap();
return format!("ok");
tauri::async_runtime::spawn(async move {
let commands = warp::path!("commands" / "visible").map(move || {
resolve::create_window(&app_handle);
format!("ok")
});
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
});
warp::serve(commands)
.bind(([127, 0, 0, 1], SERVER_PORT))
.await;
});
}
-366
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}"))
}
}
+2 -36
View File
@@ -1,37 +1,5 @@
///! 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
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
@@ -60,10 +28,8 @@ append-proxy-groups:
";
/// enhanced profile
pub const ITEM_SCRIPT: &str = "// Should define the `main` function
// The argument to this function is the clash config
// or the result of the previous handler
// so you should return the config after processing
pub const ITEM_SCRIPT: &str = "// Define the `main` function
function main(params) {
return params;
}
+69
View File
@@ -0,0 +1,69 @@
#![cfg(target_os = "windows")]
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
//!
//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs
//!
use windows_sys::Win32::{
Foundation::*,
System::{LibraryLoader::*, SystemInformation::*},
};
fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {
assert_eq!(library.chars().last(), Some('\0'));
assert_eq!(function.chars().last(), Some('\0'));
let module = unsafe { LoadLibraryA(library.as_ptr()) };
if module == 0 {
return None;
}
Some(unsafe { GetProcAddress(module, function.as_ptr()) })
}
macro_rules! get_function {
($lib:expr, $func:ident) => {
get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe {
std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f)
})
};
}
/// Returns a tuple of (major, minor, buildnumber)
fn get_windows_ver() -> Option<(u32, u32, u32)> {
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32;
let handle = get_function!("ntdll.dll", RtlGetVersion);
if let Some(rtl_get_version) = handle {
unsafe {
let mut vi = OSVERSIONINFOW {
dwOSVersionInfoSize: 0,
dwMajorVersion: 0,
dwMinorVersion: 0,
dwBuildNumber: 0,
dwPlatformId: 0,
szCSDVersion: [0; 128],
};
let status = (rtl_get_version)(&mut vi as _);
if status >= 0 {
Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber))
} else {
None
}
}
} else {
None
}
}
pub fn is_win11() -> bool {
let v = get_windows_ver().unwrap_or_default();
v.2 >= 22000
}
#[test]
fn test_version() {
dbg!(get_windows_ver().unwrap_or_default());
}
+19 -47
View File
@@ -1,62 +1,40 @@
{
"package": {
"productName": "Clash Verge",
"version": "0.0.25"
"version": "1.4.4"
},
"build": {
"distDir": "../dist",
"devPath": "http://localhost:3000/",
"beforeDevCommand": "yarn run web:dev",
"beforeBuildCommand": "yarn run web:build"
"beforeDevCommand": "pnpm run web:dev",
"beforeBuildCommand": "pnpm run web:build"
},
"tauri": {
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "top.gydi.clashverge",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon-new.icns",
"icons/icon.ico"
],
"resources": ["resources"],
"externalBin": ["sidecar/clash"],
"externalBin": ["sidecar/clash-meta", "sidecar/clash-meta-alpha"],
"copyright": "© 2022 zzzgydi All Rights Reserved",
"category": "DeveloperTool",
"shortDescription": "A Clash GUI based on tauri.",
"longDescription": "A Clash GUI based on tauri.",
"deb": {
"depends": [],
"useBootstrapper": false
},
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"useBootstrapper": false,
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
},
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
"shortDescription": "A Clash Meta GUI based on tauri.",
"longDescription": "A Clash Meta GUI based on tauri."
},
"updater": {
"active": true,
"endpoints": [
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
"https://mirror.ghproxy.com/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json",
"https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
],
"dialog": false,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEQyOEMyRjBCQkVGOUJEREYKUldUZnZmbStDeStNMHU5Mmo1N24xQXZwSVRYbXA2NUpzZE5oVzlqeS9Bc0t6RVV4MmtwVjBZaHgK"
},
"allowlist": {
"shell": {
@@ -67,23 +45,17 @@
},
"process": {
"all": true
},
"globalShortcut": {
"all": true
},
"clipboard": {
"all": true
}
},
"windows": [
{
"title": "Clash Verge",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false,
"decorations": false,
"transparent": true,
"minWidth": 600,
"minHeight": 520
}
],
"windows": [],
"security": {
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
"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';"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"targets": ["deb", "appimage", "updater"],
"deb": {
"depends": ["openssl"]
}
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"tauri": {
"systemTray": {
"iconPath": "icons/icon.png",
"iconAsTemplate": true
},
"bundle": {
"targets": ["app", "dmg", "updater"],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"tauri": {
"systemTray": {
"iconPath": "icons/tray-icon.png"
},
"bundle": {
"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"
}
}
}
}
}
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+4
View File
@@ -0,0 +1,4 @@
@font-face {
font-family: "twemoji mozilla";
src: url("../fonts/Twemoji.Mozilla.ttf");
}
+27 -1
View File
@@ -4,13 +4,21 @@ body {
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
:root {
--primary-main: #5b5c9d;
--text-primary: #637381;
--selection-color: #f5f5f5;
--scroller-color: #90939980;
--background-color: #ffffff;
--background-color-alpha: rgba(24, 103, 192, 0.1);
--border-radius: 8px;
}
::selection {
@@ -25,8 +33,26 @@ body {
}
*::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: rgba(#909399, 0.5);
background-color: var(--scroller-color);
}
body {
overflow: hidden;
}
@import "./layout.scss";
@import "./page.scss";
@import "./font.scss";
@media (prefers-color-scheme: dark) {
:root {
background-color: rgba(18, 18, 18, 1);
}
}
.user-none {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
+46 -12
View File
@@ -5,44 +5,59 @@
overflow: hidden;
&__left {
flex: 1 0 25%;
flex: 1 0 15%;
display: flex;
height: 100%;
max-width: 225px;
min-width: 125px;
padding: 8px 0;
padding: 16px 0 8px;
position: relative;
flex-direction: column;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
overflow: hidden;
background-color: var(--background-color-alpha);
$maxLogo: 100px;
.the-logo {
position: relative;
flex: 0 1 180px;
flex: 0 1 $maxLogo;
width: 100%;
max-width: 168px;
max-height: 168px;
max-width: $maxLogo + 32px;
max-height: $maxLogo;
margin: 0 auto;
padding: 0 8px;
padding: 0 16px;
text-align: center;
box-sizing: border-box;
img {
img,
svg {
width: 100%;
height: 100%;
pointer-events: none;
fill: var(--primary-main);
#bg {
fill: var(--background-color);
}
}
.the-newbtn {
position: absolute;
right: 20px;
bottom: 12px;
right: 10px;
bottom: 0px;
transform: scale(0.8);
}
}
.the-menu {
flex: 1 1 75%;
flex: 1 1 80%;
overflow-y: auto;
margin-bottom: 8px;
margin-bottom: 0px;
}
.the-traffic {
@@ -58,6 +73,7 @@
position: relative;
flex: 1 1 75%;
height: 100%;
background-color: var(--background-color-alpha);
.the-bar {
position: absolute;
@@ -75,7 +91,7 @@
top: 0;
left: 0;
right: 2px;
bottom: 10px;
bottom: 0px;
}
}
}
@@ -84,8 +100,26 @@
.windows,
.unknown {
&.layout {
$maxLogo: 115px;
.layout__left .the-logo {
flex: 0 1 $maxLogo;
max-width: $maxLogo + 32px;
max-height: $maxLogo;
}
.layout__right .the-content {
top: 30px;
}
}
}
.macos {
&.layout {
.layout__left {
padding-top: 24px;
}
.layout__right .the-content {
top: 20px;
}
}
}
+21 -15
View File
@@ -6,30 +6,36 @@
> header {
flex: 0 0 58px;
width: 90%;
max-width: 850px;
width: 100%;
// max-width: 850px;
margin: 0 auto;
padding-right: 4px;
padding-right: 8px;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: space-between;
}
> section {
position: relative;
flex: 1 1 100%;
width: 100%;
.base-container {
height: 100%;
overflow: auto;
padding: 8px 0;
box-sizing: border-box;
scrollbar-gutter: stable;
overflow: hidden;
border-radius: var(--border-radius);
.base-content {
width: 90%;
max-width: 850px;
margin: 0 auto;
> section {
position: relative;
flex: 1 1 100%;
width: 100%;
height: 100%;
overflow: auto;
padding: 5px 5px;
box-sizing: border-box;
scrollbar-gutter: stable;
.base-content {
width: 100%;
// max-width: 850px;
margin: 0 auto;
}
}
}
}
+74
View File
@@ -0,0 +1,74 @@
import { ReactNode } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
type SxProps,
type Theme,
} from "@mui/material";
import { LoadingButton } from "@mui/lab";
interface Props {
title: ReactNode;
open: boolean;
okBtn?: ReactNode;
cancelBtn?: ReactNode;
disableOk?: boolean;
disableCancel?: boolean;
disableFooter?: boolean;
contentSx?: SxProps<Theme>;
children?: ReactNode;
loading?: boolean;
onOk?: () => void;
onCancel?: () => void;
onClose?: () => void;
}
export interface DialogRef {
open: () => void;
close: () => void;
}
export const BaseDialog: React.FC<Props> = (props) => {
const {
open,
title,
children,
okBtn,
cancelBtn,
contentSx,
disableCancel,
disableOk,
disableFooter,
loading,
} = props;
return (
<Dialog open={open} onClose={props.onClose}>
<DialogTitle>{title}</DialogTitle>
<DialogContent sx={contentSx}>{children}</DialogContent>
{!disableFooter && (
<DialogActions>
{!disableCancel && (
<Button variant="outlined" onClick={props.onCancel}>
{cancelBtn}
</Button>
)}
{!disableOk && (
<LoadingButton
loading={loading}
variant="contained"
onClick={props.onOk}
>
{okBtn}
</LoadingButton>
)}
</DialogActions>
)}
</Dialog>
);
};
+29
View File
@@ -0,0 +1,29 @@
import { alpha, Box, Typography } from "@mui/material";
import { InboxRounded } from "@mui/icons-material";
interface Props {
text?: React.ReactNode;
extra?: React.ReactNode;
}
export const BaseEmpty = (props: Props) => {
const { text = "Empty", extra } = props;
return (
<Box
sx={({ palette }) => ({
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
color: alpha(palette.text.secondary, 0.75),
})}
>
<InboxRounded sx={{ fontSize: "4em" }} />
<Typography sx={{ fontSize: "1.25em" }}>{text}</Typography>
{extra}
</Box>
);
};
@@ -0,0 +1,29 @@
import { ReactNode } from "react";
import { ErrorBoundary, FallbackProps } from "react-error-boundary";
function ErrorFallback({ error }: FallbackProps) {
return (
<div role="alert" style={{ padding: 16 }}>
<h4>Something went wrong:(</h4>
<pre>{error.message}</pre>
<details title="Error Stack">
<summary>Error Stack</summary>
<pre>{error.stack}</pre>
</details>
</div>
);
}
interface Props {
children?: ReactNode;
}
export const BaseErrorBoundary = (props: Props) => {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
{props.children}
</ErrorBoundary>
);
};

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