Compare commits

..
204 Commits
Author SHA1 Message Date
wonfen 7743c42dd1 Release - 2.1.2 真·臻 2025-03-01 01:49:01 +08:00
Christine. 3f3dab9495 add: i18n text for settings page. (#2815) 2025-03-01 01:36:36 +08:00
Tunglies 2dc9672b20 feat: better setting UI layout (#2814) 2025-03-01 01:29:23 +08:00
wonfen 2c90d1ca69 chore: v2 updater 2025-03-01 01:29:23 +08:00
Tunglies 1555e2910d feat: better setting layout ui (#2809) 2025-03-01 01:29:23 +08:00
Tunglies 19b6bd35f5 feat: quiting when enable tun mode no more blocking system network (#2805) 2025-03-01 01:29:23 +08:00
wonfen 3d4d60edc8 test updater 2025-03-01 01:29:23 +08:00
wonfen db0a000ecb chore: updater v2 Capabilities 2025-02-28 10:55:53 +08:00
wonfen 24ff48b41c fix: updater 2025-02-28 09:33:50 +08:00
wonfen 18a67d0b2f chore: updatelog 2025-02-28 07:34:56 +08:00
wonfen e23af1ad58 feat: add ability to check service version and auto-reinstall 2025-02-28 06:45:30 +08:00
wonfen 70af392059 feat: enhance updater script with comprehensive platform support and logging 2025-02-27 17:19:04 +08:00
Christine. d255df23ee fix: connection details (#2778) 2025-02-27 14:53:17 +08:00
wonfen 1317a8b2db feat: enhance merge config validation and error handling 2025-02-27 14:49:55 +08:00
wonfen 4cac118442 style: UI tweak 2025-02-27 13:04:46 +08:00
wonfen 2ce43ccd23 Release - 2.1.1 臻fix 2025-02-27 03:18:23 +08:00
Tunglies d23b2949d8 feat: macos display colorful icon with speed rate (#2771) 2025-02-27 01:51:52 +08:00
wonfen 3471476ba2 feat: improve file type detection for better script recognition 2025-02-26 15:59:19 +08:00
wonfen cee61e5619 chore: downgrade Tauri updater plugin and add i18n for core switching 2025-02-26 15:04:47 +08:00
wonfen 5026e2bade feat: optimize hotkey behavior and window management logic 2025-02-26 11:03:50 +08:00
wonfen 83f005c256 fix: CI 2025-02-26 09:52:15 +08:00
wonfen 80843c5ee3 Release 2.1.0 - 臻 2025-02-26 08:36:02 +08:00
wonfen ee00defe43 feat: enhance script validation and error handling 2025-02-26 05:21:14 +08:00
wonfen 7696504d97 feat: Add window state monitoring and auto-save in real-time 2025-02-26 00:36:02 +08:00
wonfen 7a0e38a1b4 perf: Improve config validation error messages and handling 2025-02-25 13:47:29 +08:00
wonfen bc5d577553 chore: revise translation 2025-02-24 23:26:04 +08:00
wonfen cfed2b7236 chore: Change default TUN stack from 'mixed' to 'gvisor' 2025-02-24 23:10:09 +08:00
Christine. e34e6654bc add: i18n text for config check (#2750) 2025-02-24 22:23:42 +08:00
wonfen d6b85f1a01 fix: remove node related group info when deleting a node 2025-02-24 11:40:28 +08:00
wonfen 16af7b53cb fix: menu switching issue 2025-02-24 11:37:23 +08:00
wonfen befc856207 feat: Improve core change configuration validation and error handling 2025-02-24 07:34:03 +08:00
wonfen 5ab8e7a7c7 feat: Enhance configuration validation and error handling during app startup 2025-02-24 06:21:32 +08:00
zhaoyuanandzymouse 443bfa5928 feat: 通过添加CLASH_VERGE_REV_IP环境变量的方式,修改复制环境变量按钮的IP (#2734)
Co-authored-by: zymouse <zymouse@pixmoving.net>
2025-02-24 03:42:40 +08:00
wonfen 52627575ff feat: Enhance configuration validation and error handling
- Improve config validation process with detailed logging and error tracking
- Add more robust error handling in profile updates and config patches
- Implement comprehensive config validation using clash core subprocess
2025-02-23 10:53:09 +08:00
Christine. 39973f2d24 feat: auto rename alpha version (#2740) 2025-02-23 02:30:15 +08:00
wonfen 2b01857d15 chore: Disable automatic alpha tag update in workflow 2025-02-20 14:25:58 +08:00
wonfen 2650aa845f refactor: Optimize proxy rendering and layout calculation 2025-02-20 14:21:55 +08:00
wonfen 8400a61bf0 refactor: Simplify tray icon event handling across platforms 2025-02-20 07:03:28 +08:00
wonfen 763b6b9003 chore: Update Node.js version and remove updater file generation 2025-02-20 02:55:47 +08:00
wonfen 053f414f3c feat: Add system proxy status indicators 2025-02-19 13:56:22 +08:00
wonfen 2defa2cc56 refactor: Simplify log data management and improve search functionality 2025-02-19 13:06:15 +08:00
wonfen bb1dbfcfc3 fix: connections page traffic calculation 2025-02-19 02:24:21 +08:00
Tunglies 1378068a30 rm dead code (#2718)
* rm: verge service takes full control of mihomo process. no more required.

* rm: dead code
2025-02-19 01:13:52 +08:00
wonfen 5d9dce7d10 style: tweak 2025-02-18 11:40:41 +08:00
wonfen acb66a3012 feat: Add persistent column width settings for connection table & process filtering 2025-02-18 11:04:22 +08:00
wonfen 054f902cc6 refactor: Remove unused routing select component from settings page 2025-02-18 09:04:23 +08:00
wonfen 4333153a59 feat: Improve letter item hover interaction with debounce mechanism 2025-02-18 09:00:03 +08:00
wonfen 19e623d7c2 feat: Add pause/resume functionality for connection tracking 2025-02-18 08:14:09 +08:00
wonfen 2dd646537e fix: resolve deprecated warnings in console 2025-02-18 07:10:28 +08:00
wonfen 1ece079978 perf: Improve proxy list interaction and UI responsiveness 2025-02-18 05:54:16 +08:00
wonfen 0d8f779634 feat: Enhance alphabet selector with dynamic tooltip and scrolling 2025-02-18 01:55:44 +08:00
wonfen a7ffc9fc38 refactor: Improve proxy data update mechanism with optimistic UI and error handling 2025-02-18 00:37:55 +08:00
wonfen 932b29e66f fix: Refine TypeScript types for proxy groups component 2025-02-17 16:27:06 +08:00
wonfen 6be7a3b94c feat: Enhance proxy groups with Initials navigation and performance optimizations 2025-02-17 16:07:46 +08:00
wonfen 8e8dd1ec03 feat: Optimize tray speed rate rendering and update logic 2025-02-17 15:08:19 +08:00
wonfen 772f3268ce feat: Improve Virtuoso list rendering for proxy groups 2025-02-17 14:30:21 +08:00
wonfen a179591ac2 feat: Add persistent scroll position for proxy groups 2025-02-17 14:24:33 +08:00
wonfen d071e5971f fix: alpha build win webview pack rename issue 2025-02-17 12:06:57 +08:00
wonfen a9032f5f20 chore: Update updater artifacts configuration 2025-02-17 11:22:14 +08:00
Akioe Yu 0190702616 fix: check script error (#2453)
(cherry picked from commit 97bde64fd6516019a190d52f05af54ade623d57c)
2025-02-17 03:10:19 +08:00
wonfen c35bf38420 chore: Add macOS-specific test for format_bytes_speed function 2025-02-17 03:10:03 +08:00
wonfen 89d20e564a Revert "perf: Improve kernel management logic & add more dev mode logs"
This reverts commit 18c7ed1ccc.
2025-02-15 05:51:46 +08:00
Christine. 3c24ff3afc add: i18n text for lightweight mode (#2692) 2025-02-13 14:27:12 +08:00
Tunglies daf0398750 fix: build front cannot find IvergeConfig.enable_lite_mode and macos port switching causes crash (#2691)
* fix: macos switch protocol port or enable protocol port causes crash

* fix: build time front cannot find IVergeConfig attribute enable_lite_mode
2025-02-12 18:14:13 -08:00
wonfen 6b14c2b763 feat: add lightweight mode 2025-02-13 02:18:17 +08:00
wonfen 5bf2f9b8ed fix(macos): add missing required dependencies 2025-02-12 15:06:42 +08:00
wonfen 10a2655288 fix: remove unused imports to resolve compile warnings 2025-02-12 14:35:49 +08:00
wonfen ac07397818 refactor: improve hotkey management, logging, and error handling; fix tray freeze and hotkey failure on silent startup 2025-02-12 14:23:42 +08:00
wonfen 30d061d00f fix(tray): resolve blank icon issue on Windows and optimize creation logic 2025-02-12 13:56:33 +08:00
Christine. 360d8a5143 fix: use remoteDestination replace DestinationIP in connection page, #2668 (#2679)
* fix: use remoteDestination replace DestinationIP  in connection page, #2668

* add: missing i18n text

* fix: display the target address details in connection page
2025-02-11 14:52:27 +08:00
wonfen 70e0a5adc8 refactor: Improve tray icon and event handling across platforms & unify click behavior 2025-02-11 14:48:31 +08:00
wonfen ef52f81494 fix: app crash without a default global_hotkey vaule 2025-02-11 01:50:48 +08:00
wonfen e5c1de3ad3 fix: deprecated CLI warnings 2025-02-11 01:05:51 +08:00
wonfen 576fd700ae feat: Add independent alpha update channel 2025-02-11 00:53:44 +08:00
wonfen 476df65a1b fix: try to fix linux CI updater problem 2025-02-11 00:53:44 +08:00
Christine. bd0a4863a8 add: i18n text (#2675) 2025-02-10 00:29:43 -08:00
wonfen d25fbc05e2 perf: Optimize kernel shutdown speed & logic 2025-02-10 13:18:00 +08:00
wonfen b8a0b6f1f4 fix: crash caused by global_hotkey 2025-02-10 12:39:07 +08:00
wonfen 18c7ed1ccc perf: Improve kernel management logic & add more dev mode logs 2025-02-09 14:19:15 +08:00
Tunglies b14db06955 perf: imporve clash mode switching performance on the main window (#2667) 2025-02-09 07:45:46 +08:00
Tunglies 215dcee3f1 feat: option to enable global hotkey (#2665) 2025-02-09 07:45:22 +08:00
Tunglies 37e5c22a5a fix: duplicate checked tray menu when profile name are same (#2660) 2025-02-08 09:36:04 +08:00
wonfen bb0b6c3f77 feat: Optimize UI layout and page hierarchy 2025-02-07 09:13:30 +08:00
wonfen a37df46ce7 perf: optimize node latency refresh rate for faster updates 2025-02-06 07:58:42 +08:00
wonfen 73fbec2514 chore: update deps 2025-02-05 14:19:43 +08:00
Tunglies f66fa08b2c Feature: Switch Proxy Profile from Tray Menu (#2644) 2025-02-05 08:52:47 +08:00
wonfen bae606bc9d fix: restore hotkey functionality after silent startup 2025-02-02 11:37:10 +08:00
wonfen 1a2d5e988a feat: allow users to customize enhanced-mode and fakeip-range in tun mode by wonfen 2025-02-02 05:12:16 +08:00
wonfen d20bd62b90 style: refine tray speed display 2025-02-02 03:43:15 +08:00
wonfen 204bf43b9a fix: windows rounded corners 2025-01-19 09:47:43 +08:00
wonfen 3ea0d20e2c add donation 2025-01-18 16:08:24 +08:00
wonfen 6d527a1cdb release 2.0.3 2025-01-16 03:30:07 +08:00
白铭骢 (Mingcong Bai) 9c1ece754e fix(locales): clean up typesetting (#2539)
Following localisation guidelines from 《大陆简中自由软件本地化工作指南》
1.5.4 版 (Simplified Chinese - Mainland China FOSS Localization
Guidelines, version 1.5.4).

Link: https://repo.aosc.io/aosc-l10n/zh_CN_l10n_1.5.4.pdf
2025-01-14 13:00:30 +08:00
huzibaca 9070260d41 chore: update 2025-01-14 12:56:08 +08:00
huzibaca cd8df52aad revert: update deps 2025-01-14 12:41:52 +08:00
huzibaca fa86efcdfb chore: avoid duplicate updates when tray rate is off 2025-01-14 11:48:43 +08:00
huzibaca 1a61fab79a feat: maoos tray speed can be closed 2025-01-13 20:48:25 +08:00
huzibaca 41a27641df fix: fix: try to fix the language pack issue(2) 2025-01-13 17:09:38 +08:00
huzibaca a39696151d fix: windows tray icon color not updated 2025-01-13 13:49:56 +08:00
huzibaca 4a4ca5c409 chore: update husky/pre-commit 2025-01-13 13:03:46 +08:00
huzibaca 5bff7ea469 fix: tray tooltip not updating 2025-01-13 13:01:12 +08:00
huzibaca f2cc116ff9 chore: update 2025-01-13 12:41:07 +08:00
huzibaca 80a18c9172 chore: update deps 2025-01-12 23:10:15 +08:00
huzibaca a56732e0a3 fix: try to fix the language pack issue 2025-01-12 22:22:06 +08:00
huzibaca 9655f7712b Merge branch 'languagefixes' into dev
# Conflicts:
#	src-tauri/Cargo.lock
#	src-tauri/Cargo.toml
#	src-tauri/src/core/tray/mod.rs
2025-01-11 15:07:30 +08:00
huzibaca 13b63b5d96 fix: fixes #2502 2025-01-11 12:55:20 +08:00
lucidhz c5989d2735 fix: put_configs response add detail error message (#2492)
感谢pr
2025-01-05 01:27:12 -08:00
huzibaca fb4032d6ce fix: modify the external control access key, the tray rate display is abnormal 2025-01-02 15:27:28 +08:00
huzibaca a29c2d4b14 chore: update deps 2025-01-02 15:02:07 +08:00
huzibaca aa7e911c63 chore: update 2025-01-01 08:25:31 +08:00
huzibaca eeff4d41f4 feat: the tray displays the shortcut keys that have been set 2025-01-01 08:14:15 +08:00
huzibaca 086f023ebc chore: update 2024-12-31 11:11:29 +08:00
huzibaca b70336c026 fix: unused code 2024-12-31 08:08:52 +08:00
huzibaca 7d84279370 fix: syntax issues 2024-12-31 04:50:12 +08:00
huzibaca 1b2f1b6106 Merge branch 'feat-macos-spped-rate-icon' into dev 2024-12-31 04:43:54 +08:00
huzibaca fb41c915cc feat: macos system tray addition rate display 2024-12-31 04:42:55 +08:00
blagodaren bab291a60b fix: small refactor 2024-12-28 13:57:30 +03:00
blagodaren 8461046a4f fix: add more lang and lang fix for tray 2024-12-28 10:05:30 +03:00
blagodaren fc925ea032 fix: add system language sup 2024-12-28 08:12:46 +03:00
huzibaca 542baf9d69 fix: fixes #2460 2024-12-28 06:05:09 +08:00
huzibaca 3f02859203 chore:add macos entitlements.list 2024-12-27 04:38:34 +08:00
huzibaca af97bd15a9 chore: update 2024-12-27 02:42:35 +08:00
huzibaca 2c081e5a04 chore: secret is empty and no parameters are passed 2024-12-25 02:15:06 +08:00
huzibaca 2d2521e434 chore: update 2024-12-24 06:03:23 +08:00
huzibaca d9291d4f79 refactor: when updating the tray, the logic is split to improve performance. 2024-12-24 04:52:14 +08:00
huzibaca 0669f7a10b chore: after saving the configuration file, restart the core 2024-12-24 02:22:46 +08:00
huzibaca f0d953ff59 chore: update default_bypass
1. add 172.29.0.0/16
2024-12-23 06:06:46 +08:00
huzibaca 4f6ca40811 fix: turn off window shadow fixer, fixes #2425 2024-12-23 04:36:41 +08:00
huzibaca 47848099be chore: update 2024-12-15 01:14:16 +08:00
huzibaca f839d3bc88 fix: fixer #2346 2024-12-14 17:30:58 +08:00
huzibaca 800f994b10 chore: update deps 2024-12-14 16:21:16 +08:00
Langning Chen b149084e39 Fix l10n issues (#2315)
* Complete the missing translation

* Match the Chinese language file order with others
2024-12-11 01:02:21 +08:00
huzibaca e6580f2f05 chorea: update deps 2024-12-10 22:36:13 +08:00
huzibaca c5c840d378 feat: improve system bypass settings 2024-12-10 14:37:11 +08:00
huzibaca 2e0be4b426 chore: enable default proxy bypass and hide custom bypass settings 2024-12-10 13:59:13 +08:00
huzibaca 460d72f392 chore: update deps(tauri-plugin-autostart) 2024-12-08 22:10:15 +08:00
huzibaca aa18c4870d chore: deeplink uses the latest API 2024-12-08 15:54:46 +08:00
huzibaca 61533646ad chore: update deps 2024-12-08 09:58:59 +08:00
huzibaca b42d13f573 fix: when the window is hidden, close the websocket connection, reduce the risk of memory leaks 2024-12-07 16:47:41 +08:00
wonfen 0e3b631118 chore: change fakeip range 2024-12-06 15:34:41 +08:00
huzibaca 38745d4513 chore:set the sysproxy_rs version to a fixed version 2024-12-06 15:03:04 +08:00
huzibaca d22b37c7bf fix: extension script dns is overwritten fixer #2235 2024-12-06 12:35:09 +08:00
Myles Mo d233a84362 feat: add nullshell env variable (#2285) 2024-12-04 19:01:13 +08:00
huzibaca 589324b582 fix: shift hotkey conversion fixer #2278 2024-12-04 13:19:56 +08:00
huzibaca c11efcb9be chore: update version 2024-12-04 00:30:46 +08:00
huzibaca 6197249377 fix: set fontLigatures to false fixer #2267 2024-12-04 00:28:33 +08:00
lollapalooza c71e18e97e feat: add bypass check feature (#2272) 2024-12-03 16:18:07 +08:00
Christine. f400f900e6 fix: #2126 (#2233) 2024-12-03 16:03:21 +08:00
wonfen ae5b2cfb79 release 2.0.2 2024-12-01 11:00:09 +08:00
huzibaca 0bb8786ef2 fix: resource file initialization failed 2024-12-01 09:38:29 +08:00
huzibaca f7d5be774d fix: resolve service permission failed(2) 2024-11-30 10:45:53 +08:00
huzibaca c0a0b82fa6 chore: update 2024-11-30 10:38:15 +08:00
huzibaca 277ef51375 fix: resolve service permission failed 2024-11-30 10:29:16 +08:00
huzibaca 67b1cf9e1b feat: user uploaded icons can use templates, provided they are monochrome icons fixer #2213 2024-11-30 09:49:43 +08:00
huzibaca 4f797eb7b8 chore: update 2024-11-30 07:04:09 +08:00
huzibaca 29ccabc054 refactor: update_core_config, simplify logic and delete invalid notifications 2024-11-30 05:43:59 +08:00
yyhhyy 857574cbc1 🐛 fix: Dynamically set IPv6 DNS configuration based on existing config (#2198)
thanks
2024-11-30 00:55:15 +08:00
wonfen acc97f28b5 chore: change mac tun dns 2024-11-29 11:10:41 +08:00
huzibaca 02e32aec41 chore: update 2024-11-29 03:46:37 +08:00
huzibaca fe0fdc5603 chore: remove debug code 2024-11-28 14:44:13 +08:00
Yu-Haifeng 66941a18c0 fix: macOS tray icon not changed when use system proxy or tun mode (#2176) 2024-11-28 14:42:05 +08:00
huzibaca f772f92b88 fix: the deadlock caused by incorrect call of window_state due to document error 2024-11-28 12:54:55 +08:00
huzibaca d80db8c91e chore: clean startup registry keys for older versions(2) 2024-11-28 06:40:44 +08:00
huzibaca a1e67820c7 chore: clean startup registry keys for older versions 2024-11-28 05:34:56 +08:00
wonfen ebe0cfbb0c chore: set tun default stack 2024-11-28 04:22:13 +08:00
huzibaca 59c52199e3 chore: update 2024-11-27 14:27:16 +08:00
wonfen 148def4c0f release 2.0.2 2024-11-27 12:41:51 +08:00
huzibaca 3177ea0f4d chore: update 2024-11-27 11:34:52 +08:00
huzibaca 7d65ce847a fix: when tun is closed, the full profile configuration is not restored 2024-11-27 10:35:42 +08:00
huzibaca 2ac27fcfa7 refactor: backup implementation
1. timeouts can be set for different operations
2. performance optimization
2024-11-27 07:34:34 +08:00
huzibaca 373c8ec0e5 chore: update 2024-11-27 07:09:03 +08:00
huzibaca 7df5ca1912 fix: linux build failed 2024-11-27 05:54:48 +08:00
huzibaca d8dc60ff0d fix: fixer #2158 2024-11-27 05:47:08 +08:00
huzibaca 56deafee61 chore: update version 2024-11-27 05:09:46 +08:00
huzibaca 4c29850e1c fix: build failed. #2156 2024-11-27 05:07:02 +08:00
huzibaca 90bf5e0782 chore: timeout adjusted to 15 seconds 2024-11-26 15:09:58 +08:00
huzibaca c2d07ce4c1 fix: add scroll bar and scroll to top button to the test page . fixer #2118 2024-11-26 14:44:43 +08:00
wonfen 564d81fd31 style: adjust 2024-11-26 12:11:20 +08:00
huzibaca f8b7b82ba9 chore: remove window title 2024-11-26 10:11:55 +08:00
huzibaca 71cb2a97c0 fix: kernel-caused silent mode failure to start windows 2024-11-26 09:00:01 +08:00
huzibaca 1cc6472002 chore: update 2024-11-26 05:46:34 +08:00
huzibaca 9b2c80494d refactor: Implement using third-party libraries 2024-11-26 04:51:19 +08:00
huzibaca 4ccc2a2961 chore: update 2024-11-26 03:30:15 +08:00
huzibaca 17a8dfb58a fix: file drag and drop import cannot be used 2024-11-26 03:07:25 +08:00
wonfen 184b588f20 style: Increased light color contrast to prevent blurring on some displays 2024-11-26 01:05:30 +08:00
huzibaca 87c4ebe0da chore: replace mui grid with mui grid2 2024-11-25 12:51:13 +08:00
wonfen 18a0a15e16 update change log 2024-11-25 06:46:29 +08:00
huzibaca c8fd62f388 fix: Try to fix vcruntime duplicate installation 2024-11-25 06:33:47 +08:00
huzibaca 97f03de295 fix: update failed(updater.install called before updater.download) 2024-11-25 05:27:26 +08:00
huzibaca 2c0a5666fc fix: when restoring webdav, the current username, password and url are not preserved 2024-11-25 03:41:35 +08:00
huzibaca 5c3f0d5b60 fix: password failed due to character escaping 2024-11-25 02:48:56 +08:00
huzibaca 746763ed34 fix: remove comments from svg icons to prevent front-end crashes. fixer #2093 2024-11-25 01:58:02 +08:00
wonfen e3800a575f fix: fakeip dns 2024-11-25 01:50:30 +08:00
huzibaca c78c936762 chore: replace mui grid with mui grid2 2024-11-25 01:34:18 +08:00
huzibaca ea5d6f9c46 fix: tun allocates the wrong private network segment, causing conflicts. 2024-11-25 01:06:51 +08:00
wonfen 97f8022276 release 2.0.1 2024-11-24 09:00:20 +08:00
huzibaca d718bd9141 fix: If an older version of the executable exists, delete it(2) 2024-11-24 08:25:46 +08:00
huzibaca 8ddd48eda1 feat: try to use vscode first, if not found then use system default app 2024-11-24 08:20:00 +08:00
huzibaca 3e20404959 fix: If an older version of the executable exists, delete it 2024-11-24 01:24:51 +08:00
huzibaca dde7ead751 Merge branch 'fix-migrate-tauri2-errors'
* fix-migrate-tauri2-errors: (288 commits)

# Conflicts:
#	.github/ISSUE_TEMPLATE/bug_report.yml
2024-11-24 00:14:46 +08:00
huzibaca be719680b0 fix: windows cannot open yaml file 2024-11-23 19:46:53 +08:00
wonfen d6f5a79ac9 fix: release workflow 2024-11-23 12:36:16 +08:00
wonfen 26dd097962 update: Readme 2024-11-09 09:00:53 +08:00
wonfen eb1a1b3786 chore: update bug report template 2024-10-30 12:59:19 +08:00
104 changed files with 9255 additions and 4260 deletions
+1 -1
View File
@@ -57,6 +57,6 @@ body:
- type: textarea
attributes:
label: 日志 / Log
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到trace,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
description: 请提供完整或相关部分的Debug日志(请在“软件左侧菜单”->“设置”->“日志等级”调整到debug,Verge错误请把“杂项设置”->“app日志等级”调整到debug/trace,并重启Verge生效。日志文件在“软件左侧菜单”->“设置”->“日志目录”下) / Please provide a complete or relevant part of the Debug log (please adjust the "Log level" to debug in "Software left menu" -> "Settings" -> "Log level". If there is a Verge error, please adjust "Miscellaneous settings" -> "app log level" to trace, and restart Verge to take effect. The log file is under "Software left menu" -> "Settings" -> "Log directory")
validations:
required: true
+37 -23
View File
@@ -2,9 +2,6 @@ name: Alpha Build
on:
workflow_dispatch:
push:
branches: [main]
tags-ignore: [updater, alpha]
permissions: write-all
env:
CARGO_INCREMENTAL: 0
@@ -48,7 +45,7 @@ jobs:
workspaces: src-tauri
cache-all-crates: true
cache-on-failure: true
- name: Install dependencies (ubuntu only)
if: matrix.os == 'ubuntu-22.04'
run: |
@@ -58,7 +55,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -70,6 +67,9 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: Alpha Version update
run: pnpm run fix-alpha-version
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
@@ -119,7 +119,7 @@ jobs:
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
@@ -129,7 +129,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -141,10 +141,10 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: 'Setup for linux'
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
@@ -170,20 +170,20 @@ jobs:
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: 'Install aarch64 tools'
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: 'Install armv7 tools'
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -196,7 +196,7 @@ jobs:
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: '--max_old_space_size=4096'
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
@@ -248,7 +248,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -281,9 +281,23 @@ jobs:
- name: Rename
run: |
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
Rename-Item $file.FullName $newName
}
- name: Upload Release
uses: softprops/action-gh-release@v2
@@ -313,12 +327,12 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash
- name: Update Tag
uses: richardsimko/update-tag@v1
with:
tag_name: alpha
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# - name: Update Tag
# uses: richardsimko/update-tag@v1
# with:
# tag_name: alpha
# env:
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
cat > release.txt << 'EOF'
+34 -19
View File
@@ -50,7 +50,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -67,8 +67,8 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
@@ -109,7 +109,7 @@ jobs:
- name: Add Rust Target
run: rustup target add ${{ matrix.target }}
- name: Rust Cache
uses: Swatinem/rust-cache@v2
with:
@@ -119,7 +119,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: "22"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -131,10 +131,10 @@ jobs:
pnpm i
pnpm check ${{ matrix.target }}
- name: 'Setup for linux'
- name: "Setup for linux"
run: |-
sudo ls -lR /etc/apt/
cat > /tmp/sources.list << EOF
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy main multiverse universe restricted
deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu jammy-security main multiverse universe restricted
@@ -160,20 +160,20 @@ jobs:
patchelf:${{ matrix.arch }} \
librsvg2-dev:${{ matrix.arch }}
- name: 'Install aarch64 tools'
- name: "Install aarch64 tools"
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu
- name: 'Install armv7 tools'
- name: "Install armv7 tools"
if: matrix.target == 'armv7-unknown-linux-gnueabihf'
run: |
sudo apt install -y \
gcc-arm-linux-gnueabihf \
g++-arm-linux-gnueabihf
- name: Build for Linux
run: |
export PKG_CONFIG_ALLOW_CROSS=1
@@ -186,7 +186,7 @@ jobs:
fi
pnpm build --target ${{ matrix.target }}
env:
NODE_OPTIONS: '--max_old_space_size=4096'
NODE_OPTIONS: "--max_old_space_size=4096"
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
@@ -195,6 +195,7 @@ jobs:
sudo apt-get update
sudo apt-get install jq
echo "VERSION=$(cat package.json | jq '.version' | tr -d '"')" >> $GITHUB_ENV
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
- name: Upload Release
uses: softprops/action-gh-release@v2
@@ -237,7 +238,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -262,17 +263,31 @@ jobs:
env:
NODE_OPTIONS: "--max_old_space_size=4096"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
with:
tauriScript: pnpm
args: --target ${{ matrix.target }}
- name: Rename
run: |
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.exe' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.exe'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip'
Rename-Item '.\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}-setup.nsis.zip.sig' 'Clash Verge_${{steps.build.outputs.appVersion}}_${{ matrix.arch }}_fixed_webview2-setup.nsis.zip.sig'
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-setup.exe"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.exe$", "_fixed_webview2-setup.exe"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip$", "_fixed_webview2-setup.nsis.zip"
Rename-Item $file.FullName $newName
}
$files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*.nsis.zip.sig"
foreach ($file in $files) {
$newName = $file.Name -replace "-setup\.nsis\.zip\.sig$", "_fixed_webview2-setup.nsis.zip.sig"
Rename-Item $file.FullName $newName
}
- name: Upload Release
uses: softprops/action-gh-release@v2
@@ -298,7 +313,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -323,7 +338,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -36,7 +36,7 @@ jobs:
- name: Install Node
uses: actions/setup-node@v4
with:
node-version: "20"
node-version: "22"
- uses: pnpm/action-setup@v4
name: Install pnpm
-3
View File
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm pretty-quick --staged
+4
View File
@@ -55,6 +55,10 @@ Supports Windows (x64/x86), Linux (x64/arm64) and macOS 10.15+ (intel/apple).
Refer to [Doc FAQ Page](https://clash-verge-rev.github.io/faq/windows.html)
### Donation
[捐助Clash Verge Rev的开发](https://github.com/sponsors/clash-verge-rev)
## Development
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
+189 -7
View File
@@ -1,16 +1,198 @@
## v2.0.0
## v2.1.2
**发行代号:臻**
代号释义: 千锤百炼臻至善,集性能跃升、功能拓展、交互焕新于一体,彰显持续打磨、全方位优化的迭代精神。
感谢 Tychristine 对社区群组管理做出的重大贡献!
2.1.2相对2.1.1(已下架不在提供)更新了:
- 无法更新和签名验证失败的问题(该死的CDN缓存)
- 设置菜单区分Verge基本设置和高级设置
- 增加v2 Updater的更多功能和权限
- 退出Verge后Tun代理状态仍保留的问题
2.1.1相对2.1.0(已下架不在提供)更新了:
- 检测所需的Clash Verge Service版本(杀毒软件误报可能与此有关,因为检测和安装新版本Service需管理员权限)
- MacOS下支持彩色托盘图标和更好速率显示(感谢Tunglies)
- 文件类型判断不准导致脚本检测报错的问题
- 打开Win下的阴影(Win10因底层兼容性问题,可能圆角和边框显示不太完美)
- 边框去白边
- 修复Linux下编译问题
- 修复热键无法关闭面板的问题
## v2.1.0 - 发行代号:臻
### 功能新增
- 新增窗口状态实时监控与自动保存功能
- 增强核心配置变更时的验证与错误处理机制
- 支持通过环境变量`CLASH_VERGE_REV_IP`自定义复制IP地址
- 添加连接表列宽持久化设置与进程过滤功能
- 新增代理组首字母导航与动态滚动定位功能
- 实现连接追踪暂停/恢复功能
- 支持从托盘菜单快速切换代理配置
- 添加轻量级模式开关选项
- 允许用户自定义TUN模式增强类型和FakeIP范围
- 新增系统代理状态指示器
- 增加Alpha版本自动重命名逻辑
- 优化字母导航工具提示与防抖交互机制
### 性能优化
- 重构代理列表渲染逻辑,提升布局计算效率
- 优化代理数据更新机制,采用乐观UI策略
- 改进虚拟列表渲染性能(Virtuoso)
- 提升主窗口Clash模式切换速度(感谢Tunglies)
- 加速内核关闭流程并优化管理逻辑
- 优化节点延迟刷新速率
- 改进托盘网速显示更新逻辑
- 提升配置验证错误信息的可读性
- 重构服务架构,优化代码组织结构(感谢Tunglies)
- 优化内核启动时的配置验证流程
### 问题修复
- 修复删除节点时关联组信息残留问题
- 解决菜单切换异常与重复勾选问题
- 修正连接页流量计算错误
- 修复Windows圆角显示异常问题
- 解决控制台废弃API警告
- 修复全局热键空值导致的崩溃
- 修复Alpha版本Windows打包重命名问题
- 修复MacOS端口切换崩溃问题
- 解决Linux持续集成更新器问题
- 修复静默启动后热键失效问题
- 修正TypeScript代理组类型定义
- 修复Windows托盘图标空白问题
- 优化远程目标地址显示(替换旧版IP展示)
### 交互体验
- 统一多平台托盘图标点击行为
- 优化代理列表滚动流畅度
- 改进日志搜索功能与数据管理
- 重构热键管理逻辑,修复托盘冻结问题
- 优化托盘网速显示样式
- 增强字母导航工具提示的动态响应
### 国际化
- 新增配置检查多语言支持
- 添加轻量级模式多语言文本
- 完善多语言翻译内容
### 维护更新
- 将默认TUN协议栈改为gVisor
- 更新Node.js运行版本
- 移除自动生成更新器文件
- 清理废弃代码与未使用组件
- 禁用工作流自动Alpha标签更新
- 更新依赖库版本
- 添加MacOS格式转换函数专项测试
- 优化开发模式日志输出
### 安全增强
- 强化应用启动时的配置验证机制
- 改进脚本验证与异常处理流程
- 修复编译警告(移除无用导入)
## v2.0.3
### Notice
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了:巨量改进与性能、稳定性提升,目前Clash Verge Rev已经有了比肩cfw的健壮性;而且更强大易用!
- 由于更改了服务安装逻辑,Mac/Linux 首次安装需要输入 2 遍系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
- 因 Tauri 2.0 底层 bug,关闭窗口暂时修改为最小化功能
- 由于更改了服务安装逻辑,每次更新安装需要输入系统密码卸载老版本服务和安装新版本服务,以后可以丝滑使用tun(虚拟网卡)模式
### 2.0.3相对于2.0.2改进修复了:
1. 修复VLess-URL识别网络类型错误 f400f90 #2126
2. 新增系统代理绕过文本校验 c71e18e
3. 修复脚本编辑器UI显示不正确 6197249 #2267
4. 修复Shift热键无效 589324b #2278
5. 新增nushell环境变量复制 d233a84
6. 修复全局扩展脚本无法覆写DNS d22b37c #2235
7. 切换到系统代理相对于稳定的版本 38745d4
8. 修改fake-ip-range网段 0e3b631
9. 修复窗口隐藏后WebSocket未断开连接,减小内存风险 b42d13f
10. 改进系统代理绕过设置 c5c840d
11. 修复i18n翻译文本缺失 b149084
12. 修复双击托盘图标打开面板 f839d3b #2346
13. 修复Windows10窗口白色边框 4f6ca40 #2425
14. 修复Windows窗口状态恢复 4f6ca40
15. 改进保存配置文件自动重启Mihomo内核 0669f7a
16. 改进更新托盘图标性能 d9291d4
17. 修复保存配置后代理列表未更新 542baf9 #2460
18. 新增MacOS托盘显示实时速率,可在"界面设置"中关闭 1b2f1b6
19. 新增托盘菜单显示已设置的快捷键 eeff4d4
20. 新增重载配置文件错误响应"400"时显示更多错误信息 c5989d2 #2492
21. 修复GUI代理状态与菜单显示不一致 13b63b5 #2502
22. 新增默认语言跟随系统语言(无语言支持即为英语),添加了阿拉伯语、印尼语、鞑靼语支持 9655f77 #2940
### Features
- Meta(mihomo)内核升级 1.19.1
- 增加更多语言和托盘语言跟随
- MacOS增加状态栏速率显示
- 托盘显示快捷键
- 重载配置文件错误响应"400"时显示更多错误信息
- 改进保存配置文件自动重启Mihomo内核
### Performance
- 改进更新托盘图标性能
- 窗口隐藏后WebSocket断开连接
## v2.0.2
### Notice
- !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
- 历时3个月的紧密开发与严格测试稳定版2.0.0终于发布了:巨量改进与性能、稳定性提升,目前Clash Verge Rev已经有了比肩cfw的健壮性;而且更强大易用!
- 由于更改了服务安装逻辑,Mac/Linux 首次安装需要输入系统密码卸载和安装服务,以后可以丝滑使用 tun(虚拟网卡)模式
- 因 Tauri 2.0 底层 bug,关闭窗口后保留webview进程,优点是再次打开面板更快,缺点是内存使用略有增加
### 2.0.2相对于2.0.1改进了:
- MacOS 下自定义图标可以支持彩色、单色切换
- 修正了 Linux 下多个内核僵尸进程的问题
- 修正了 DNS ipv6 强制覆盖的逻辑
- 修改了 MacOS tun 模式下覆盖设置 dns 字段的问题
- 修正了 MacOS tray 图标不会随代理模式更改的问题
- 静默启动下重复运行会出现多个实例的bug
- 安装的时候自动删除历史残留启动项
- Tun模式默认是还用内核推荐的 mixed 堆栈
- 改进了默认窗口大小(启动软件窗口不会那么小了)
- 改进了 WebDAV 备份超时时间机制
- 测试菜单添加滚动条
- 改进和修正了 Tun 模式下对设置的覆盖逻辑
- 修复了打开配置出错的问题
- 修复了配置文件无法拖拽添加的问题
- 改善了浅色模式的对比度
### 2.0.1相对于2.0.0改进了:
- 无法从 2.0rc和2.0.0 升级的问题(已经安装了2.0版本的需手动下载安装)
- MacOS 系统下少有的无法安装服务,无法启动的问题,目前更健壮了
- 当系统中没有 yaml 编辑器的情况下,打开文件程序崩溃的问题
- Windows 应用内升级和覆盖安装不会删除老执行文件的问题
- 修改优化了 mac 下 fakeip 段和 dns
- 测试菜单 svg 图标格式检查
- 应用内升级重复安装 vs runtime 的问题
- 修复外部控制下密码有特殊字符认证出错的问题
- 修复恢复 Webdav 备份设置后, Webdav 设置丢失的问题
- 代理页面增加快速回到顶部的按钮
### Breaking changes
- 重大框架升级:使用 Tauri 2.0(巨量改进与性能提升)
- 强烈建议完全删除 1.x 老版本再安装此版本
- 出现 bug 到 issues 中提出;以后不再接受1.x版本的bug反馈。
- 强烈建议完全删除 1.x 老版本再安装此版本 !!使用出现异常的,打开设置-->配置目录 备份 后 删除所有文件 尝试是否正常!!
### Features
@@ -30,7 +212,7 @@
- 添加统一延迟的设置开关
- 添加 Windows 下自动检测并下载 vc runtime 的功能
- 支持显示 mux 和 mptcp 的节点标识
- 延迟测试连接更换 https 的 cp.cloudflare.com/generate_204 以防止机场劫持(关闭统一延迟的情况下延迟测试结果会有所增加)。
- 延迟测试连接更换 http 的 cp.cloudflare.com/generate_204 (关闭统一延迟的情况下延迟测试结果会有所增加)
- 重构日志记录逻辑,可以收集和筛选所有日志类型了(之前无法记录debug的日志类型)
### Performance
@@ -58,10 +240,10 @@
- 修复快捷键设置的相关 bug
- 修复 Win 下点左键菜单闪现的问题(Mac 下的操作逻辑相反,默认情况下不管点左/右键均会打开菜单,闪现不属于 bug)
### Know issues
### Known issues
- Windows 下窗口大小无法记忆(等待上游修复)
- Webdav 备份因为安全性和兼容性问题,暂不支持同步 Webdav 服务器地址和登录信息;跨平台配置同步
- Webdav 备份因为安全性和兼容性问题,暂不支持跨平台配置同步
---
+48 -47
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.0.0",
"version": "2.1.2",
"license": "GPL-3.0-only",
"scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev",
@@ -15,82 +15,83 @@
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/alpha_version.mjs",
"prepare": "husky"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^6.1.6",
"@mui/lab": "5.0.0-alpha.149",
"@mui/material": "^6.1.6",
"@mui/x-data-grid": "^7.22.2",
"@tauri-apps/api": "2.1.1",
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
"@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-fs": "^2.0.2",
"@tauri-apps/plugin-global-shortcut": "^2.0.0",
"@tauri-apps/plugin-notification": "^2.0.0",
"@tauri-apps/plugin-process": "^2.0.0",
"@tauri-apps/plugin-shell": "^2.0.1",
"@tauri-apps/plugin-updater": "^2.0.0",
"@mui/icons-material": "^6.4.2",
"@mui/lab": "6.0.0-beta.25",
"@mui/material": "^6.4.2",
"@mui/x-data-grid": "^7.25.0",
"@tauri-apps/api": "2.2.0",
"@tauri-apps/plugin-clipboard-manager": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "2.2.0",
"@tauri-apps/plugin-updater": "2.3.0",
"@types/json-schema": "^7.0.15",
"ahooks": "^3.8.1",
"axios": "^1.7.7",
"ahooks": "^3.8.4",
"axios": "^1.7.9",
"cli-color": "^2.0.4",
"dayjs": "1.11.13",
"foxact": "^0.2.41",
"glob": "^11.0.0",
"i18next": "^23.16.5",
"foxact": "^0.2.43",
"glob": "^11.0.1",
"i18next": "^24.2.2",
"js-base64": "^3.7.7",
"js-yaml": "^4.1.0",
"lodash-es": "^4.17.21",
"monaco-editor": "^0.52.0",
"monaco-editor": "^0.52.2",
"monaco-yaml": "^5.2.3",
"nanoid": "^5.0.8",
"peggy": "^4.1.1",
"nanoid": "^5.0.9",
"peggy": "^4.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2",
"react-hook-form": "^7.53.2",
"react-i18next": "^15.1.1",
"react-error-boundary": "^4.0.12",
"react-hook-form": "^7.54.2",
"react-i18next": "^13.5.0",
"react-markdown": "^9.0.1",
"react-monaco-editor": "^0.56.2",
"react-router-dom": "^6.28.0",
"react-monaco-editor": "^0.56.0",
"react-router-dom": "^6.22.3",
"react-transition-group": "^4.4.5",
"react-virtuoso": "^4.12.0",
"react-virtuoso": "^4.6.3",
"sockette": "^2.0.6",
"swr": "^2.2.5",
"swr": "^2.3.0",
"tar": "^7.4.3",
"types-pac": "^1.0.3",
"zustand": "^5.0.1"
"zustand": "^5.0.3"
},
"devDependencies": {
"@actions/github": "^6.0.0",
"@tauri-apps/cli": "2.1.0",
"@tauri-apps/cli": "2.2.7",
"@types/js-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-transition-group": "^4.4.11",
"@vitejs/plugin-legacy": "^5.4.3",
"@vitejs/plugin-react": "^4.3.3",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-transition-group": "^4.4.12",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-react": "^4.2.1",
"adm-zip": "^0.5.16",
"cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.5",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
"meta-json-schema": "^1.18.10",
"meta-json-schema": "^1.19.1",
"node-fetch": "^3.3.2",
"prettier": "^3.3.3",
"prettier": "^3.4.2",
"pretty-quick": "^4.0.0",
"sass": "^1.81.0",
"terser": "^5.36.0",
"typescript": "^5.6.3",
"vite": "^5.4.11",
"sass": "^1.83.4",
"terser": "^5.37.0",
"typescript": "^5.7.3",
"vite": "^6.0.11",
"vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.0"
},
+1555 -1660
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs/promises";
import path from "path";
/**
* 为Alpha版本重命名版本号
*/
const execPromise = promisify(exec);
/**
* 标准输出HEAD hash
*/
async function getLatestCommitHash() {
try {
const { stdout } = await execPromise("git rev-parse HEAD");
const commitHash = stdout.trim();
// 格式化,只截取前7位字符
const formathash = commitHash.substring(0, 7);
console.log(`Found the latest commit hash code: ${commitHash}`);
return formathash;
} catch (error) {
console.error("pnpm run fix-alpha-version ERROR", error);
}
}
/**
* @param string 传入格式化后的hash
* 将新的版本号写入文件 package.json
*/
async function updatePackageVersion(newVersion) {
// 获取内容根目录
const _dirname = process.cwd();
const packageJsonPath = path.join(_dirname, "package.json");
try {
const data = await fs.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(data);
const initversion = packageJson.version;
// 将匹配到的第一个 "alpha" => 具体的hash
const fixversion = initversion.replace("alpha", newVersion);
packageJson.version = fixversion;
// 写入版本号
await fs.writeFile(
packageJsonPath,
JSON.stringify(packageJson, null, 2),
"utf8",
);
console.log(`Alpha version update to: ${fixversion}`);
} catch (error) {
console.error("pnpm run fix-alpha-version ERROR", error);
}
}
const newVersion = await getLatestCommitHash();
updatePackageVersion(newVersion);
+44 -12
View File
@@ -5,7 +5,7 @@ import { extract } from "tar";
import path from "path";
import AdmZip from "adm-zip";
import fetch from "node-fetch";
import proxyAgent from "https-proxy-agent";
import { HttpsProxyAgent } from "https-proxy-agent";
import { execSync } from "child_process";
import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
import { glob } from "glob";
@@ -85,7 +85,7 @@ async function getLatestAlphaVersion() {
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
@@ -132,7 +132,7 @@ async function getLatestReleaseVersion() {
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
options.agent = new HttpsProxyAgent(httpProxy);
}
try {
const response = await fetch(META_VERSION_URL, {
@@ -153,13 +153,13 @@ async function getLatestReleaseVersion() {
*/
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(
`clash meta alpha unsupported platform "${platform}-${arch}"`
`clash meta alpha unsupported platform "${platform}-${arch}"`,
);
}
@@ -332,7 +332,7 @@ async function resolveResource(binInfo) {
process.env.https_proxy;
if (httpProxy) {
options.agent = proxyAgent(httpProxy);
options.agent = new HttpsProxyAgent(httpProxy);
}
const response = await fetch(url, {
@@ -354,7 +354,7 @@ const resolvePlugin = async () => {
const tempDir = path.join(TEMP_DIR, "SimpleSC");
const tempZip = path.join(
tempDir,
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip"
"NSIS_Simple_Service_Plugin_Unicode_1.30.zip",
);
const tempDll = path.join(tempDir, "SimpleSC.dll");
const pluginDir = path.join(process.env.APPDATA, "Local/NSIS");
@@ -398,6 +398,34 @@ const resolveServicePermission = async () => {
}
};
// 在 resolveResource 函数后添加新函数
async function resolveLocales() {
const srcLocalesDir = path.join(cwd, "src/locales");
const targetLocalesDir = path.join(cwd, "src-tauri/resources/locales");
try {
// 确保目标目录存在
await fsp.mkdir(targetLocalesDir, { recursive: true });
// 读取所有语言文件
const files = await fsp.readdir(srcLocalesDir);
// 复制每个文件
for (const file of files) {
const srcPath = path.join(srcLocalesDir, file);
const targetPath = path.join(targetLocalesDir, file);
await fsp.copyFile(srcPath, targetPath);
log_success(`Copied locale file: ${file}`);
}
log_success("All locale files copied successfully");
} catch (err) {
log_error("Error copying locale files:", err.message);
throw err;
}
}
/**
* main
*/
@@ -488,8 +516,8 @@ const tasks = [
{
name: "service_chmod",
func: resolveServicePermission,
retry: 1,
unixOnly: true,
retry: 5,
unixOnly: platform === "linux" || platform === "darwin",
},
{
name: "windows-sysproxy",
@@ -509,15 +537,20 @@ const tasks = [
retry: 5,
macosOnly: true,
},
{
name: "locales",
func: resolveLocales,
retry: 2,
},
];
async function runTask() {
const task = tasks.shift();
if (!task) return;
if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
if (task.winOnly && platform !== "win32") return runTask();
if (task.macosOnly && platform !== "darwin") return runTask();
if (task.linuxOnly && platform !== "linux") return runTask();
for (let i = 0; i < task.retry; i++) {
try {
@@ -532,4 +565,3 @@ async function runTask() {
}
runTask();
runTask();
+2 -2
View File
@@ -49,9 +49,9 @@ async function resolvePortable() {
zip.addLocalFolder(
path.join(
releaseDir,
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
),
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`
`Microsoft.WebView2.FixedVersionRuntime.109.0.1518.78.${arch}`,
);
zip.addLocalFolder(configDir, ".config");
+6 -6
View File
@@ -60,35 +60,35 @@ async function resolveUpdater() {
const { name, browser_download_url } = asset;
// win64 url
if (name.endsWith("x64-setup.nsis.zip")) {
if (name.endsWith("x64-setup.exe")) {
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")) {
if (name.endsWith("x64-setup.exe.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")) {
if (name.endsWith("x86-setup.exe")) {
updateData.platforms["windows-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url;
}
// win32 signature
if (name.endsWith("x86-setup.nsis.zip.sig")) {
if (name.endsWith("x86-setup.exe.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig;
}
// win arm url
if (name.endsWith("arm64-setup.nsis.zip")) {
if (name.endsWith("arm64-setup.exe")) {
updateData.platforms["windows-aarch64"].url = browser_download_url;
}
// win arm signature
if (name.endsWith("arm64-setup.nsis.zip.sig")) {
if (name.endsWith("arm64-setup.exe.sig")) {
const sig = await getSignature(browser_download_url);
updateData.platforms["windows-aarch64"].signature = sig;
}
+1217 -812
View File
File diff suppressed because it is too large Load Diff
+33 -24
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.0.0"
version = "2.1.2"
description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -9,34 +9,40 @@ default-run = "clash-verge"
edition = "2021"
build = "build.rs"
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies]
tauri-build = { version = "2.0.3", features = [] }
tauri-build = { version = "2.0.5", features = [] }
[dependencies]
warp = "0.3"
anyhow = "1.0"
dirs = "5.0"
dirs = "6.0"
open = "5.1"
log = "0.4"
dunce = "1.0"
log4rs = "1"
nanoid = "0.4"
chrono = "0.4"
sysinfo = "0.32.0"
boa_engine = "0.19.1"
sysinfo = "0.33.1"
boa_engine = "0.20.0"
serde_json = "1.0"
serde_yaml = "0.9"
once_cell = "1.19"
port_scanner = "0.1.5"
delay_timer = "0.11"
delay_timer = "0.11.6"
parking_lot = "0.12"
percent-encoding = "2.3.1"
window-shadows = { version = "0.2.2" }
tokio = { version = "1", features = ["full"] }
tokio = { version = "1.43", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" }
tauri = { version = "2.1.1", features = [
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
image = "0.24"
imageproc = "0.23"
rusttype = "0.9"
tauri = { version = "2.2.5", features = [
"protocol-asset",
"devtools",
"tray-icon",
@@ -44,36 +50,39 @@ tauri = { version = "2.1.1", features = [
"image-png",
] }
network-interface = { version = "2.0.0", features = ["serde"] }
tauri-plugin-shell = "2.0.2"
tauri-plugin-dialog = "2.0.2"
tauri-plugin-fs = "2.0.2"
tauri-plugin-notification = "2.0.1"
tauri-plugin-process = "2.0.1"
tauri-plugin-clipboard-manager = "2.0.1"
tauri-plugin-deep-link = "2.0.1"
tauri-plugin-shell = "2.2.0"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-notification = "2.2.1"
tauri-plugin-process = "2.2.0"
tauri-plugin-clipboard-manager = "2.2.1"
tauri-plugin-deep-link = "2.2.0"
tauri-plugin-devtools = "2.0.0-rc"
url = "2.5.2"
zip = "2.2.0"
url = "2.5.4"
zip = "2.2.2"
reqwest_dav = "0.1.14"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.2"
tokio-tungstenite = "0.26.1"
futures = "0.3"
sys-locale = "0.3.1"
[target.'cfg(windows)'.dependencies]
runas = "=1.2.0"
deelevate = "0.2.0"
winreg = "0.52.0"
url = "2.5.2"
winreg = "0.55.0"
url = "2.5.4"
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.0.0-rc"
tauri-plugin-global-shortcut = "2.0.1"
tauri-plugin-updater = "2.0.2"
tauri-plugin-window-state = "2.0.2"
tauri-plugin-autostart = "2.2.0"
tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-updater = "2.3.0"
tauri-plugin-window-state = "2.2.1"
#openssl
[features]
BIN
View File
Binary file not shown.
+7
View File
@@ -6,6 +6,13 @@
"permissions": [
"global-shortcut:default",
"updater:default",
"dialog:default",
"dialog:allow-ask",
"dialog:allow-message",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:allow-restart",
"deep-link:default",
"window-state:default",
"window-state:default",
+6
View File
@@ -16,6 +16,12 @@
"identifier": "fs:scope",
"allow": ["$APPDATA/**", "$RESOURCE/../**", "**"]
},
"fs:allow-app-read",
"fs:allow-app-read-recursive",
"fs:allow-appcache-read",
"fs:allow-appcache-read-recursive",
"fs:allow-appconfig-read",
"fs:allow-appconfig-read-recursive",
"core:window:allow-create",
"core:window:allow-center",
"core:window:allow-request-user-attention",
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.application-groups</key>
<array>
<string>io.github.clash-verge-rev.clash-verge-rev</string>
</array>
<key>com.apple.security.inherit</key>
<true/>
</dict>
</plist>
+109 -5
View File
@@ -697,25 +697,62 @@ Var VC_REDIST_URL
Var VC_REDIST_EXE
Section CheckAndInstallVSRuntime
; 检查是否已安装 Visual C++ Redistributable
${If} ${IsNativeARM64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.arm64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.arm64.exe"
IfFileExists "$SYSDIR\msvcp140.dll" Done
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${ElseIf} ${RunningX64}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x64.exe"
IfFileExists "$WINDIR\SysWOW64\msvcp140.dll" Done
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${Else}
StrCpy $VC_REDIST_URL "https://aka.ms/vs/17/release/vc_redist.x86.exe"
StrCpy $VC_REDIST_EXE "vc_redist.x86.exe"
IfFileExists "$SYSDIR\msvcp140.dll" Done
; 检查关键DLL
IfFileExists "$SYSDIR\vcruntime140.dll" 0 checkInstall
IfFileExists "$SYSDIR\msvcp140.dll" Done checkInstall
${EndIf}
; 下载并安装VC运行库
checkInstall:
; 检查注册表
${If} ${RunningX64}
SetRegView 64
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\${ARCH}" "Installed"
${If} $R0 == "1"
Goto Done
${EndIf}
${Else}
ReadRegDword $R0 HKLM "SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86" "Installed"
${If} $R0 == "1"
Goto Done
${EndIf}
${EndIf}
; 如果没有安装,则下载并安装
DetailPrint "正在下载 Visual C++ Redistributable..."
nsisdl::download "$VC_REDIST_URL" "$TEMP\$VC_REDIST_EXE"
Pop $0
${If} $0 == "success"
nsExec::Exec '"$TEMP\$VC_REDIST_EXE" /quiet /norestart'
DetailPrint "正在安装 Visual C++ Redistributable..."
ExecWait '"$TEMP\$VC_REDIST_EXE" /quiet /norestart' $0
${If} $0 == 0
DetailPrint "Visual C++ Redistributable 安装成功"
${Else}
DetailPrint "Visual C++ Redistributable 安装失败"
${EndIf}
Delete "$TEMP\$VC_REDIST_EXE"
${Else}
DetailPrint "Visual C++ Redistributable 下载失败"
${EndIf}
Done:
@@ -728,6 +765,40 @@ Section Install
nsExec::Exec 'netsh int tcp res'
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete old files before installation
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Copy main executable
File "${MAINBINARYSRCPATH}"
@@ -848,6 +919,35 @@ Section Uninstall
!insertmacro CheckIfAppIsRunning
!insertmacro CheckAllVergeProcesses
!insertmacro RemoveVergeService
; 清理自启动注册表项
DetailPrint "Cleaning auto-launch registry entries..."
StrCpy $R1 "Software\Microsoft\Windows\CurrentVersion\Run"
SetRegView 64
; 清理旧版本的注册表项 (Clash Verge)
ReadRegStr $R2 HKCU "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "Clash Verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "Clash Verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "Clash Verge"
${EndIf}
; 清理新版本的注册表项 (clash-verge)
ReadRegStr $R2 HKCU "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKCU "$R1" "clash-verge"
${EndIf}
ReadRegStr $R2 HKLM "$R1" "clash-verge"
${If} $R2 != ""
DeleteRegValue HKLM "$R1" "clash-verge"
${EndIf}
; Delete the app directory and its content from disk
; Copy main executable
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
@@ -862,6 +962,10 @@ Section Uninstall
Delete "$INSTDIR\\{{this}}"
{{/each}}
; Delete clash-verge.desktop
IfFileExists "$INSTDIR\Clash Verge.exe" 0 +2
Delete "$INSTDIR\Clash Verge.exe"
; Delete uninstaller
Delete "$INSTDIR\uninstall.exe"
+294 -24
View File
@@ -4,7 +4,7 @@ use crate::{
feat,
utils::{dirs, help},
};
use crate::{ret_err, wrap_err};
use crate::{log_err, ret_err, wrap_err};
use anyhow::{Context, Result};
use network_interface::NetworkInterface;
use serde_yaml::Mapping;
@@ -13,6 +13,7 @@ use sysproxy::{Autoproxy, Sysproxy};
type CmdResult<T = ()> = Result<T, String>;
use reqwest_dav::list_cmd::ListFile;
use tauri::Manager;
use std::fs;
#[tauri::command]
pub fn copy_clash_env() -> CmdResult {
@@ -22,14 +23,30 @@ pub fn copy_clash_env() -> CmdResult {
#[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> {
let _ = tray::Tray::global().update_menu();
Ok(Config::profiles().data().clone())
}
#[tauri::command]
pub async fn enhance_profiles() -> CmdResult {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
Ok(())
match CoreManager::global().update_config().await {
Ok((true, _)) => {
println!("[enhance_profiles] 配置更新成功");
log_err!(tray::Tray::global().update_tooltip());
handle::Handle::refresh_clash();
Ok(())
}
Ok((false, error_msg)) => {
println!("[enhance_profiles] 配置验证失败: {}", error_msg);
handle::Handle::notice_message("config_validate::error", &error_msg);
Ok(())
}
Err(e) => {
println!("[enhance_profiles] 更新过程发生错误: {}", e);
handle::Handle::notice_message("config_validate::process_terminated", &e.to_string());
Ok(())
}
}
}
#[tauri::command]
@@ -61,36 +78,80 @@ pub async fn delete_profile(index: String) -> CmdResult {
wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash();
}
Ok(())
}
/// 修改profiles的
/// 修改profiles的配置
#[tauri::command]
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
pub async fn patch_profiles_config(
profiles: IProfiles
) -> CmdResult<bool> {
println!("[cmd配置patch] 开始修改配置文件");
// 保存当前配置,以便在验证失败时恢复
let current_profile = Config::profiles().latest().current.clone();
println!("[cmd配置patch] 当前配置: {:?}", current_profile);
// 更新profiles配置
println!("[cmd配置patch] 正在更新配置草稿");
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
// 更新配置并进行验证
match CoreManager::global().update_config().await {
Ok(_) => {
Ok((true, _)) => {
println!("[cmd配置patch] 配置更新成功");
handle::Handle::refresh_clash();
let _ = handle::Handle::update_systray_part();
let _ = tray::Tray::global().update_tooltip();
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
Ok(())
Ok(true)
}
Err(err) => {
Ok((false, error_msg)) => {
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
Config::profiles().discard();
log::error!(target: "app", "{err}");
Err(format!("{err}"))
// 如果验证失败,恢复到之前的配置
if let Some(prev_profile) = current_profile {
println!("[cmd配置patch] 尝试恢复到之前的配置: {}", prev_profile);
let restore_profiles = IProfiles {
current: Some(prev_profile),
items: None,
};
// 静默恢复,不触发验证
wrap_err!({ Config::profiles().draft().patch_config(restore_profiles) })?;
Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?;
println!("[cmd配置patch] 成功恢复到之前的配置");
}
// 发送验证错误通知
handle::Handle::notice_message("config_validate::error", &error_msg);
Ok(false)
}
Err(e) => {
println!("[cmd配置patch] 更新过程发生错误: {}", e);
Config::profiles().discard();
handle::Handle::notice_message("config_validate::boot_error", &e.to_string());
Ok(false)
}
}
}
/// 根据profile name修改profiles
#[tauri::command]
pub async fn patch_profiles_config_by_profile_index(
_app_handle: tauri::AppHandle,
profile_index: String
) -> CmdResult<bool> {
let profiles = IProfiles{current: Some(profile_index), items: None};
patch_profiles_config(profiles).await
}
/// 修改某个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())
Ok(())
}
#[tauri::command]
@@ -118,17 +179,107 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
let data = wrap_err!(item.read_file())?;
Ok(data)
}
/// 保存profiles的配置
#[tauri::command]
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
pub async fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
if file_data.is_none() {
return 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()))
// 在异步操作前完成所有文件操作
let (file_path, original_content, is_merge_file) = {
let profiles = Config::profiles();
let profiles_guard = profiles.latest();
let item = wrap_err!(profiles_guard.get_item(&index))?;
// 确定是否为merge类型文件
let is_merge = item.itype.as_ref().map_or(false, |t| t == "merge");
let content = wrap_err!(item.read_file())?;
let path = item.file.clone().ok_or("file field is null")?;
let profiles_dir = wrap_err!(dirs::app_profiles_dir())?;
(profiles_dir.join(path), content, is_merge)
};
// 保存新的配置文件
wrap_err!(fs::write(&file_path, file_data.clone().unwrap()))?;
let file_path_str = file_path.to_string_lossy().to_string();
println!("[cmd配置save] 开始验证配置文件: {}, 是否为merge文件: {}", file_path_str, is_merge_file);
// 对于 merge 文件,只进行语法验证,不进行后续内核验证
if is_merge_file {
println!("[cmd配置save] 检测到merge文件,只进行语法验证");
match CoreManager::global().validate_config_file(&file_path_str, Some(true)).await {
Ok((true, _)) => {
println!("[cmd配置save] merge文件语法验证通过");
// 成功后尝试更新整体配置
if let Err(e) = CoreManager::global().update_config().await {
println!("[cmd配置save] 更新整体配置时发生错误: {}", e);
log::warn!(target: "app", "更新整体配置时发生错误: {}", e);
}
return Ok(());
}
Ok((false, error_msg)) => {
println!("[cmd配置save] merge文件语法验证失败: {}", error_msg);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 发送合并文件专用错误通知
let result = (false, error_msg.clone());
handle_yaml_validation_notice(&result, "合并配置文件");
return Ok(());
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
return Err(e.to_string());
}
}
}
// 非merge文件使用完整验证流程
match CoreManager::global().validate_config_file(&file_path_str, None).await {
Ok((true, _)) => {
println!("[cmd配置save] 验证成功");
Ok(())
}
Ok((false, error_msg)) => {
println!("[cmd配置save] 验证失败: {}", error_msg);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
// 智能判断错误类型
let is_script_error = file_path_str.ends_with(".js") ||
error_msg.contains("Script syntax error") ||
error_msg.contains("Script must contain a main function") ||
error_msg.contains("Failed to read script file");
if error_msg.contains("YAML syntax error") ||
error_msg.contains("Failed to read file:") ||
(!file_path_str.ends_with(".js") && !is_script_error) {
// 普通YAML错误使用YAML通知处理
println!("[cmd配置save] YAML配置文件验证失败,发送通知");
let result = (false, error_msg.clone());
handle_yaml_validation_notice(&result, "YAML配置文件");
} else if is_script_error {
// 脚本错误使用专门的通知处理
println!("[cmd配置save] 脚本文件验证失败,发送通知");
let result = (false, error_msg.clone());
handle_script_validation_notice(&result, "脚本文件");
} else {
// 普通配置错误使用一般通知
println!("[cmd配置save] 其他类型验证失败,发送一般通知");
handle::Handle::notice_message("config_validate::error", &error_msg);
}
Ok(())
}
Err(e) => {
println!("[cmd配置save] 验证过程发生错误: {}", e);
// 恢复原始配置文件
wrap_err!(fs::write(&file_path, original_content))?;
Err(e.to_string())
}
}
}
#[tauri::command]
@@ -168,6 +319,12 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
wrap_err!(feat::patch_clash(payload).await)
}
#[tauri::command]
pub async fn patch_clash_mode(payload: String) -> CmdResult {
Ok(feat::change_clash_mode(payload))
}
#[tauri::command]
pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge();
@@ -177,12 +334,27 @@ pub fn get_verge_config() -> CmdResult<IVergeResponse> {
#[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
wrap_err!(feat::patch_verge(payload).await)
wrap_err!(feat::patch_verge(payload, false).await)
}
#[tauri::command]
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
wrap_err!(CoreManager::global().change_core(clash_core).await)
pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
log::info!(target: "app", "changing core to {clash_core}");
match CoreManager::global().change_core(Some(clash_core.clone())).await {
Ok(_) => {
log::info!(target: "app", "core changed to {clash_core}");
handle::Handle::notice_message("config_core::change_success", &clash_core);
handle::Handle::refresh_clash();
Ok(None)
}
Err(err) => {
let error_msg = err.to_string();
log::error!(target: "app", "failed to change core: {error_msg}");
handle::Handle::notice_message("config_core::change_error", &error_msg);
Ok(Some(error_msg))
}
}
}
/// restart the sidecar
@@ -426,3 +598,101 @@ pub mod uwp {
Ok(())
}
}
#[tauri::command]
pub async fn script_validate_notice(status: String, msg: String) -> CmdResult {
handle::Handle::notice_message(&status, &msg);
Ok(())
}
/// 处理脚本验证相关的所有消息通知
/// 统一通知接口,保持消息类型一致性
pub fn handle_script_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 {
let error_msg = &result.1;
// 根据错误消息内容判断错误类型
let status = if error_msg.starts_with("File not found:") {
"config_validate::file_not_found"
} else if error_msg.starts_with("Failed to read script file:") {
"config_validate::script_error"
} else if error_msg.starts_with("Script syntax error:") {
"config_validate::script_syntax_error"
} else if error_msg == "Script must contain a main function" {
"config_validate::script_missing_main"
} else {
// 如果是其他类型错误,作为一般脚本错误处理
"config_validate::script_error"
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
handle::Handle::notice_message(status, error_msg);
}
}
/// 验证指定脚本文件
#[tauri::command]
pub async fn validate_script_file(file_path: String) -> CmdResult<bool> {
log::info!(target: "app", "验证脚本文件: {}", file_path);
match CoreManager::global().validate_config_file(&file_path, None).await {
Ok(result) => {
handle_script_validation_notice(&result, "脚本文件");
Ok(result.0) // 返回验证结果布尔值
},
Err(e) => {
let error_msg = e.to_string();
log::error!(target: "app", "验证脚本文件过程发生错误: {}", error_msg);
handle::Handle::notice_message("config_validate::process_terminated", &error_msg);
Ok(false)
}
}
}
/// 处理YAML验证相关的所有消息通知
/// 统一通知接口,保持消息类型一致性
pub fn handle_yaml_validation_notice(result: &(bool, String), file_type: &str) {
if !result.0 {
let error_msg = &result.1;
println!("[通知] 处理{}验证错误: {}", file_type, error_msg);
// 检查是否为merge文件
let is_merge_file = file_type.contains("合并");
// 根据错误消息内容判断错误类型
let status = if error_msg.starts_with("File not found:") {
"config_validate::file_not_found"
} else if error_msg.starts_with("Failed to read file:") {
"config_validate::yaml_read_error"
} else if error_msg.starts_with("YAML syntax error:") {
if is_merge_file {
"config_validate::merge_syntax_error"
} else {
"config_validate::yaml_syntax_error"
}
} else if error_msg.contains("mapping values are not allowed") {
if is_merge_file {
"config_validate::merge_mapping_error"
} else {
"config_validate::yaml_mapping_error"
}
} else if error_msg.contains("did not find expected key") {
if is_merge_file {
"config_validate::merge_key_error"
} else {
"config_validate::yaml_key_error"
}
} else {
// 如果是其他类型错误,根据文件类型作为一般错误处理
if is_merge_file {
"config_validate::merge_error"
} else {
"config_validate::yaml_error"
}
};
log::warn!(target: "app", "{} 验证失败: {}", file_type, error_msg);
println!("[通知] 发送通知: status={}, msg={}", status, error_msg);
handle::Handle::notice_message(status, error_msg);
}
}
+6 -3
View File
@@ -33,7 +33,7 @@ impl IClashTemp {
let mut map = Mapping::new();
let mut tun = Mapping::new();
tun.insert("enable".into(), false.into());
tun.insert("stack".into(), "gvisor".into());
tun.insert("stack".into(), "mixed".into());
tun.insert("auto-route".into(), true.into());
tun.insert("strict-route".into(), false.into());
tun.insert("auto-detect-interface".into(), true.into());
@@ -156,17 +156,20 @@ impl IClashTemp {
}
pub fn guard_mixed_port(config: &Mapping) -> u16 {
let mut port = config
.get("mixed-port")
let raw_value = config.get("mixed-port");
let mut port = raw_value
.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
}
+50 -11
View File
@@ -3,10 +3,12 @@ use crate::{
config::PrfItem,
enhance,
utils::{dirs, help},
core::{handle, CoreManager},
};
use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell;
use std::path::PathBuf;
use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
@@ -64,20 +66,57 @@ impl Config {
let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?;
}
crate::log_err!(Self::generate().await);
if let Err(err) = Self::generate_file(ConfigType::Run) {
log::error!(target: "app", "{err}");
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"),
)?;
// 生成运行时配置
crate::log_err!(Self::generate().await);
// 生成运行时配置文件并验证
let config_result = Self::generate_file(ConfigType::Run);
let validation_result = if let Ok(_) = config_result {
// 验证配置文件
println!("[首次启动] 开始验证配置");
match CoreManager::global().validate_config().await {
Ok((is_valid, error_msg)) => {
if !is_valid {
println!("[首次启动] 配置验证失败,使用默认最小配置启动: {}", error_msg);
CoreManager::global()
.use_default_config("config_validate::boot_error", &error_msg)
.await?;
Some(("config_validate::boot_error", error_msg))
} else {
println!("[首次启动] 配置验证成功");
Some(("config_validate::success", String::new()))
}
}
Err(err) => {
println!("[首次启动] 验证进程执行失败: {}", err);
CoreManager::global()
.use_default_config("config_validate::process_terminated", "")
.await?;
Some(("config_validate::process_terminated", String::new()))
}
}
} else {
println!("[首次启动] 生成配置文件失败,使用默认配置");
CoreManager::global()
.use_default_config(
"config_validate::error",
"",
)
.await?;
Some(("config_validate::error", String::new()))
};
// 在单独的任务中发送通知
if let Some((msg_type, msg_content)) = validation_result {
tauri::async_runtime::spawn(async move {
sleep(Duration::from_secs(2)).await;
handle::Handle::notice_message(msg_type, &msg_content);
});
}
Ok(())
}
+19 -1
View File
@@ -71,7 +71,6 @@ impl IProfiles {
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;
}
@@ -465,4 +464,23 @@ impl IProfiles {
_ => None,
}
}
/// 判断profile是否是current指向的
pub fn is_current_profile_index(&self, index: String) -> bool {
self.current == Some(index)
}
/// 获取所有的profiles(uid,名称)
pub fn all_profile_uid_and_name(&self) -> Option<Vec<(String, String)>> {
match self.items.as_ref() {
Some(items) => Some(items.iter().filter_map(|e| {
if let (Some(uid), Some(name)) = (e.uid.clone(), e.name.clone()) {
Some((uid, name))
} else {
None
}
}).collect()),
None => None,
}
}
}
+37 -1
View File
@@ -1,5 +1,6 @@
use crate::config::DEFAULT_PAC;
use crate::config::{deserialize_encrypted, serialize_encrypted};
use crate::utils::i18n;
use crate::utils::{dirs, help};
use anyhow::Result;
use log::LevelFilter;
@@ -97,6 +98,9 @@ pub struct IVerge {
/// hotkey map
/// format: {func},{key}
pub hotkeys: Option<Vec<String>>,
/// enable global hotkey
pub enable_global_hotkey: Option<bool>,
/// 切换代理时自动关闭连接
pub auto_close_connection: Option<bool>,
@@ -175,6 +179,11 @@ pub struct IVerge {
default
)]
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
/// 轻量模式 - 只保留内核运行
pub enable_lite_mode: Option<bool>,
}
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -202,6 +211,21 @@ pub struct IVergeTheme {
}
impl IVerge {
fn get_system_language() -> String {
let sys_lang = sys_locale::get_locale()
.unwrap_or_else(|| String::from("en"))
.to_lowercase();
let lang_code = sys_lang.split(['_', '-']).next().unwrap_or("en");
let supported_languages = i18n::get_supported_languages();
if supported_languages.contains(&lang_code.to_string()) {
lang_code.to_string()
} else {
String::from("en")
}
}
pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config,
@@ -215,7 +239,7 @@ impl IVerge {
pub fn template() -> Self {
Self {
clash_core: Some("verge-mihomo".into()),
language: Some("zh".into()),
language: Some(Self::get_system_language()),
theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))]
env_type: Some("bash".into()),
@@ -260,6 +284,9 @@ impl IVerge {
webdav_url: None,
webdav_username: None,
webdav_password: None,
enable_tray_speed: Some(true),
enable_global_hotkey: Some(true),
enable_lite_mode: Some(false),
..Self::default()
}
}
@@ -326,6 +353,7 @@ impl IVerge {
patch!(web_ui_list);
patch!(clash_core);
patch!(hotkeys);
patch!(enable_global_hotkey);
patch!(auto_close_connection);
patch!(auto_check_update);
@@ -339,6 +367,8 @@ impl IVerge {
patch!(webdav_url);
patch!(webdav_username);
patch!(webdav_password);
patch!(enable_tray_speed);
patch!(enable_lite_mode);
}
/// 在初始化前尝试拿到单例端口的值
@@ -391,6 +421,7 @@ pub struct IVergeResponse {
pub enable_silent_start: Option<bool>,
pub enable_system_proxy: Option<bool>,
pub enable_proxy_guard: Option<bool>,
pub enable_global_hotkey: Option<bool>,
pub use_default_bypass: Option<bool>,
pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>,
@@ -425,6 +456,8 @@ pub struct IVergeResponse {
pub webdav_url: Option<String>,
pub webdav_username: Option<String>,
pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
pub enable_lite_mode: Option<bool>,
}
impl From<IVerge> for IVergeResponse {
@@ -451,6 +484,7 @@ impl From<IVerge> for IVergeResponse {
enable_silent_start: verge.enable_silent_start,
enable_system_proxy: verge.enable_system_proxy,
enable_proxy_guard: verge.enable_proxy_guard,
enable_global_hotkey: verge.enable_global_hotkey,
use_default_bypass: verge.use_default_bypass,
system_proxy_bypass: verge.system_proxy_bypass,
proxy_guard_duration: verge.proxy_guard_duration,
@@ -485,6 +519,8 @@ impl From<IVerge> for IVergeResponse {
webdav_url: verge.webdav_url,
webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password,
enable_tray_speed: verge.enable_tray_speed,
enable_lite_mode: verge.enable_lite_mode,
}
}
}
+137 -67
View File
@@ -4,114 +4,184 @@ use anyhow::Error;
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reqwest_dav::list_cmd::{ListEntity, ListFile};
use std::collections::HashMap;
use std::env::{consts::OS, temp_dir};
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use zip::write::SimpleFileOptions;
const TIMEOUT_UPLOAD: u64 = 300; // 上传超时 5 分钟
const TIMEOUT_DOWNLOAD: u64 = 300; // 下载超时 5 分钟
const TIMEOUT_LIST: u64 = 3; // 列表超时 30 秒
const TIMEOUT_DELETE: u64 = 3; // 删除超时 30 秒
#[derive(Clone)]
struct WebDavConfig {
url: String,
username: String,
password: String,
}
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
enum Operation {
Upload,
Download,
List,
Delete,
}
impl Operation {
fn timeout(&self) -> u64 {
match self {
Operation::Upload => TIMEOUT_UPLOAD,
Operation::Download => TIMEOUT_DOWNLOAD,
Operation::List => TIMEOUT_LIST,
Operation::Delete => TIMEOUT_DELETE,
}
}
}
pub struct WebDavClient {
client: Arc<Mutex<Option<reqwest_dav::Client>>>,
config: Arc<Mutex<Option<WebDavConfig>>>,
clients: Arc<Mutex<HashMap<Operation, reqwest_dav::Client>>>,
}
impl WebDavClient {
pub fn global() -> &'static WebDavClient {
static WEBDAV_CLIENT: OnceCell<WebDavClient> = OnceCell::new();
WEBDAV_CLIENT.get_or_init(|| WebDavClient {
client: Arc::new(Mutex::new(None)),
config: Arc::new(Mutex::new(None)),
clients: Arc::new(Mutex::new(HashMap::new())),
})
}
async fn get_client(&self) -> Result<reqwest_dav::Client, Error> {
if self.client.lock().is_none() {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg =
"Unable to create web dav client, please make sure the webdav config is correct"
.to_string();
log::error!(target: "app","{}",msg);
return Err(anyhow::Error::msg(msg));
async fn get_client(&self, op: Operation) -> Result<reqwest_dav::Client, Error> {
// 先尝试从缓存获取
{
let clients = self.clients.lock();
if let Some(client) = clients.get(&op) {
return Ok(client.clone());
}
let url = verge.webdav_url.unwrap_or_default();
let username = verge.webdav_username.unwrap_or_default();
let password = verge.webdav_password.unwrap_or_default();
let url = url.trim_end_matches('/');
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(3))
.build()
.unwrap(),
)
.set_host(url.to_owned())
.set_auth(reqwest_dav::Auth::Basic(
username.to_owned(),
password.to_owned(),
))
.build()?;
if (client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await)
.is_err()
{
client.mkcol(dirs::BACKUP_DIR).await?;
}
*self.client.lock() = Some(client.clone());
}
Ok(self.client.lock().clone().unwrap())
// 获取或创建配置
let config = {
let mut lock = self.config.lock();
if let Some(cfg) = lock.as_ref() {
cfg.clone()
} else {
let verge = Config::verge().latest().clone();
if verge.webdav_url.is_none()
|| verge.webdav_username.is_none()
|| verge.webdav_password.is_none()
{
let msg = "Unable to create web dav client, please make sure the webdav config is correct".to_string();
return Err(anyhow::Error::msg(msg));
}
let config = WebDavConfig {
url: verge
.webdav_url
.unwrap_or_default()
.trim_end_matches('/')
.to_string(),
username: verge.webdav_username.unwrap_or_default(),
password: verge.webdav_password.unwrap_or_default(),
};
*lock = Some(config.clone());
config
}
};
// 创建新的客户端
let client = reqwest_dav::ClientBuilder::new()
.set_agent(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(op.timeout()))
.build()
.unwrap(),
)
.set_host(config.url)
.set_auth(reqwest_dav::Auth::Basic(config.username, config.password))
.build()?;
// 确保备份目录存在
let list_result = client
.list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(0))
.await;
if list_result.is_err() {
client.mkcol(dirs::BACKUP_DIR).await?;
}
// 缓存客户端
{
let mut clients = self.clients.lock();
clients.insert(op, client.clone());
}
Ok(client)
}
pub fn reset(&self) {
if !self.client.lock().is_none() {
self.client.lock().take();
}
*self.config.lock() = None;
self.clients.lock().clear();
}
pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::Upload).await?;
let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client
.put(webdav_path.as_ref(), fs::read(file_path)?)
.await?;
let fut = client.put(webdav_path.as_ref(), fs::read(file_path)?);
timeout(Duration::from_secs(TIMEOUT_UPLOAD), fut).await??;
Ok(())
}
pub async fn download(&self, filename: String, storage_path: PathBuf) -> Result<(), Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::Download).await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, filename);
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
let fut = async {
let response = client.get(path.as_str()).await?;
let content = response.bytes().await?;
fs::write(&storage_path, &content)?;
Ok::<(), Error>(())
};
timeout(Duration::from_secs(TIMEOUT_DOWNLOAD), fut).await??;
Ok(())
}
pub async fn list(&self) -> Result<Vec<ListFile>, Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::List).await?;
let path = format!("{}/", dirs::BACKUP_DIR);
let files = client
.list(path.as_str(), reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
let fut = async {
let files = client
.list(path.as_str(), reqwest_dav::Depth::Number(1))
.await?;
let mut final_files = Vec::new();
for file in files {
if let ListEntity::File(file) = file {
final_files.push(file);
}
}
}
Ok(final_files)
Ok::<Vec<ListFile>, Error>(final_files)
};
timeout(Duration::from_secs(TIMEOUT_LIST), fut).await?
}
pub async fn delete(&self, file_name: String) -> Result<(), Error> {
let client = self.get_client().await?;
let client = self.get_client(Operation::Delete).await?;
let path = format!("{}/{}", dirs::BACKUP_DIR, file_name);
client.delete(&path).await?;
let fut = client.delete(&path);
timeout(Duration::from_secs(TIMEOUT_DELETE), fut).await??;
Ok(())
}
}
+16 -1
View File
@@ -5,6 +5,12 @@ use serde::{Deserialize, Serialize};
use serde_yaml::Mapping;
use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Rate {
pub up: u64,
pub down: u64,
}
/// PUT /configs
/// path 是绝对路径
pub async fn put_configs(path: &str) -> Result<()> {
@@ -21,7 +27,9 @@ pub async fn put_configs(path: &str) -> Result<()> {
match response.status().as_u16() {
204 => Ok(()),
status => {
bail!("failed to put configs with status \"{status}\"")
let body = response.text().await?;
// print!("failed to put configs with status \"{}\"\n{}\n{}", status, url, body);
bail!("failed to put configs with status \"{status}\"\n{url}\n{body}");
}
}
}
@@ -123,6 +131,13 @@ pub fn parse_check_output(log: String) -> String {
log
}
#[cfg(target_os = "macos")]
pub fn get_traffic_ws_url() -> Result<String> {
let (url, _) = clash_client_info()?;
let ws_url = url.replace("http://", "ws://") + "/traffic";
Ok(ws_url)
}
#[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 {}'""#;
+367 -60
View File
@@ -1,7 +1,9 @@
use crate::config::*;
use crate::core::{clash_api, handle, service};
#[cfg(target_os = "macos")]
use crate::core::tray::Tray;
use crate::log_err;
use crate::utils::dirs;
use crate::utils::{dirs, help};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use serde_yaml::Mapping;
@@ -31,28 +33,6 @@ impl CoreManager {
Ok(())
}
/// 检查订阅是否正确
pub async 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("verge-mihomo".into());
let test_dir = dirs::app_home_dir()?.join("test");
let test_dir = dirs::path_to_str(&test_dir)?;
let app_handle = handle::Handle::global().app_handle().unwrap();
let _ = app_handle
.shell()
.sidecar(clash_core)?
.args(["-t", "-d", test_dir, "-f", config_path])
.output()
.await?;
Ok(())
}
/// 停止核心运行
pub async fn stop_core(&self) -> Result<()> {
let mut running = self.running.lock().await;
@@ -76,7 +56,6 @@ impl CoreManager {
service::stop_core_by_service().await?;
}
*running = false;
Ok(())
}
@@ -94,8 +73,13 @@ impl CoreManager {
if service::check_service().await.is_ok() {
log::info!(target: "app", "try to run core in service mode");
service::run_core_by_service(&config_path).await?;
*running = true;
}
// 流量订阅
#[cfg(target_os = "macos")]
log_err!(Tray::global().subscribe_traffic().await);
*running = true;
Ok(())
}
@@ -107,6 +91,23 @@ impl CoreManager {
Ok(())
}
/// 使用默认配置
pub async fn use_default_config(&self, msg_type: &str, msg_content: &str) -> Result<()> {
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
*Config::runtime().draft() = IRuntime {
config: Some(Config::clash().latest().0.clone()),
exists_keys: vec![],
chain_logs: Default::default(),
};
help::save_yaml(
&runtime_path,
&Config::clash().latest().0,
Some("# Clash Verge Runtime"),
)?;
handle::Handle::notice_message(msg_type, msg_content);
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"))?;
@@ -117,57 +118,363 @@ impl CoreManager {
}
log::info!(target: "app", "change core to `{clash_core}`");
// 1. 先更新内核配置(但不应用)
Config::verge().draft().clash_core = Some(clash_core);
// 更新订阅
Config::generate().await?;
self.check_config().await?;
match self.restart_core().await {
Ok(_) => {
// 2. 使用新内核验证配置
println!("[切换内核] 使用新内核验证配置");
match self.validate_config().await {
Ok((true, _)) => {
println!("[切换内核] 配置验证通过,开始切换内核");
// 3. 验证通过后,应用内核配置并重启
Config::verge().apply();
Config::runtime().apply();
log_err!(Config::verge().latest().save_file());
Ok(())
match self.restart_core().await {
Ok(_) => {
println!("[切换内核] 内核切换成功");
Config::runtime().apply();
Ok(())
}
Err(err) => {
println!("[切换内核] 内核切换失败: {}", err);
Config::verge().discard();
Config::runtime().discard();
Err(err)
}
}
}
Ok((false, error_msg)) => {
println!("[切换内核] 配置验证失败: {}", error_msg);
// 使用默认配置并继续切换内核
self.use_default_config("config_validate::core_change", &error_msg).await?;
Config::verge().apply();
log_err!(Config::verge().latest().save_file());
match self.restart_core().await {
Ok(_) => {
println!("[切换内核] 内核切换成功(使用默认配置)");
Ok(())
}
Err(err) => {
println!("[切换内核] 内核切换失败: {}", err);
Config::verge().discard();
Err(err)
}
}
}
Err(err) => {
println!("[切换内核] 验证过程发生错误: {}", 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().await?;
/// 内部验证配置文件的实现
async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
println!("[core配置验证] 开始验证配置文件: {}", config_path);
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
println!("[core配置验证] 使用内核: {}", clash_core);
let app_handle = handle::Handle::global().app_handle().unwrap();
let test_dir = dirs::app_home_dir()?.join("test");
let test_dir = dirs::path_to_str(&test_dir)?;
println!("[core配置验证] 测试目录: {}", test_dir);
// 检查订阅是否正常
self.check_config().await?;
// 使用子进程运行clash验证配置
println!("[core配置验证] 运行子进程验证配置");
let output = app_handle
.shell()
.sidecar(clash_core)?
.args(["-t", "-d", test_dir, "-f", config_path])
.output()
.await?;
// 更新运行时订阅
let path = Config::generate_file(ConfigType::Run)?;
let path = dirs::path_to_str(&path)?;
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
// 检查进程退出状态和错误输出
let error_keywords = ["FATA", "fatal", "Parse config error", "level=fatal"];
let has_error = !output.status.success() || error_keywords.iter().any(|&kw| stderr.contains(kw));
println!("\n[core配置验证] -------- 验证结果 --------");
println!("[core配置验证] 进程退出状态: {:?}", output.status);
if !stderr.is_empty() {
println!("[core配置验证] stderr输出:\n{}", stderr);
}
if !stdout.is_empty() {
println!("[core配置验证] stdout输出:\n{}", stdout);
}
// 发送请求 发送5次
for i in 0..10 {
match clash_api::put_configs(path).await {
Ok(_) => break,
if has_error {
println!("[core配置验证] 发现错误,开始处理错误信息");
let error_msg = if !stdout.is_empty() {
stdout.to_string()
} else if !stderr.is_empty() {
stderr.to_string()
} else if let Some(code) = output.status.code() {
format!("验证进程异常退出,退出码: {}", code)
} else {
"验证进程被终止".to_string()
};
println!("[core配置验证] -------- 验证结束 --------\n");
Ok((false, error_msg)) // 返回错误消息给调用者处理
} else {
println!("[core配置验证] 验证成功");
println!("[core配置验证] -------- 验证结束 --------\n");
Ok((true, String::new()))
}
}
/// 验证运行时配置
pub async fn validate_config(&self) -> Result<(bool, String)> {
let config_path = Config::generate_file(ConfigType::Check)?;
let config_path = dirs::path_to_str(&config_path)?;
self.validate_config_internal(config_path).await
}
/// 验证指定的配置文件
pub async fn validate_config_file(&self, config_path: &str, is_merge_file: Option<bool>) -> Result<(bool, String)> {
// 检查文件是否存在
if !std::path::Path::new(config_path).exists() {
let error_msg = format!("File not found: {}", config_path);
//handle::Handle::notice_message("config_validate::file_not_found", &error_msg);
return Ok((false, error_msg));
}
// 如果是合并文件且不是强制验证,执行语法检查但不进行完整验证
if is_merge_file.unwrap_or(false) {
println!("[core配置验证] 检测到Merge文件,仅进行语法检查: {}", config_path);
return self.validate_file_syntax(config_path).await;
}
// 检查是否为脚本文件
let is_script = if config_path.ends_with(".js") {
true
} else {
match self.is_script_file(config_path) {
Ok(result) => result,
Err(err) => {
if i < 9 {
log::info!(target: "app", "{err}");
} else {
bail!(err);
}
// 如果无法确定文件类型,尝试使用Clash内核验证
log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err);
return self.validate_config_internal(config_path).await;
}
}
sleep(Duration::from_millis(100)).await;
};
if is_script {
log::info!(target: "app", "检测到脚本文件,使用JavaScript验证: {}", config_path);
return self.validate_script_file(config_path).await;
}
Ok(())
// 对YAML配置文件使用Clash内核验证
log::info!(target: "app", "使用Clash内核验证配置文件: {}", config_path);
self.validate_config_internal(config_path).await
}
}
/// 检查文件是否为脚本文件
fn is_script_file(&self, path: &str) -> Result<bool> {
// 1. 先通过扩展名快速判断
if path.ends_with(".yaml") || path.ends_with(".yml") {
return Ok(false); // YAML文件不是脚本文件
} else if path.ends_with(".js") {
return Ok(true); // JS文件是脚本文件
}
// 2. 读取文件内容
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) => {
log::warn!(target: "app", "无法读取文件以检测类型: {}, 错误: {}", path, err);
return Err(anyhow::anyhow!("Failed to read file to detect type: {}", err));
}
};
// 3. 检查是否存在明显的YAML特征
let has_yaml_features = content.contains(": ") ||
content.contains("#") ||
content.contains("---") ||
content.lines().any(|line| line.trim().starts_with("- "));
// 4. 检查是否存在明显的JS特征
let has_js_features = content.contains("function ") ||
content.contains("const ") ||
content.contains("let ") ||
content.contains("var ") ||
content.contains("//") ||
content.contains("/*") ||
content.contains("*/") ||
content.contains("export ") ||
content.contains("import ");
// 5. 决策逻辑
if has_yaml_features && !has_js_features {
// 只有YAML特征,没有JS特征
return Ok(false);
} else if has_js_features && !has_yaml_features {
// 只有JS特征,没有YAML特征
return Ok(true);
} else if has_yaml_features && has_js_features {
// 两种特征都有,需要更精细判断
// 优先检查是否有明确的JS结构特征
if content.contains("function main") ||
content.contains("module.exports") ||
content.contains("export default") {
return Ok(true);
}
// 检查冒号后是否有空格(YAML的典型特征)
let yaml_pattern_count = content.lines()
.filter(|line| line.contains(": "))
.count();
if yaml_pattern_count > 2 {
return Ok(false); // 多个键值对格式,更可能是YAML
}
}
// 默认情况:无法确定时,假设为非脚本文件(更安全)
log::debug!(target: "app", "无法确定文件类型,默认当作YAML处理: {}", path);
Ok(false)
}
/// 验证脚本文件语法
async fn validate_script_file(&self, path: &str) -> Result<(bool, String)> {
// 读取脚本内容
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read script file: {}", err);
log::warn!(target: "app", "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
return Ok((false, error_msg));
}
};
log::debug!(target: "app", "验证脚本文件: {}", path);
// 使用boa引擎进行基本语法检查
use boa_engine::{Context, Source};
let mut context = Context::default();
let result = context.eval(Source::from_bytes(&content));
match result {
Ok(_) => {
log::debug!(target: "app", "脚本语法验证通过: {}", path);
// 检查脚本是否包含main函数
if !content.contains("function main") && !content.contains("const main") && !content.contains("let main") {
let error_msg = "Script must contain a main function";
log::warn!(target: "app", "脚本缺少main函数: {}", path);
//handle::Handle::notice_message("config_validate::script_missing_main", error_msg);
return Ok((false, error_msg.to_string()));
}
Ok((true, String::new()))
},
Err(err) => {
let error_msg = format!("Script syntax error: {}", err);
log::warn!(target: "app", "脚本语法错误: {}", err);
//handle::Handle::notice_message("config_validate::script_syntax_error", &error_msg);
Ok((false, error_msg))
}
}
}
/// 更新proxies等配置
pub async fn update_config(&self) -> Result<(bool, String)> {
println!("[core配置更新] 开始更新配置");
// 1. 先生成新的配置内容
println!("[core配置更新] 生成新的配置内容");
Config::generate().await?;
// 2. 生成临时文件并进行验证
println!("[core配置更新] 生成临时配置文件用于验证");
let temp_config = Config::generate_file(ConfigType::Check)?;
let temp_config = dirs::path_to_str(&temp_config)?;
println!("[core配置更新] 临时配置文件路径: {}", temp_config);
// 3. 验证配置
match self.validate_config().await {
Ok((true, _)) => {
println!("[core配置更新] 配置验证通过");
// 4. 验证通过后,生成正式的运行时配置
println!("[core配置更新] 生成运行时配置");
let run_path = Config::generate_file(ConfigType::Run)?;
let run_path = dirs::path_to_str(&run_path)?;
// 5. 应用新配置
println!("[core配置更新] 应用新配置");
for i in 0..3 {
match clash_api::put_configs(run_path).await {
Ok(_) => {
println!("[core配置更新] 配置应用成功");
Config::runtime().apply();
return Ok((true, String::new()));
}
Err(err) => {
if i < 2 {
println!("[core配置更新] 第{}次重试应用配置", i + 1);
log::info!(target: "app", "{err}");
sleep(Duration::from_millis(100)).await;
} else {
println!("[core配置更新] 配置应用失败: {}", err);
Config::runtime().discard();
return Ok((false, err.to_string()));
}
}
}
}
Ok((true, String::new()))
}
Ok((false, error_msg)) => {
println!("[core配置更新] 配置验证失败: {}", error_msg);
Config::runtime().discard();
Ok((false, error_msg))
}
Err(e) => {
println!("[core配置更新] 验证过程发生错误: {}", e);
Config::runtime().discard();
Err(e)
}
}
}
/// 只进行文件语法检查,不进行完整验证
async fn validate_file_syntax(&self, config_path: &str) -> Result<(bool, String)> {
println!("[core配置语法检查] 开始检查文件: {}", config_path);
// 读取文件内容
let content = match std::fs::read_to_string(config_path) {
Ok(content) => content,
Err(err) => {
let error_msg = format!("Failed to read file: {}", err);
println!("[core配置语法检查] 无法读取文件: {}", error_msg);
return Ok((false, error_msg));
}
};
// 对YAML文件尝试解析,只检查语法正确性
println!("[core配置语法检查] 进行YAML语法检查");
match serde_yaml::from_str::<serde_yaml::Value>(&content) {
Ok(_) => {
println!("[core配置语法检查] YAML语法检查通过");
Ok((true, String::new()))
},
Err(err) => {
// 使用标准化的前缀,以便错误处理函数能正确识别
let error_msg = format!("YAML syntax error: {}", err);
println!("[core配置语法检查] YAML语法错误: {}", error_msg);
Ok((false, error_msg))
}
}
}
}
-8
View File
@@ -1,6 +1,4 @@
use super::tray::Tray;
use crate::log_err;
use anyhow::Result;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
use std::sync::Arc;
@@ -65,12 +63,6 @@ impl Handle {
}
}
/// update the system tray state
pub fn update_systray_part() -> Result<()> {
Tray::update_part()?;
Ok(())
}
pub fn set_is_exiting(&self) {
let mut is_exiting = self.is_exiting.write();
*is_exiting = true;
+121 -10
View File
@@ -1,11 +1,14 @@
use crate::core::handle;
use crate::{config::Config, feat, log_err};
use crate::utils::resolve;
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc};
use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
use tauri::async_runtime;
pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
}
@@ -21,8 +24,22 @@ impl Hotkey {
pub fn init(&self) -> Result<()> {
let verge = Config::verge();
let enable_global_hotkey = verge.latest().enable_global_hotkey.unwrap_or(true);
println!("Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
log::info!(target: "app", "Initializing hotkeys, global hotkey enabled: {}", enable_global_hotkey);
// 如果全局热键被禁用,则不注册热键
if !enable_global_hotkey {
println!("Global hotkey is disabled, skipping registration");
log::info!(target: "app", "Global hotkey is disabled, skipping registration");
return Ok(());
}
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
println!("Found {} hotkeys to register", hotkeys.len());
log::info!(target: "app", "Found {} hotkeys to register", hotkeys.len());
for hotkey in hotkeys.iter() {
let mut iter = hotkey.split(',');
let func = iter.next();
@@ -30,56 +47,151 @@ impl Hotkey {
match (key, func) {
(Some(key), Some(func)) => {
log_err!(self.register(key, func));
println!("Registering hotkey: {} -> {}", key, func);
log::info!(target: "app", "Registering hotkey: {} -> {}", key, func);
if let Err(e) = self.register(key, func) {
println!("Failed to register hotkey {} -> {}: {:?}", key, func, e);
log::error!(target: "app", "Failed to register hotkey {} -> {}: {:?}", key, func, e);
} else {
println!("Successfully registered hotkey {} -> {}", key, func);
log::info!(target: "app", "Successfully registered hotkey {} -> {}", key, func);
}
}
_ => {
let key = key.unwrap_or("None");
let func = func.unwrap_or("None");
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
println!("Invalid hotkey configuration: `{key}`:`{func}`");
log::error!(target: "app", "Invalid hotkey configuration: `{key}`:`{func}`");
}
}
}
self.current.lock().clone_from(hotkeys);
} else {
println!("No hotkeys configured");
log::info!(target: "app", "No hotkeys configured");
}
Ok(())
}
pub fn reset(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
manager.unregister_all()?;
Ok(())
}
pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
println!("Attempting to register hotkey: {} for function: {}", hotkey, func);
log::info!(target: "app", "Attempting to register hotkey: {} for function: {}", hotkey, func);
if manager.is_registered(hotkey) {
println!("Hotkey {} was already registered, unregistering first", hotkey);
log::info!(target: "app", "Hotkey {} was already registered, unregistering first", hotkey);
manager.unregister(hotkey)?;
}
let f = match func.trim() {
"open_or_close_dashboard" => feat::open_or_close_dashboard,
"open_or_close_dashboard" => {
println!("Registering open_or_close_dashboard function");
log::info!(target: "app", "Registering open_or_close_dashboard function");
|| {
println!("=== Hotkey Dashboard Window Operation Start ===");
log::info!(target: "app", "=== Hotkey Dashboard Window Operation Start ===");
// 使用 spawn_blocking 来确保在正确的线程上执行
async_runtime::spawn_blocking(|| {
println!("Toggle dashboard window visibility");
log::info!(target: "app", "Toggle dashboard window visibility");
// 检查窗口是否存在
if let Some(window) = handle::Handle::global().get_window() {
// 如果窗口可见,则隐藏它
if window.is_visible().unwrap_or(false) {
println!("Window is visible, hiding it");
log::info!(target: "app", "Window is visible, hiding it");
let _ = window.hide();
} else {
// 如果窗口不可见,则显示它
println!("Window is hidden, showing it");
log::info!(target: "app", "Window is hidden, showing it");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
} else {
// 如果窗口不存在,创建一个新窗口
println!("Window does not exist, creating a new one");
log::info!(target: "app", "Window does not exist, creating a new one");
resolve::create_window();
}
});
println!("=== Hotkey Dashboard Window Operation End ===");
log::info!(target: "app", "=== Hotkey Dashboard Window Operation End ===");
}
},
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => feat::toggle_system_proxy,
"toggle_tun_mode" => feat::toggle_tun_mode,
"toggle_system_proxy" => || feat::toggle_system_proxy(),
"toggle_tun_mode" => || feat::toggle_tun_mode(None),
"quit" => || feat::quit(Some(0)),
_ => bail!("invalid function \"{func}\""),
_ => {
println!("Invalid function: {}", func);
log::error!(target: "app", "Invalid function: {}", func);
bail!("invalid function \"{func}\"");
}
};
let is_quit = func.trim() == "quit";
let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed {
if hotkey.key == Code::KeyQ {
println!("Hotkey pressed: {:?}", hotkey);
log::info!(target: "app", "Hotkey pressed: {:?}", hotkey);
if hotkey.key == Code::KeyQ && is_quit {
if let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) {
println!("Executing quit function");
log::info!(target: "app", "Executing quit function");
f();
}
}
} else {
f();
// 直接执行函数,不做任何状态检查
println!("Executing function directly");
log::info!(target: "app", "Executing function directly");
// 获取轻量模式状态和全局热键状态
let is_lite_mode = Config::verge().latest().enable_lite_mode.unwrap_or(false);
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
// 在轻量模式下或配置了全局热键时,始终执行热键功能
if is_lite_mode || is_enable_global_hotkey {
f();
} else if let Some(window) = app_handle.get_webview_window("main") {
// 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键
let is_visible = window.is_visible().unwrap_or(false);
let is_focused = window.is_focused().unwrap_or(false);
if is_focused && is_visible {
f();
}
}
}
}
});
log::debug!(target: "app", "register hotkey {hotkey} {func}");
println!("Successfully registered hotkey {} for {}", hotkey, func);
log::info!(target: "app", "Successfully registered hotkey {} for {}", hotkey, func);
Ok(())
}
@@ -87,7 +199,6 @@ impl Hotkey {
let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?;
log::debug!(target: "app", "unregister hotkey {hotkey}");
Ok(())
}
+49
View File
@@ -10,6 +10,7 @@ use tokio::time::Duration;
// Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211";
const REQUIRED_SERVICE_VERSION: &str = "1.0.1"; // 定义所需的服务版本号
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody {
@@ -19,6 +20,12 @@ pub struct ResponseBody {
pub log_file: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionResponse {
pub service: String,
pub version: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse {
pub code: u64,
@@ -26,6 +33,13 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionJsonResponse {
pub code: u64,
pub msg: String,
pub data: Option<VersionResponse>,
}
#[cfg(target_os = "windows")]
pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service");
@@ -177,8 +191,43 @@ pub async fn check_service() -> Result<JsonResponse> {
Ok(response)
}
/// check the service version
pub async fn check_service_version() -> Result<String> {
let url = format!("{SERVICE_URL}/version");
let response = reqwest::ClientBuilder::new()
.no_proxy()
.timeout(Duration::from_secs(3))
.build()?
.get(url)
.send()
.await
.context("failed to connect to the Clash Verge Service")?
.json::<VersionJsonResponse>()
.await
.context("failed to parse the Clash Verge Service version response")?;
match response.data {
Some(data) => Ok(data.version),
None => bail!("service version not found in response"),
}
}
/// check if service needs to be reinstalled
pub async fn check_service_needs_reinstall() -> bool {
match check_service_version().await {
Ok(version) => version != REQUIRED_SERVICE_VERSION,
Err(_) => true, // 如果无法获取版本或服务未运行,也需要重新安装
}
}
/// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
// 检查服务版本,如果不匹配则重新安装
if check_service_needs_reinstall().await {
log::info!(target: "app", "service version mismatch, reinstalling");
reinstall_service().await?;
}
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
+3 -2
View File
@@ -24,10 +24,11 @@ pub struct Sysopt {
#[cfg(target_os = "windows")]
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;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.*;<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";
static DEFAULT_BYPASS: &str =
"localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::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>";
"127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
fn get_bypass() -> String {
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true);
+12 -1
View File
@@ -1,5 +1,6 @@
use crate::config::Config;
use crate::feat;
use crate::core::CoreManager;
use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell;
@@ -172,7 +173,17 @@ impl Timer {
/// 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);
// 使用更轻量级的更新方式
if let Err(e) = feat::update_profile(uid.clone(), None).await {
log::error!(target: "app", "timer task update error: {}", e);
return;
}
// 只有更新成功后才刷新配置
if let Err(e) = CoreManager::global().update_config().await {
log::error!(target: "app", "timer task refresh error: {}", e);
}
}
}
-432
View File
@@ -1,432 +0,0 @@
use crate::{
cmds,
config::Config,
feat, t,
utils::{
dirs,
resolve::{self, VERSION},
},
};
use anyhow::Result;
use tauri::AppHandle;
use tauri::{
menu::CheckMenuItem,
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
};
use tauri::{
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
Wry,
};
use super::handle;
pub struct Tray {}
impl Tray {
pub fn create_systray() -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let tray_incon_id = TrayIconId::new("main");
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
tray.on_tray_icon_event(|_, event| {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
#[cfg(target_os = "macos")]
if let TrayIconEvent::Click {
button: MouseButton::Right,
button_state: MouseButtonState::Down,
..
} = event
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"main_window" => resolve::create_window(),
_ => {}
}
}
#[cfg(not(target_os = "macos"))]
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"main_window" => resolve::create_window(),
_ => {}
}
}
});
tray.on_menu_event(on_menu_event);
Ok(())
}
pub fn update_part() -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let use_zh = { Config::verge().latest().language == Some("zh".into()) };
let version = VERSION.get().unwrap();
let mode = {
Config::clash()
.latest()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let tray = app_handle.tray_by_id("main").unwrap();
#[cfg(target_os = "macos")]
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let _ = tray.set_menu(Some(create_tray_menu(
&app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
)?));
#[cfg(target_os = "macos")]
let mut use_custom_icon = false;
#[allow(unused)]
let mut indication_icon = if *system_proxy && !*tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon-sys.ico").to_vec()
}
_ => include_bytes!("../../icons/tray-icon-sys-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-sys.ico").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
} else if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon-tun.ico").to_vec()
}
_ => include_bytes!("../../icons/tray-icon-tun-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon-tun.ico").to_vec();
if *tun_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => {
use_custom_icon = true;
include_bytes!("../../icons/tray-icon.ico").to_vec()
}
_ => include_bytes!("../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
#[cfg(target_os = "macos")]
{
use_custom_icon = true;
}
}
icon
};
#[cfg(target_os = "macos")]
{
if use_custom_icon {
let _ = tray.set_icon_as_template(false);
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
} else {
let _ = tray.set_icon_as_template(true);
}
}
#[cfg(not(target_os = "macos"))]
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
let switch_map = {
let mut map = std::collections::HashMap::new();
map.insert(true, "on");
map.insert(false, "off");
map
};
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
if let Some(current_profile_uid) = profiles.get_current() {
let current_profile = profiles.get_item(&current_profile_uid);
current_profile_name = match &current_profile.unwrap().name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
};
let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t!("SysProxy", "系统代理", use_zh),
switch_map[system_proxy],
t!("TUN", "Tun模式", use_zh),
switch_map[tun_mode],
t!("Profile", "当前订阅", use_zh),
current_profile_name
)));
Ok(())
}
}
fn create_tray_menu(
app_handle: &AppHandle,
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
) -> Result<tauri::menu::Menu<Wry>> {
let mode = mode.unwrap_or("");
let use_zh = { Config::verge().latest().language == Some("zh".into()) };
let version = VERSION.get().unwrap();
let open_window = &MenuItem::with_id(
app_handle,
"open_window",
t!("Dashboard", "打开面板", use_zh),
true,
None::<&str>,
)
.unwrap();
let rule_mode = &CheckMenuItem::with_id(
app_handle,
"rule_mode",
t!("Rule Mode", "规则模式", use_zh),
true,
mode == "rule",
None::<&str>,
)
.unwrap();
let global_mode = &CheckMenuItem::with_id(
app_handle,
"global_mode",
t!("Global Mode", "全局模式", use_zh),
true,
mode == "global",
None::<&str>,
)
.unwrap();
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t!("Direct Mode", "直连模式", use_zh),
true,
mode == "direct",
None::<&str>,
)
.unwrap();
let system_proxy = &CheckMenuItem::with_id(
app_handle,
"system_proxy",
t!("System Proxy", "系统代理", use_zh),
true,
system_proxy_enabled,
None::<&str>,
)
.unwrap();
let tun_mode = &CheckMenuItem::with_id(
app_handle,
"tun_mode",
t!("TUN Mode", "Tun模式", use_zh),
true,
tun_mode_enabled,
None::<&str>,
)
.unwrap();
let copy_env = &MenuItem::with_id(
app_handle,
"copy_env",
t!("Copy Env", "复制环境变量", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t!("Conf Dir", "配置目录", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t!("Core Dir", "内核目录", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t!("Logs Dir", "日志目录", use_zh),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t!("Open Dir", "打开目录", use_zh),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id(
app_handle,
"restart_clash",
t!("Restart Clash Core", "重启Clash内核", use_zh),
true,
None::<&str>,
)
.unwrap();
let restart_app = &MenuItem::with_id(
app_handle,
"restart_app",
t!("Restart App", "重启App", use_zh),
true,
None::<&str>,
)
.unwrap();
let app_version = &MenuItem::with_id(
app_handle,
"app_version",
format!("Version {version}"),
true,
None::<&str>,
)
.unwrap();
let more = &Submenu::with_id_and_items(
app_handle,
"more",
t!("More", "更多", use_zh),
true,
&[restart_clash, restart_app, app_version],
)
.unwrap();
let quit = &MenuItem::with_id(
app_handle,
"quit",
t!("Quit", "退出", use_zh),
true,
Some("CmdOrControl+Q"),
)
.unwrap();
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[
open_window,
separator,
rule_mode,
global_mode,
direct_mode,
separator,
system_proxy,
tun_mode,
copy_env,
open_dir,
more,
separator,
quit,
])
.build()
.unwrap();
Ok(menu)
}
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
match event.id.as_ref() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
println!("change mode to: {}", mode);
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(),
"copy_env" => feat::copy_clash_env(),
"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" => feat::restart_app(),
"quit" => {
println!("quit");
feat::quit(Some(0));
}
_ => {}
}
}
+614
View File
@@ -0,0 +1,614 @@
use once_cell::sync::OnceCell;
#[cfg(target_os = "macos")]
pub mod speed_rate;
use crate::core::clash_api::Rate;
use crate::{
cmds,
config::Config,
feat, resolve,
utils::resolve::VERSION,
utils::{dirs, i18n::t},
};
use anyhow::Result;
#[cfg(target_os = "macos")]
use futures::StreamExt;
#[cfg(target_os = "macos")]
use parking_lot::Mutex;
#[cfg(target_os = "macos")]
use parking_lot::RwLock;
#[cfg(target_os = "macos")]
pub use speed_rate::{SpeedRate, Traffic};
#[cfg(target_os = "macos")]
use std::sync::Arc;
use tauri::menu::{CheckMenuItem, IsMenuItem};
use tauri::AppHandle;
use tauri::{
menu::{MenuEvent, MenuItem, PredefinedMenuItem, Submenu},
tray::{MouseButton, MouseButtonState, TrayIconEvent, TrayIconId},
Wry,
};
#[cfg(target_os = "macos")]
use tokio::sync::broadcast;
use super::handle;
#[cfg(target_os = "macos")]
pub struct Tray {
pub speed_rate: Arc<Mutex<Option<SpeedRate>>>,
shutdown_tx: Arc<RwLock<Option<broadcast::Sender<()>>>>,
is_subscribed: Arc<RwLock<bool>>,
}
#[cfg(not(target_os = "macos"))]
pub struct Tray {}
impl Tray {
pub fn global() -> &'static Tray {
static TRAY: OnceCell<Tray> = OnceCell::new();
#[cfg(target_os = "macos")]
return TRAY.get_or_init(|| Tray {
speed_rate: Arc::new(Mutex::new(None)),
shutdown_tx: Arc::new(RwLock::new(None)),
is_subscribed: Arc::new(RwLock::new(false)),
});
#[cfg(not(target_os = "macos"))]
return TRAY.get_or_init(|| Tray {});
}
pub fn init(&self) -> Result<()> {
#[cfg(target_os = "macos")]
{
let mut speed_rate = self.speed_rate.lock();
*speed_rate = Some(SpeedRate::new());
}
Ok(())
}
pub fn create_systray(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let tray_incon_id = TrayIconId::new("main");
let tray = app_handle.tray_by_id(&tray_incon_id).unwrap();
#[cfg(target_os = "macos")]
tray.set_show_menu_on_left_click(false)?;
tray.on_tray_icon_event(|_, event| {
let tray_event = { Config::verge().latest().tray_event.clone() };
let tray_event: String = tray_event.unwrap_or("main_window".into());
#[cfg(target_os = "macos")]
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(),
_ => {}
}
}
#[cfg(not(target_os = "macos"))]
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Down,
..
} = event
{
match tray_event.as_str() {
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"main_window" => resolve::create_window(),
_ => {}
}
}
});
tray.on_menu_event(on_menu_event);
Ok(())
}
/// 更新托盘菜单
pub fn update_menu(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let verge = Config::verge().latest().clone();
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 mode = {
Config::clash()
.latest()
.0
.get("mode")
.map(|val| val.as_str().unwrap_or("rule"))
.unwrap_or("rule")
.to_owned()
};
let profile_uid_and_name = Config::profiles()
.data()
.all_profile_uid_and_name()
.unwrap_or(Vec::new());
let tray = app_handle.tray_by_id("main").unwrap();
let _ = tray.set_menu(Some(create_tray_menu(
&app_handle,
Some(mode.as_str()),
*system_proxy,
*tun_mode,
profile_uid_and_name,
)?));
Ok(())
}
/// 更新托盘图标
#[allow(unused_variables)]
pub fn update_icon(&self, rate: Option<Rate>) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let verge = Config::verge().latest().clone();
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
let common_tray_icon = verge.common_tray_icon.as_ref().unwrap_or(&false);
let sysproxy_tray_icon = verge.sysproxy_tray_icon.as_ref().unwrap_or(&false);
let tun_tray_icon = verge.tun_tray_icon.as_ref().unwrap_or(&false);
let tray = app_handle.tray_by_id("main").unwrap();
#[cfg(target_os = "macos")]
let tray_icon = verge.tray_icon.clone().unwrap_or("monochrome".to_string());
let icon_bytes = if *system_proxy && !*tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon-sys.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-sys-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon-sys.ico").to_vec();
if *sysproxy_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("sysproxy.png");
let ico_path = icon_dir_path.join("sysproxy.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else if *tun_mode {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon-tun.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-tun-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon-tun.ico").to_vec();
if *tun_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("tun.png");
let ico_path = icon_dir_path.join("tun.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
} else {
#[cfg(target_os = "macos")]
let mut icon = match tray_icon.as_str() {
"colorful" => include_bytes!("../../../icons/tray-icon.ico").to_vec(),
_ => include_bytes!("../../../icons/tray-icon-mono.ico").to_vec(),
};
#[cfg(not(target_os = "macos"))]
let mut icon = include_bytes!("../../../icons/tray-icon.ico").to_vec();
if *common_tray_icon {
let icon_dir_path = dirs::app_home_dir()?.join("icons");
let png_path = icon_dir_path.join("common.png");
let ico_path = icon_dir_path.join("common.ico");
if ico_path.exists() {
icon = std::fs::read(ico_path).unwrap();
} else if png_path.exists() {
icon = std::fs::read(png_path).unwrap();
}
}
icon
};
#[cfg(target_os = "macos")]
{
let enable_tray_speed = Config::verge().latest().enable_tray_speed.unwrap_or(true);
let is_colorful = tray_icon == "colorful";
// 处理图标和速率
let final_icon_bytes = if enable_tray_speed {
let rate = rate.or_else(|| {
self.speed_rate
.lock()
.as_ref()
.and_then(|speed_rate| speed_rate.get_curent_rate())
});
// 使用新的方法渲染图标和速率
SpeedRate::add_speed_text(icon_bytes, rate)?
} else {
icon_bytes
};
// 设置系统托盘图标
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&final_icon_bytes)?));
// 只对单色图标使用 template 模式
let _ = tray.set_icon_as_template(!is_colorful);
}
#[cfg(not(target_os = "macos"))]
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?));
Ok(())
}
/// 更新托盘提示
pub fn update_tooltip(&self) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap();
let version = VERSION.get().unwrap();
let verge = Config::verge().latest().clone();
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 switch_map = {
let mut map = std::collections::HashMap::new();
map.insert(true, "on");
map.insert(false, "off");
map
};
let mut current_profile_name = "None".to_string();
let profiles = Config::profiles();
let profiles = profiles.latest();
if let Some(current_profile_uid) = profiles.get_current() {
let current_profile = profiles.get_item(&current_profile_uid);
current_profile_name = match &current_profile.unwrap().name {
Some(profile_name) => profile_name.to_string(),
None => current_profile_name,
};
};
let tray = app_handle.tray_by_id("main").unwrap();
let _ = tray.set_tooltip(Some(&format!(
"Clash Verge {version}\n{}: {}\n{}: {}\n{}: {}",
t("SysProxy"),
switch_map[system_proxy],
t("TUN"),
switch_map[tun_mode],
t("Profile"),
current_profile_name
)));
Ok(())
}
pub fn update_part(&self) -> Result<()> {
self.update_menu()?;
self.update_icon(None)?;
self.update_tooltip()?;
Ok(())
}
/// 订阅流量数据
#[cfg(target_os = "macos")]
pub async fn subscribe_traffic(&self) -> Result<()> {
log::info!(target: "app", "subscribe traffic");
// 如果已经订阅,先取消订阅
if *self.is_subscribed.read() {
self.unsubscribe_traffic();
}
let (shutdown_tx, shutdown_rx) = broadcast::channel(1);
*self.shutdown_tx.write() = Some(shutdown_tx);
*self.is_subscribed.write() = true;
let speed_rate = Arc::clone(&self.speed_rate);
let is_subscribed = Arc::clone(&self.is_subscribed);
tauri::async_runtime::spawn(async move {
let mut shutdown = shutdown_rx;
'outer: loop {
match Traffic::get_traffic_stream().await {
Ok(mut stream) => loop {
tokio::select! {
Some(traffic) = stream.next() => {
if let Ok(traffic) = traffic {
let guard = speed_rate.lock();
let enable_tray_speed: bool = Config::verge().latest().enable_tray_speed.unwrap_or(true);
if !enable_tray_speed {
continue;
}
if let Some(sr) = guard.as_ref() {
if let Some(rate) = sr.update_and_check_changed(traffic.up, traffic.down) {
let _ = Tray::global().update_icon(Some(rate));
}
}
}
}
_ = shutdown.recv() => break 'outer,
}
},
Err(e) => {
log::error!(target: "app", "Failed to get traffic stream: {}", e);
// 如果获取流失败,等待一段时间后重试
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// 检查是否应该继续重试
if !*is_subscribed.read() {
break;
}
}
}
}
});
Ok(())
}
/// 取消订阅 traffic 数据
#[cfg(target_os = "macos")]
pub fn unsubscribe_traffic(&self) {
log::info!(target: "app", "unsubscribe traffic");
*self.is_subscribed.write() = false;
if let Some(tx) = self.shutdown_tx.write().take() {
drop(tx);
}
}
}
fn create_tray_menu(
app_handle: &AppHandle,
mode: Option<&str>,
system_proxy_enabled: bool,
tun_mode_enabled: bool,
profile_uid_and_name: Vec<(String, String)>,
) -> Result<tauri::menu::Menu<Wry>> {
let mode = mode.unwrap_or("");
let version = VERSION.get().unwrap();
let hotkeys = Config::verge()
.latest()
.hotkeys
.as_ref()
.map(|h| {
h.iter()
.filter_map(|item| {
let mut parts = item.split(',');
match (parts.next(), parts.next()) {
(Some(func), Some(key)) => Some((func.to_string(), key.to_string())),
_ => None,
}
})
.collect::<std::collections::HashMap<String, String>>()
})
.unwrap_or_default();
let profile_menu_items: Vec<CheckMenuItem<Wry>> = profile_uid_and_name
.iter()
.map(|(profile_uid, profile_name)| {
let is_current_profile = Config::profiles().data().is_current_profile_index(profile_uid.to_string());
CheckMenuItem::with_id(
app_handle,
&format!("profiles_{}", profile_uid),
t(&profile_name),
true,
is_current_profile,
None::<&str>,
)
.unwrap()
}).collect();
let profile_menu_items: Vec<&dyn IsMenuItem<Wry>> = profile_menu_items
.iter()
.map(|item| item as &dyn IsMenuItem<Wry>)
.collect();
let open_window = &MenuItem::with_id(
app_handle,
"open_window",
t("Dashboard"),
true,
hotkeys.get("open_or_close_dashboard").map(|s| s.as_str()),
)
.unwrap();
let rule_mode = &CheckMenuItem::with_id(
app_handle,
"rule_mode",
t("Rule Mode"),
true,
mode == "rule",
hotkeys.get("clash_mode_rule").map(|s| s.as_str()),
)
.unwrap();
let global_mode = &CheckMenuItem::with_id(
app_handle,
"global_mode",
t("Global Mode"),
true,
mode == "global",
hotkeys.get("clash_mode_global").map(|s| s.as_str()),
)
.unwrap();
let direct_mode = &CheckMenuItem::with_id(
app_handle,
"direct_mode",
t("Direct Mode"),
true,
mode == "direct",
hotkeys.get("clash_mode_direct").map(|s| s.as_str()),
)
.unwrap();
let profiles = &Submenu::with_id_and_items(
app_handle,
"profiles",
t("Profiles"),
true,
&profile_menu_items,
).unwrap();
let system_proxy = &CheckMenuItem::with_id(
app_handle,
"system_proxy",
t("System Proxy"),
true,
system_proxy_enabled,
hotkeys.get("toggle_system_proxy").map(|s| s.as_str()),
)
.unwrap();
let tun_mode = &CheckMenuItem::with_id(
app_handle,
"tun_mode",
t("TUN Mode"),
true,
tun_mode_enabled,
hotkeys.get("toggle_tun_mode").map(|s| s.as_str()),
)
.unwrap();
let copy_env =
&MenuItem::with_id(app_handle, "copy_env", t("Copy Env"), true, None::<&str>).unwrap();
let open_app_dir = &MenuItem::with_id(
app_handle,
"open_app_dir",
t("Conf Dir"),
true,
None::<&str>,
)
.unwrap();
let open_core_dir = &MenuItem::with_id(
app_handle,
"open_core_dir",
t("Core Dir"),
true,
None::<&str>,
)
.unwrap();
let open_logs_dir = &MenuItem::with_id(
app_handle,
"open_logs_dir",
t("Logs Dir"),
true,
None::<&str>,
)
.unwrap();
let open_dir = &Submenu::with_id_and_items(
app_handle,
"open_dir",
t("Open Dir"),
true,
&[open_app_dir, open_core_dir, open_logs_dir],
)
.unwrap();
let restart_clash = &MenuItem::with_id(
app_handle,
"restart_clash",
t("Restart Clash Core"),
true,
None::<&str>,
)
.unwrap();
let restart_app = &MenuItem::with_id(
app_handle,
"restart_app",
t("Restart App"),
true,
None::<&str>,
)
.unwrap();
let app_version = &MenuItem::with_id(
app_handle,
"app_version",
format!("{} {version}", t("Verge Version")),
true,
None::<&str>,
)
.unwrap();
let more = &Submenu::with_id_and_items(
app_handle,
"more",
t("More"),
true,
&[restart_clash, restart_app, app_version],
)
.unwrap();
let quit =
&MenuItem::with_id(app_handle, "quit", t("Exit"), true, Some("CmdOrControl+Q")).unwrap();
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[
open_window,
separator,
rule_mode,
global_mode,
direct_mode,
separator,
profiles,
separator,
system_proxy,
tun_mode,
copy_env,
open_dir,
more,
separator,
quit,
])
.build()
.unwrap();
Ok(menu)
}
fn on_menu_event(_: &AppHandle, event: MenuEvent) {
match event.id.as_ref() {
mode @ ("rule_mode" | "global_mode" | "direct_mode") => {
let mode = &mode[0..mode.len() - 5];
println!("change mode to: {}", mode);
feat::change_clash_mode(mode.into());
}
"open_window" => resolve::create_window(),
"system_proxy" => feat::toggle_system_proxy(),
"tun_mode" => feat::toggle_tun_mode(None),
"copy_env" => feat::copy_clash_env(),
"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" => feat::restart_app(),
"quit" => {
println!("quit");
feat::quit(Some(0));
},
id if id.starts_with("profiles_") => {
let profile_index = &id["profiles_".len()..];
feat::toggle_proxy_profile(profile_index.into());
}
_ => {}
}
}
+226
View File
@@ -0,0 +1,226 @@
use crate::core::clash_api::{get_traffic_ws_url, Rate};
use crate::utils::help::format_bytes_speed;
use anyhow::Result;
use futures::Stream;
use image::{Rgba, GenericImageView, RgbaImage};
use imageproc::drawing::draw_text_mut;
use parking_lot::Mutex;
use rusttype::{Font, Scale};
use std::io::Cursor;
use std::sync::Arc;
use tokio_tungstenite::tungstenite::Message;
#[derive(Debug, Clone)]
pub struct SpeedRate {
rate: Arc<Mutex<(Rate, Rate)>>,
last_update: Arc<Mutex<std::time::Instant>>,
// 移除 base_image,不再缓存原始图像
}
impl SpeedRate {
pub fn new() -> Self {
Self {
rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))),
last_update: Arc::new(Mutex::new(std::time::Instant::now())),
}
}
pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option<Rate> {
let mut rates = self.rate.lock();
let mut last_update = self.last_update.lock();
let now = std::time::Instant::now();
// 限制更新频率为每秒最多2次(500ms)
if now.duration_since(*last_update).as_millis() < 500 {
return None;
}
let (current, previous) = &mut *rates;
// 如果速率变化不大(小于10%),则不更新
let should_update = {
let up_change = (current.up as f64 - up as f64).abs() / (current.up as f64 + 1.0);
let down_change = (current.down as f64 - down as f64).abs() / (current.down as f64 + 1.0);
up_change > 0.1 || down_change > 0.1
};
if !should_update {
return None;
}
*previous = current.clone();
current.up = up;
current.down = down;
*last_update = now;
if previous != current {
Some(current.clone())
} else {
None
}
}
pub fn get_curent_rate(&self) -> Option<Rate> {
let rates = self.rate.lock();
let (current, _) = &*rates;
Some(current.clone())
}
// 分离图标加载和速率渲染
pub fn add_speed_text(icon_bytes: Vec<u8>, rate: Option<Rate>) -> Result<Vec<u8>> {
let rate = rate.unwrap_or(Rate { up: 0, down: 0 });
// 加载原始图标
let icon_image = image::load_from_memory(&icon_bytes)?;
let (icon_width, icon_height) = (icon_image.width(), icon_image.height());
// 判断是否为彩色图标
let is_colorful = !crate::utils::help::is_monochrome_image_from_bytes(&icon_bytes).unwrap_or(false);
// 增加文本宽度和间距
let text_width = 580; // 文本区域宽度
let total_width = icon_width + text_width;
// 创建新的透明画布
let mut combined_image = RgbaImage::new(total_width, icon_height);
// 将原始图标绘制到新画布的左侧
for y in 0..icon_height {
for x in 0..icon_width {
let pixel = icon_image.get_pixel(x, y);
combined_image.put_pixel(x, y, pixel);
}
}
// 选择文本颜色
let (text_color, shadow_color) = if is_colorful {
// 彩色图标使用黑色文本和轻微白色阴影
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 160u8]))
} else {
// 单色图标使用白色文本和轻微黑色阴影
(Rgba([255u8, 255u8, 255u8, 255u8]), Rgba([0u8, 0u8, 0u8, 120u8]))
};
// 减小字体大小以适应文本区域
let font = Font::try_from_bytes(include_bytes!("../../../assets/fonts/SF-Pro.ttf")).unwrap();
let font_size = icon_height as f32 * 0.6; // 稍微减小字体
let scale = Scale::uniform(font_size);
// 使用更简洁的速率格式
let up_text = format_bytes_speed(rate.up);
let down_text = format_bytes_speed(rate.down);
// 计算文本位置,确保垂直间距合适
// 修改文本位置为居右显示
let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32;
let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32;
// 计算右对齐的文本位置
let up_text_x = total_width - up_text_width;
let down_text_x = total_width - down_text_width;
// 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小
let text_height = font_size as i32;
let total_text_height = text_height * 2;
let up_y = (icon_height as i32 - total_text_height) / 2;
let down_y = up_y + text_height;
// 绘制速率文本(先阴影后文字)
let shadow_offset = 1;
// 绘制上行速率
draw_text_mut(
&mut combined_image,
shadow_color,
up_text_x as i32 + shadow_offset,
up_y + shadow_offset,
scale,
&font,
&up_text,
);
draw_text_mut(
&mut combined_image,
text_color,
up_text_x as i32,
up_y,
scale,
&font,
&up_text,
);
// 绘制下行速率
draw_text_mut(
&mut combined_image,
shadow_color,
down_text_x as i32 + shadow_offset,
down_y + shadow_offset,
scale,
&font,
&down_text,
);
draw_text_mut(
&mut combined_image,
text_color,
down_text_x as i32,
down_y,
scale,
&font,
&down_text,
);
// 将结果转换为 PNG 数据
let mut bytes = Vec::new();
combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?;
Ok(bytes)
}
}
#[derive(Debug, Clone)]
pub struct Traffic {
pub up: u64,
pub down: u64,
}
impl Traffic {
pub async fn get_traffic_stream() -> Result<impl Stream<Item = Result<Traffic, anyhow::Error>>>
{
use futures::stream::{self, StreamExt};
use std::time::Duration;
let stream = Box::pin(
stream::unfold((), |_| async {
loop {
let ws_url = get_traffic_ws_url().unwrap();
match tokio_tungstenite::connect_async(&ws_url).await {
Ok((ws_stream, _)) => {
log::info!(target: "app", "traffic ws connection established");
return Some((
ws_stream.map(|msg| {
msg.map_err(anyhow::Error::from).and_then(|msg: Message| {
let data = msg.into_text()?;
let json: serde_json::Value = serde_json::from_str(&data)?;
Ok(Traffic {
up: json["up"].as_u64().unwrap_or(0),
down: json["down"].as_u64().unwrap_or(0),
})
})
}),
(),
));
}
Err(e) => {
log::error!(target: "app", "traffic ws connection failed: {e}");
tokio::time::sleep(Duration::from_secs(5)).await;
continue;
}
}
}
})
.flatten(),
);
Ok(stream)
}
}
+125 -42
View File
@@ -1,55 +1,138 @@
use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap {
prepend: Sequence,
append: Sequence,
delete: Sequence,
pub prepend: Sequence,
pub append: Sequence,
pub delete: Vec<String>,
}
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping {
let mut prepend = seq_map.prepend;
let append = seq_map.append;
let delete = seq_map.delete;
pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
let SeqMap {
prepend,
append,
delete,
} = seq;
let origin_seq = config.get(name).map_or(Sequence::default(), |val| {
val.as_sequence().unwrap_or(&Sequence::default()).clone()
});
let mut seq = origin_seq.clone();
let mut new_seq = Sequence::new();
new_seq.extend(prepend);
let mut delete_names = Vec::new();
for item in delete {
let item = item.clone();
if let Some(name) = if item.is_string() {
Some(item)
} else {
item.get("name").cloned()
} {
delete_names.push(name.clone());
if let Some(Value::Sequence(origin)) = config.get(field) {
// Filter out deleted items
let filtered: Sequence = origin
.iter()
.filter(|item| {
if let Value::String(s) = item {
!delete.contains(s)
} else if let Value::Mapping(m) = item {
if let Some(Value::String(name)) = m.get("name") {
!delete.contains(name)
} else {
true
}
} else {
true
}
})
.cloned()
.collect();
new_seq.extend(filtered);
}
new_seq.extend(append);
config.insert(Value::String(field.into()), Value::Sequence(new_seq));
// If this is proxies field, we also need to filter proxy-groups
if field == "proxies" {
if let Some(Value::Sequence(groups)) = config.get_mut("proxy-groups") {
let mut new_groups = Sequence::new();
for group in groups {
if let Value::Mapping(group_map) = group {
let mut new_group = group_map.clone();
if let Some(Value::Sequence(proxies)) = group_map.get("proxies") {
let filtered_proxies: Sequence = proxies
.iter()
.filter(|p| {
if let Value::String(name) = p {
!delete.contains(name)
} else {
true
}
})
.cloned()
.collect();
new_group.insert(
Value::String("proxies".into()),
Value::Sequence(filtered_proxies),
);
}
new_groups.push(Value::Mapping(new_group));
} else {
new_groups.push(group.clone());
}
}
config.insert(
Value::String("proxy-groups".into()),
Value::Sequence(new_groups),
);
}
}
seq.retain(|x| {
if let Some(x_name) = if x.is_string() {
Some(x)
} else {
x.get("name")
} {
!delete_names.contains(x_name)
} else {
true
}
});
prepend.reverse();
for item in prepend {
seq.insert(0, item);
}
for item in append {
seq.push(item);
}
let mut config = config.clone();
config.insert(Value::from(name), Value::from(seq));
config
}
#[cfg(test)]
mod tests {
use super::*;
use serde_yaml::Value;
#[test]
fn test_delete_proxy_and_references() {
let config_str = r#"
proxies:
- name: "proxy1"
type: "ss"
- name: "proxy2"
type: "vmess"
proxy-groups:
- name: "group1"
type: "select"
proxies:
- "proxy1"
- "proxy2"
- name: "group2"
type: "select"
proxies:
- "proxy1"
"#;
let mut config: Mapping = serde_yaml::from_str(config_str).unwrap();
let seq = SeqMap {
prepend: Sequence::new(),
append: Sequence::new(),
delete: vec!["proxy1".to_string()],
};
config = use_seq(seq, config, "proxies");
// Check if proxy1 is removed from proxies
let proxies = config.get("proxies").unwrap().as_sequence().unwrap();
assert_eq!(proxies.len(), 1);
assert_eq!(
proxies[0].as_mapping().unwrap().get("name").unwrap().as_str().unwrap(),
"proxy2"
);
// Check if proxy1 is removed from all groups
let groups = config.get("proxy-groups").unwrap().as_sequence().unwrap();
let group1_proxies = groups[0].as_mapping().unwrap()
.get("proxies").unwrap().as_sequence().unwrap();
let group2_proxies = groups[1].as_mapping().unwrap()
.get("proxies").unwrap().as_sequence().unwrap();
assert_eq!(group1_proxies.len(), 1);
assert_eq!(group1_proxies[0].as_str().unwrap(), "proxy2");
assert_eq!(group2_proxies.len(), 0);
}
}
+58 -9
View File
@@ -29,19 +29,68 @@ pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
let ipv6_key = Value::from("ipv6");
let ipv6_val = config
.get(&ipv6_key)
.and_then(|v| v.as_bool())
.unwrap_or(false);
if enable {
revise!(dns_val, "enable", true);
revise!(dns_val, "ipv6", true);
revise!(dns_val, "enhanced-mode", "fake-ip");
revise!(dns_val, "fake-ip-range", "10.96.0.0/16");
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("10.96.0.2".to_string()).await;
// 检查现有的 enhanced-mode 设置
let current_mode = dns_val
.get(&Value::from("enhanced-mode"))
.and_then(|v| v.as_str())
.unwrap_or("fake-ip");
// 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
if current_mode == "fake-ip" || !dns_val.contains_key(&Value::from("enhanced-mode")) {
revise!(dns_val, "enable", true);
revise!(dns_val, "ipv6", ipv6_val);
if !dns_val.contains_key(&Value::from("enhanced-mode")) {
revise!(dns_val, "enhanced-mode", "fake-ip");
}
if !dns_val.contains_key(&Value::from("fake-ip-range")) {
revise!(dns_val, "fake-ip-range", "198.18.0.1/16");
}
#[cfg(target_os = "macos")]
{
crate::utils::resolve::restore_public_dns().await;
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await;
}
}
} else {
revise!(dns_val, "enhanced-mode", "redir-host");
revise!(
dns_val,
"enable",
dns_val
.get("enable")
.and_then(|v| v.as_bool())
.unwrap_or(true)
);
revise!(dns_val, "ipv6", ipv6_val);
revise!(
dns_val,
"enhanced-mode",
dns_val
.get("enhanced-mode")
.and_then(|v| v.as_str())
.unwrap_or("redir-host")
);
revise!(
dns_val,
"fake-ip-range",
dns_val
.get("fake-ip-range")
.and_then(|v| v.as_str())
.unwrap_or("198.18.0.1/16")
);
#[cfg(target_os = "macos")]
crate::utils::resolve::restore_public_dns().await;
}
+216 -69
View File
@@ -4,6 +4,7 @@
//! - timer 定时器
//! - cmds 页面调用
//!
use crate::cmds;
use crate::config::*;
use crate::core::*;
use crate::log_err;
@@ -16,16 +17,48 @@ use std::fs;
use tauri::Manager;
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use std::env;
// 打开面板
#[allow(dead_code)]
pub fn open_or_close_dashboard() {
println!("Attempting to open/close dashboard");
log::info!(target: "app", "Attempting to open/close dashboard");
if let Some(window) = handle::Handle::global().get_window() {
if let Ok(true) = window.is_focused() {
let _ = window.hide();
return;
println!("Found existing window");
log::info!(target: "app", "Found existing window");
// 如果窗口存在,则切换其显示状态
match window.is_visible() {
Ok(visible) => {
println!("Window visibility status: {}", visible);
log::info!(target: "app", "Window visibility status: {}", visible);
if visible {
println!("Attempting to hide window");
log::info!(target: "app", "Attempting to hide window");
let _ = window.hide();
} else {
println!("Attempting to show and focus window");
log::info!(target: "app", "Attempting to show and focus window");
if window.is_minimized().unwrap_or(false) {
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
}
}
Err(e) => {
println!("Failed to get window visibility: {:?}", e);
log::error!(target: "app", "Failed to get window visibility: {:?}", e);
}
}
} else {
println!("No existing window found, creating new window");
log::info!(target: "app", "No existing window found, creating new window");
resolve::create_window();
}
resolve::create_window();
}
// 重启clash
@@ -52,16 +85,44 @@ pub fn restart_app() {
resolve::resolve_reset();
let app_handle = handle::Handle::global().app_handle().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1));
let _ = app_handle.save_window_state(StateFlags::default());
tauri::process::restart(&app_handle.env());
});
}
/// 设置窗口状态监控,实时保存窗口位置和大小
pub fn setup_window_state_monitor(app_handle: &tauri::AppHandle) {
let window = app_handle.get_webview_window("main").unwrap();
let app_handle_clone = app_handle.clone();
// 监听窗口移动事件
let app_handle_move = app_handle_clone.clone();
window.on_window_event(move |event| {
match event {
// 窗口移动时保存状态
tauri::WindowEvent::Moved(_) => {
let _ = app_handle_move.save_window_state(StateFlags::all());
},
// 窗口调整大小时保存状态
tauri::WindowEvent::Resized(_) => {
let _ = app_handle_move.save_window_state(StateFlags::all());
},
// 其他可能改变窗口状态的事件
tauri::WindowEvent::ScaleFactorChanged { .. } => {
let _ = app_handle_move.save_window_state(StateFlags::all());
},
// 窗口关闭时保存
tauri::WindowEvent::CloseRequested { .. } => {
let _ = app_handle_move.save_window_state(StateFlags::all());
},
_ => {}
}
});
}
// 切换模式 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}");
@@ -72,7 +133,8 @@ pub fn change_clash_mode(mode: String) {
if Config::clash().data().save_config().is_ok() {
handle::Handle::refresh_clash();
log_err!(handle::Handle::update_systray_part());
log_err!(tray::Tray::global().update_menu());
log_err!(tray::Tray::global().update_icon(None));
}
}
Err(err) => log::error!(target: "app", "{err}"),
@@ -89,7 +151,7 @@ pub fn toggle_system_proxy() {
match patch_verge(IVerge {
enable_system_proxy: Some(!enable),
..IVerge::default()
})
}, false)
.await
{
Ok(_) => handle::Handle::refresh_verge(),
@@ -98,8 +160,23 @@ pub fn toggle_system_proxy() {
});
}
// 切换代理文件
pub fn toggle_proxy_profile(profile_index: String) {
tauri::async_runtime::spawn(async move {
let app_handle = handle::Handle::global().app_handle().unwrap();
match cmds::patch_profiles_config_by_profile_index(app_handle, profile_index).await {
Ok(_) => {
let _ = tray::Tray::global().update_menu();
}
Err(err) => {
log::error!(target: "app", "{err}");
}
}
});
}
// 切换tun模式
pub fn toggle_tun_mode() {
pub fn toggle_tun_mode(not_save_file: Option<bool>) {
let enable = Config::verge().data().enable_tun_mode;
let enable = enable.unwrap_or(false);
@@ -107,7 +184,7 @@ pub fn toggle_tun_mode() {
match patch_verge(IVerge {
enable_tun_mode: Some(!enable),
..IVerge::default()
})
}, not_save_file.unwrap_or(false))
.await
{
Ok(_) => handle::Handle::refresh_verge(),
@@ -119,16 +196,9 @@ pub fn toggle_tun_mode() {
pub fn quit(code: Option<i32>) {
let app_handle = handle::Handle::global().app_handle().unwrap();
handle::Handle::global().set_is_exiting();
toggle_tun_mode(Some(true));
resolve::resolve_reset();
log_err!(handle::Handle::global().get_window().unwrap().close());
match app_handle.save_window_state(StateFlags::all()) {
Ok(_) => {
log::info!(target: "app", "window state saved successfully");
}
Err(e) => {
log::error!(target: "app", "failed to save window state: {}", e);
}
};
app_handle.exit(code.unwrap_or(0));
}
@@ -141,16 +211,15 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
if patch.get("secret").is_some() || patch.get("external-controller").is_some() {
Config::generate().await?;
CoreManager::global().restart_core().await?;
handle::Handle::refresh_clash();
} else {
if patch.get("mode").is_some() {
log_err!(handle::Handle::update_systray_part());
log_err!(tray::Tray::global().update_menu());
log_err!(tray::Tray::global().update_icon(None));
}
Config::runtime().latest().patch_config(patch);
update_core_config(false).await?;
CoreManager::global().update_config().await?;
}
handle::Handle::refresh_clash();
<Result<()>>::Ok(())
};
match res {
@@ -168,7 +237,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
/// 修改verge的订阅
/// 一般都是一个个的修改
pub async fn patch_verge(patch: IVerge) -> Result<()> {
pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> {
Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode;
@@ -179,6 +248,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language;
let mixed_port = patch.verge_mixed_port;
let lite_mode = patch.enable_lite_mode;
#[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon;
#[cfg(not(target_os = "macos"))]
@@ -198,23 +268,33 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port;
let enable_tray_speed = patch.enable_tray_speed;
let enable_global_hotkey = patch.enable_global_hotkey;
let res: std::result::Result<(), anyhow::Error> = {
let mut should_restart_core = false;
let mut should_update_clash_config = false;
let mut should_update_verge_config = false;
let mut should_update_launch = false;
let mut should_update_sysproxy = false;
let mut should_update_systray_part = false;
let mut should_update_systray_icon = false;
let mut should_update_hotkey = false;
let mut should_update_systray_menu = false;
let mut should_update_systray_tooltip = false;
if tun_mode.is_some() {
should_update_clash_config = true;
should_update_systray_menu = true;
should_update_systray_tooltip = true;
should_update_systray_icon = true;
}
if enable_global_hotkey.is_some() {
should_update_verge_config = true;
}
#[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() || redir_port.is_some() {
should_restart_core = true;
}
#[cfg(target_os = "linux")]
if tproxy_enabled.is_some() || tproxy_port.is_some() {
should_restart_core = true;
@@ -230,31 +310,48 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
if auto_launch.is_some() {
should_update_launch = true;
}
if system_proxy.is_some()
|| proxy_bypass.is_some()
|| mixed_port.is_some()
|| pac.is_some()
|| pac_content.is_some()
{
if system_proxy.is_some() {
should_update_sysproxy = true;
should_update_systray_menu = true;
should_update_systray_tooltip = true;
should_update_systray_icon = true;
}
if proxy_bypass.is_some() || pac_content.is_some() || pac.is_some() {
should_update_sysproxy = true;
}
if language.is_some()
|| system_proxy.is_some()
|| tun_mode.is_some()
|| common_tray_icon.is_some()
if language.is_some() {
should_update_systray_menu = true;
}
if common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some()
|| tray_icon.is_some()
{
should_update_systray_part = true;
should_update_systray_icon = true;
}
if patch.hotkeys.is_some() {
should_update_hotkey = true;
should_update_systray_menu = true;
}
if enable_tray_speed.is_some() {
should_update_systray_icon = true;
}
if should_restart_core {
Config::generate().await?;
CoreManager::global().restart_core().await?;
}
if should_update_clash_config {
update_core_config(false).await?;
CoreManager::global().update_config().await?;
handle::Handle::refresh_clash();
}
if should_update_verge_config {
Config::verge().draft().enable_global_hotkey = enable_global_hotkey;
handle::Handle::refresh_verge();
}
if should_update_launch {
sysopt::Sysopt::global().update_launch()?;
@@ -264,12 +361,36 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
sysopt::Sysopt::global().update_sysproxy().await?;
}
if let Some(hotkeys) = patch.hotkeys {
hotkey::Hotkey::global().update(hotkeys)?;
if should_update_hotkey {
hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
}
if should_update_systray_part {
handle::Handle::update_systray_part()?;
if should_update_systray_menu {
tray::Tray::global().update_menu()?;
}
if should_update_systray_icon {
tray::Tray::global().update_icon(None)?;
}
if should_update_systray_tooltip {
tray::Tray::global().update_tooltip()?;
}
// 处理轻量模式切换
if lite_mode.is_some() {
if let Some(window) = handle::Handle::global().get_window() {
if lite_mode.unwrap() {
// 完全退出 webview 进程
window.close()?; // 先关闭窗口
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Some(webview) = app_handle.get_webview_window("main") {
webview.destroy()?; // 销毁 webview 进程
}
} else {
resolve::create_window(); // 重新创建窗口
}
}
}
<Result<()>>::Ok(())
@@ -277,7 +398,9 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
match res {
Ok(()) => {
Config::verge().apply();
Config::verge().data().save_file()?;
if !not_save_file {
Config::verge().data().save_file()?;
}
Ok(())
}
@@ -291,6 +414,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
/// 更新某个profile
/// 如果更新当前订阅就激活订阅
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid);
let url_opt = {
let profiles = Config::profiles();
let profiles = profiles.latest();
@@ -298,64 +423,69 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
if !is_remote {
None // 直接更新
println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
None // 非远程订阅直接更新
} else if item.url.is_none() {
println!("[订阅更新] {} 缺少URL,无法更新", uid);
bail!("failed to get the profile item url");
} else {
println!("[订阅更新] {} 是远程订阅,URL: {}", uid, item.url.clone().unwrap());
Some((item.url.clone().unwrap(), item.option.clone()))
}
};
let should_update = match url_opt {
Some((url, opt)) => {
println!("[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
println!("[订阅更新] 更新订阅配置");
let profiles = Config::profiles();
let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?;
Some(uid) == profiles.get_current()
let is_current = Some(uid.clone()) == profiles.get_current();
println!("[订阅更新] 是否为当前使用的订阅: {}", is_current);
is_current
}
None => true,
};
if should_update {
update_core_config(true).await?;
println!("[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await {
Ok(_) => {
println!("[订阅更新] 更新成功");
handle::Handle::refresh_clash();
}
Err(err) => {
println!("[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target: "app", "{err}");
}
}
}
Ok(())
}
/// 更新订阅
async fn update_core_config(notice: bool) -> Result<()> {
match CoreManager::global().update_config().await {
Ok(_) => {
handle::Handle::refresh_clash();
if notice {
handle::Handle::notice_message("set_config::ok", "ok");
}
Ok(())
}
Err(err) => {
if notice {
handle::Handle::notice_message("set_config::error", format!("{err}"));
}
Err(err)
}
}
}
/// copy env variable
pub fn copy_clash_env() {
// 从环境变量获取IP地址,默认127.0.0.1
let clash_verge_rev_ip = env::var("CLASH_VERGE_REV_IP").unwrap_or_else(|_| "127.0.0.1".to_string());
let app_handle = handle::Handle::global().app_handle().unwrap();
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 http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let nu: String =
format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}");
let cliboard = app_handle.clipboard();
let env_type = { Config::verge().latest().env_type.clone() };
@@ -374,6 +504,7 @@ pub fn copy_clash_env() {
"bash" => cliboard.write_text(sh).unwrap_or_default(),
"cmd" => cliboard.write_text(cmd).unwrap_or_default(),
"powershell" => cliboard.write_text(ps).unwrap_or_default(),
"nushell" => cliboard.write_text(nu).unwrap_or_default(),
_ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
};
}
@@ -464,6 +595,12 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> {
}
pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let verge = Config::verge();
let verge_data = verge.data().clone();
let webdav_url = verge_data.webdav_url.clone();
let webdav_username = verge_data.webdav_username.clone();
let webdav_password = verge_data.webdav_password.clone();
let backup_storage_path = app_home_dir().unwrap().join(&filename);
backup::WebDavClient::global()
.download(filename, backup_storage_path.clone())
@@ -477,6 +614,16 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
let mut zip = zip::ZipArchive::new(fs::File::open(backup_storage_path.clone())?)?;
zip.extract(app_home_dir()?)?;
log_err!(
patch_verge(IVerge {
webdav_url,
webdav_username,
webdav_password,
..IVerge::default()
},
false)
.await
);
// 最后删除临时文件
fs::remove_file(backup_storage_path)?;
Ok(())
+27 -22
View File
@@ -6,9 +6,9 @@ mod feat;
mod utils;
use crate::core::hotkey;
use crate::utils::{resolve, resolve::resolve_scheme, server};
#[cfg(target_os = "macos")]
use tauri::Listener;
use config::Config;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
pub fn run() {
// 单例检测
@@ -48,30 +48,22 @@ pub fn run() {
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_window_state::Builder::default().build())
.setup(|app| {
#[cfg(target_os = "linux")]
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
log_err!(app.deep_link().register_all());
}
#[cfg(target_os = "macos")]
{
app.listen("deep-link://new-url", |event| {
tauri::async_runtime::spawn(async move {
let payload = event.payload();
log_err!(resolve_scheme(payload.to_owned()).await);
});
app.deep_link().on_open_url(|event| {
tauri::async_runtime::spawn(async move {
if let Some(url) = event.urls().first() {
log_err!(resolve_scheme(url.to_string()).await);
}
});
}
});
tauri::async_runtime::block_on(async move {
resolve::resolve_setup(app).await;
#[cfg(not(target_os = "macos"))]
{
let argvs: Vec<String> = std::env::args().collect();
if argvs.len() > 1 {
log_err!(resolve_scheme(argvs[1].to_owned()).await);
}
}
});
Ok(())
@@ -86,12 +78,12 @@ pub fn run() {
cmds::open_core_dir,
cmds::get_portable_flag,
cmds::get_network_interfaces,
// cmds::kill_sidecar,
cmds::restart_core,
cmds::restart_app,
// clash
cmds::get_clash_info,
cmds::patch_clash_config,
cmds::patch_clash_mode,
cmds::change_clash_core,
cmds::get_runtime_config,
cmds::get_runtime_yaml,
@@ -109,7 +101,6 @@ pub fn run() {
cmds::open_devtools,
cmds::exit_app,
cmds::get_network_interfaces_info,
// cmds::update_hotkeys,
// profile
cmds::get_profiles,
cmds::enhance_profiles,
@@ -123,6 +114,9 @@ pub fn run() {
cmds::delete_profile,
cmds::read_profile_file,
cmds::save_profile_file,
// script validation
cmds::script_validate_notice,
cmds::validate_script_file,
// clash api
cmds::clash_api_get_proxy_delay,
// backup
@@ -146,7 +140,6 @@ pub fn run() {
tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() {
api.prevent_exit();
return;
}
}
tauri::RunEvent::WindowEvent { label, event, .. } => {
@@ -171,6 +164,12 @@ pub fn run() {
{
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit"));
};
{
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
if !is_enable_global_hotkey {
log_err!(hotkey::Hotkey::global().init())
}
}
}
tauri::WindowEvent::Focused(false) => {
#[cfg(target_os = "macos")]
@@ -181,6 +180,12 @@ pub fn run() {
{
log_err!(hotkey::Hotkey::global().unregister("Control+Q"));
};
{
let is_enable_global_hotkey = Config::verge().latest().enable_global_hotkey.unwrap_or(true);
if !is_enable_global_hotkey {
log_err!(hotkey::Hotkey::global().reset())
}
}
}
tauri::WindowEvent::Destroyed => {
#[cfg(target_os = "macos")]
+46 -21
View File
@@ -4,7 +4,6 @@ use nanoid::nanoid;
use serde::{de::DeserializeOwned, Serialize};
use serde_yaml::{Mapping, Value};
use std::{fs, path::PathBuf, str::FromStr};
use tauri_plugin_shell::ShellExt;
/// read data from yaml as struct T
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
@@ -99,12 +98,24 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
}
/// open file
/// use vscode by default
pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
app.shell().open(path.to_string_lossy(), None).unwrap();
pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
open::that_detached(path.as_os_str())?;
Ok(())
}
#[cfg(target_os = "macos")]
pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result<bool> {
let img = image::load_from_memory(data)?;
let rgb_img = img.to_rgb8();
for pixel in rgb_img.pixels() {
if pixel[0] != pixel[1] || pixel[1] != pixel[2] {
return Ok(false);
}
}
Ok(true)
}
#[cfg(target_os = "linux")]
pub fn linux_elevator() -> String {
use std::process::Command;
@@ -190,22 +201,36 @@ macro_rules! t {
};
}
/// 将字节数转换为可读的流量字符串
/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换
///
/// # Examples
/// ```
/// assert_eq!(format_bytes_speed(1000), "1000B/s");
/// assert_eq!(format_bytes_speed(1024), "1.0KB/s");
/// assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
/// ```
#[cfg(target_os = "macos")]
pub fn format_bytes_speed(speed: u64) -> String {
if speed < 1024 {
format!("{}B/s", speed)
} else if speed < 1024 * 1024 {
format!("{:.1}KB/s", speed as f64 / 1024.0)
} else if speed < 1024 * 1024 * 1024 {
format!("{:.1}MB/s", speed as f64 / 1024.0 / 1024.0)
} else {
format!("{:.1}GB/s", speed as f64 / 1024.0 / 1024.0 / 1024.0)
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_parse_value() {
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, "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);
fn test_format_bytes_speed() {
assert_eq!(format_bytes_speed(0), "0B/s");
assert_eq!(format_bytes_speed(1023), "1023B/s");
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
}
+88
View File
@@ -0,0 +1,88 @@
use crate::config::Config;
use crate::utils::dirs;
use once_cell::sync::Lazy;
use serde_json::Value;
use std::{collections::HashMap, fs, path::PathBuf};
use sys_locale;
const DEFAULT_LANGUAGE: &str = "zh";
fn get_locales_dir() -> Option<PathBuf> {
dirs::app_resources_dir()
.map(|resource_path| resource_path.join("locales"))
.ok()
}
pub fn get_supported_languages() -> Vec<String> {
let mut languages = Vec::new();
if let Some(locales_dir) = get_locales_dir() {
if let Ok(entries) = fs::read_dir(locales_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str() {
if let Some(lang) = file_name.strip_suffix(".json") {
languages.push(lang.to_string());
}
}
}
}
}
if languages.is_empty() {
languages.push(DEFAULT_LANGUAGE.to_string());
}
languages
}
static TRANSLATIONS: Lazy<HashMap<String, Value>> = Lazy::new(|| {
let mut translations = HashMap::new();
if let Some(locales_dir) = get_locales_dir() {
for lang in get_supported_languages() {
let file_path = locales_dir.join(format!("{}.json", lang));
if let Ok(content) = fs::read_to_string(file_path) {
if let Ok(json) = serde_json::from_str(&content) {
translations.insert(lang.to_string(), json);
}
}
}
}
translations
});
fn get_system_language() -> String {
sys_locale::get_locale()
.map(|locale| locale.to_lowercase())
.and_then(|locale| locale.split(['_', '-']).next().map(String::from))
.filter(|lang| get_supported_languages().contains(lang))
.unwrap_or_else(|| DEFAULT_LANGUAGE.to_string())
}
pub fn t(key: &str) -> String {
let current_lang = Config::verge()
.latest()
.language
.as_deref()
.map(String::from)
.unwrap_or_else(get_system_language);
if let Some(text) = TRANSLATIONS
.get(&current_lang)
.and_then(|trans| trans.get(key))
.and_then(|val| val.as_str())
{
return text.to_string();
}
if current_lang != DEFAULT_LANGUAGE {
if let Some(text) = TRANSLATIONS
.get(DEFAULT_LANGUAGE)
.and_then(|trans| trans.get(key))
.and_then(|val| val.as_str())
{
return text.to_string();
}
}
key.to_string()
}
-5
View File
@@ -193,12 +193,7 @@ pub fn init_resources() -> Result<()> {
let _ = fs::create_dir_all(&res_dir);
}
#[cfg(target_os = "windows")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(target_os = "macos")]
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
#[cfg(target_os = "linux")]
let file_list: [&str; 0] = [];
// copy the resource file
// if the source file is newer than the destination file, copy it over
+1
View File
@@ -5,3 +5,4 @@ pub mod init;
pub mod resolve;
pub mod server;
pub mod tmpl;
pub mod i18n;
+81 -62
View File
@@ -1,14 +1,13 @@
use crate::config::IVerge;
use crate::utils::error;
use crate::{config::Config, config::PrfItem, core::*, utils::init, utils::server};
use crate::{log_err, trace_err, wrap_err};
use crate::{log_err, wrap_err};
use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use percent_encoding::percent_decode_str;
use serde_yaml::Mapping;
use std::net::TcpListener;
use tauri::{App, Manager};
use tauri_plugin_window_state::{StateFlags, WindowExt};
use url::Url;
//#[cfg(not(target_os = "linux"))]
@@ -93,24 +92,31 @@ pub async fn resolve_setup(app: &mut App) {
server::embed_server();
log::trace!(target: "app", "init system tray");
log_err!(tray::Tray::create_systray());
log_err!(tray::Tray::global().init());
log_err!(tray::Tray::global().create_systray());
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
// 初始化热键
log::trace!(target: "app", "init hotkeys");
log_err!(hotkey::Hotkey::global().init());
let silent_start = { Config::verge().data().enable_silent_start };
if !silent_start.unwrap_or(false) {
create_window();
}
log_err!(sysopt::Sysopt::global().update_sysproxy().await);
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
log_err!(handle::Handle::update_systray_part());
log_err!(hotkey::Hotkey::global().init());
log_err!(tray::Tray::global().update_part());
log_err!(timer::Timer::global().init());
}
/// reset system proxy
pub fn resolve_reset() {
tauri::async_runtime::block_on(async move {
#[cfg(target_os = "macos")]
tray::Tray::global().unsubscribe_traffic();
log_err!(sysopt::Sysopt::global().reset_sysproxy().await);
log_err!(CoreManager::global().stop_core().await);
#[cfg(target_os = "macos")]
@@ -120,71 +126,85 @@ pub fn resolve_reset() {
/// create main window
pub fn create_window() {
println!("Starting to create window");
log::info!(target: "app", "Starting to create window");
let app_handle = handle::Handle::global().app_handle().unwrap();
if let Some(window) = handle::Handle::global().get_window() {
trace_err!(window.show(), "set win visible");
trace_err!(window.set_focus(), "set win focus");
println!("Found existing window, trying to show it");
log::info!(target: "app", "Found existing window, trying to show it");
if window.is_minimized().unwrap_or(false) {
println!("Window is minimized, unminimizing");
log::info!(target: "app", "Window is minimized, unminimizing");
let _ = window.unminimize();
}
let _ = window.show();
let _ = window.set_focus();
return;
}
let builder = tauri::WebviewWindowBuilder::new(
println!("Creating new window");
log::info!(target: "app", "Creating new window");
#[cfg(target_os = "windows")]
let window = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0)
.decorations(false)
.maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.shadow(true)
.build();
#[cfg(target_os = "macos")]
let window = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0)
.build();
#[cfg(target_os = "linux")]
let window = tauri::WebviewWindowBuilder::new(
&app_handle,
"main".to_string(),
tauri::WebviewUrl::App("index.html".into()),
)
.title("Clash Verge")
.visible(false)
.fullscreen(false)
.min_inner_size(600.0, 520.0);
.decorations(false)
.inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0)
.transparent(true)
.build();
#[cfg(target_os = "windows")]
let window = builder
.decorations(false)
.maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true)
.visible(false)
.build().unwrap();
#[cfg(target_os = "macos")]
let window = builder
.decorations(true)
.hidden_title(true)
.title_bar_style(tauri::TitleBarStyle::Overlay)
.build()
.unwrap();
#[cfg(target_os = "linux")]
let window = builder
.decorations(false)
.transparent(true)
.build()
.unwrap();
match window.restore_state(StateFlags::all()) {
Ok(_) => {
log::info!(target: "app", "window state restored successfully");
match window {
Ok(window) => {
println!("Window created successfully, attempting to show");
log::info!(target: "app", "Window created successfully, attempting to show");
let _ = window.show();
let _ = window.set_focus();
// 设置窗口状态监控,实时保存窗口位置和大小
crate::feat::setup_window_state_monitor(&app_handle);
}
Err(e) => {
log::error!(target: "app", "failed to restore window state: {}", e);
#[cfg(target_os = "windows")]
window
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
width: 800,
height: 636,
}))
.unwrap();
#[cfg(not(target_os = "windows"))]
window
.set_size(tauri::Size::Physical(tauri::PhysicalSize {
width: 800,
height: 642,
}))
.unwrap();
println!("Failed to create window: {:?}", e);
log::error!(target: "app", "Failed to create window: {:?}", e);
}
};
}
}
pub async fn resolve_scheme(param: String) -> Result<()> {
@@ -230,6 +250,8 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
Ok(item) => {
let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item));
handle::Handle::notice_message("import_sub_url::ok", uid);
app_handle
.notification()
.builder()
@@ -237,10 +259,9 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
.body("Import profile success")
.show()
.unwrap();
handle::Handle::notice_message("import_sub_url::ok", uid);
}
Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
app_handle
.notification()
.builder()
@@ -248,8 +269,6 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
.body(format!("Import profile failed: {e}"))
.show()
.unwrap();
handle::Handle::notice_message("import_sub_url::error", e.to_string());
bail!("Failed to add subscriptions: {e}");
}
}
}
+7 -4
View File
@@ -10,13 +10,13 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": ["resources"],
"resources": ["resources", "resources/locales/*"],
"publisher": "Clash Verge Rev",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0",
"category": "DeveloperTool",
"shortDescription": "Clash Verge Rev",
"createUpdaterArtifacts": "v1Compatible"
"createUpdaterArtifacts": true
},
"build": {
"beforeBuildCommand": "pnpm run web:build",
@@ -25,7 +25,7 @@
"devUrl": "http://localhost:3000/"
},
"productName": "Clash Verge",
"version": "2.0.0",
"version": "2.1.2",
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"plugins": {
"updater": {
@@ -33,7 +33,10 @@
"endpoints": [
"https://download.clashverge.dev/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"
]
],
"windows": {
"installMode": "basicUi"
}
},
"deep-link": {
"desktop": {
+1 -2
View File
@@ -3,13 +3,12 @@
"identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": {
"targets": ["app", "dmg"],
"resources": ["resources"],
"macOS": {
"frameworks": [],
"minimumSystemVersion": "10.15",
"exceptionDomain": "",
"signingIdentity": null,
"entitlements": null,
"entitlements": "packages/macos/entitlements.plist",
"dmg": {
"background": "images/background.png",
"appPosition": {
+1 -1
View File
@@ -24,7 +24,7 @@
"trayIcon": {
"iconPath": "icons/tray-icon.ico",
"iconAsTemplate": true,
"menuOnLeftClick": false
"showMenuOnLeftClick": false
},
"windows": []
}
+1 -1
View File
@@ -111,7 +111,7 @@
position: absolute;
top: 0;
left: 0;
right: 2px;
right: 1px;
bottom: 0px;
}
}
+37 -7
View File
@@ -18,7 +18,7 @@ interface InnerProps {
}
const NoticeInner = (props: InnerProps) => {
const { type, message, duration = 1500, onClose } = props;
const { type, message, duration, onClose } = props;
const [visible, setVisible] = useState(true);
const [isDark, setIsDark] = useState(false);
const { verge } = useVerge();
@@ -43,7 +43,7 @@ const NoticeInner = (props: InnerProps) => {
appWindow.theme().then((m) => m && setIsDark(m === "dark"));
const unlisten = appWindow.onThemeChanged((e) =>
setIsDark(e.payload === "dark")
setIsDark(e.payload === "dark"),
);
return () => {
@@ -72,7 +72,7 @@ const NoticeInner = (props: InnerProps) => {
<Snackbar
open={visible}
anchorOrigin={{ vertical: "top", horizontal: "right" }}
autoHideDuration={duration}
autoHideDuration={duration === -1 ? null : duration || 1500}
onClose={onAutoClose}
message={msgElement}
sx={{
@@ -105,25 +105,55 @@ let parent: HTMLDivElement = null!;
// @ts-ignore
export const Notice: NoticeInstance = (props) => {
const { type, message, duration } = props;
// 验证必要的参数
if (!message) {
return;
}
if (!parent) {
parent = document.createElement("div");
parent.setAttribute("id", "notice-container"); // 添加 id 便于调试
document.body.appendChild(parent);
}
const container = document.createElement("div");
parent.appendChild(container);
const root = createRoot(container);
const onUnmount = () => {
root.unmount();
if (parent) setTimeout(() => parent.removeChild(container), 500);
if (parent && container.parentNode === parent) {
setTimeout(() => {
parent.removeChild(container);
}, 500);
}
};
root.render(<NoticeInner {...props} onClose={onUnmount} />);
root.render(
<NoticeInner
type={type}
message={message}
duration={duration || 1500}
onClose={onUnmount}
/>,
);
};
(["info", "error", "success"] as const).forEach((type) => {
Notice[type] = (message, duration) => {
setTimeout(() => Notice({ type, message, duration }), 0);
Notice[type] = (message: ReactNode, duration?: number) => {
// 确保消息不为空
if (!message) {
return;
}
Notice({
type,
message,
// 错误类型通知显示 8 秒,其他类型默认 1.5 秒
duration: type === "error" ? 8000 : duration || 1500,
});
};
});
+62 -65
View File
@@ -1,6 +1,6 @@
import { Box, SvgIcon, TextField, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { ChangeEvent, useEffect, useRef, useState, useMemo } from "react";
import { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
@@ -22,7 +22,20 @@ type SearchProps = {
onSearch: (match: (content: string) => boolean, state: SearchState) => void;
};
export const BaseSearchBox = styled((props: SearchProps) => {
const StyledTextField = styled(TextField)(({ theme }) => ({
"& .MuiInputBase-root": {
background: theme.palette.mode === "light" ? "#fff" : undefined,
paddingRight: "4px",
},
"& .MuiInputBase-root svg[aria-label='active'] path": {
fill: theme.palette.primary.light,
},
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
fill: "#A7A7A7",
},
}));
export const BaseSearchBox = (props: SearchProps) => {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
@@ -43,58 +56,58 @@ export const BaseSearchBox = styled((props: SearchProps) => {
inheritViewBox: true,
};
useEffect(() => {
if (!inputRef.current) return;
onChange({
target: inputRef.current,
} as ChangeEvent<HTMLInputElement>);
}, [matchCase, matchWholeWord, useRegularExpression]);
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
props.onSearch(
(content) => doSearch([content], e.target?.value ?? "").length > 0,
{
text: e.target?.value ?? "",
matchCase,
matchWholeWord,
useRegularExpression,
},
);
};
const doSearch = (searchList: string[], searchItem: string) => {
setErrorMessage("");
return searchList.filter((item) => {
const createMatcher = useMemo(() => {
return (searchText: string) => {
try {
let searchItemCopy = searchItem;
if (!matchCase) {
item = item.toLowerCase();
searchItemCopy = searchItemCopy.toLowerCase();
}
if (matchWholeWord) {
const regex = new RegExp(`\\b${searchItemCopy}\\b`);
return (content: string) => {
if (!searchText) return true;
let item = !matchCase ? content.toLowerCase() : content;
let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
if (useRegularExpression) {
const regexWithOptions = new RegExp(searchItemCopy);
return regexWithOptions.test(item) && regex.test(item);
} else {
return regex.test(item);
return new RegExp(searchItem).test(item);
}
} else if (useRegularExpression) {
const regex = new RegExp(searchItemCopy);
return regex.test(item);
} else {
return item.includes(searchItemCopy);
}
if (matchWholeWord) {
return new RegExp(`\\b${searchItem}\\b`).test(item);
}
return item.includes(searchItem);
};
} catch (err) {
setErrorMessage(`${err}`);
return () => true;
}
};
}, [matchCase, matchWholeWord, useRegularExpression]);
useEffect(() => {
if (!inputRef.current) return;
const value = inputRef.current.value;
setErrorMessage("");
props.onSearch(createMatcher(value), {
text: value,
matchCase,
matchWholeWord,
useRegularExpression,
});
}, [matchCase, matchWholeWord, useRegularExpression, createMatcher]);
const onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const value = e.target?.value ?? "";
setErrorMessage("");
props.onSearch(createMatcher(value), {
text: value,
matchCase,
matchWholeWord,
useRegularExpression,
});
};
return (
<Tooltip title={errorMessage} placement="bottom-start">
<TextField
<StyledTextField
autoComplete="new-password"
inputRef={inputRef}
hiddenLabel
@@ -115,9 +128,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
component={matchCaseIcon}
{...iconStyle}
aria-label={matchCase ? "active" : "inactive"}
onClick={() => {
setMatchCase(!matchCase);
}}
onClick={() => setMatchCase(!matchCase)}
/>
</div>
</Tooltip>
@@ -127,9 +138,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
component={matchWholeWordIcon}
{...iconStyle}
aria-label={matchWholeWord ? "active" : "inactive"}
onClick={() => {
setMatchWholeWord(!matchWholeWord);
}}
onClick={() => setMatchWholeWord(!matchWholeWord)}
/>
</div>
</Tooltip>
@@ -139,28 +148,16 @@ export const BaseSearchBox = styled((props: SearchProps) => {
component={useRegularExpressionIcon}
aria-label={useRegularExpression ? "active" : "inactive"}
{...iconStyle}
onClick={() => {
setUseRegularExpression(!useRegularExpression);
}}
onClick={() =>
setUseRegularExpression(!useRegularExpression)
}
/>{" "}
</div>
</Tooltip>
</Box>
),
}}
{...props}
/>
</Tooltip>
);
})(({ theme }) => ({
"& .MuiInputBase-root": {
background: theme.palette.mode === "light" ? "#fff" : undefined,
"padding-right": "4px",
},
"& .MuiInputBase-root svg[aria-label='active'] path": {
fill: theme.palette.primary.light,
},
"& .MuiInputBase-root svg[aria-label='inactive'] path": {
fill: "#A7A7A7",
},
}));
};
+1 -1
View File
@@ -49,7 +49,7 @@ export const Switch = styled((props: SwitchProps) => (
},
"& .MuiSwitch-track": {
borderRadius: 26 / 2,
backgroundColor: theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
backgroundColor: theme.palette.mode === "light" ? "#BBBBBB" : "#39393D",
opacity: 1,
transition: theme.transitions.create(["background-color"], {
duration: 500,
@@ -38,7 +38,7 @@ export const ConnectionDetail = forwardRef<ConnectionDetailRef>(
}
/>
);
}
},
);
interface InnerProps {
@@ -52,7 +52,10 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
const host = metadata.host
? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`;
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
const Destination = metadata.destinationIP
? metadata.destinationIP
: metadata.remoteDestination;
const information = [
{ label: t("Host"), value: host },
@@ -79,7 +82,8 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
label: t("Source"),
value: `${metadata.sourceIP}:${metadata.sourcePort}`,
},
{ label: t("Destination IP"), value: metadata.destinationIP },
{ label: t("Destination"), value: Destination },
{ label: t("DestinationPort"), value: `${metadata.destinationPort}` },
{ label: t("Type"), value: `${metadata.type}(${metadata.network})` },
];
+79 -20
View File
@@ -1,6 +1,6 @@
import dayjs from "dayjs";
import { useMemo, useState } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid";
import { useMemo, useState, useEffect } from "react";
import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
import { useThemeMode } from "@/services/states";
import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic";
@@ -21,12 +21,24 @@ export const ConnectionTable = (props: Props) => {
Partial<Record<keyof IConnectionsItem, boolean>>
>({});
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(
() => {
const saved = localStorage.getItem("connection-table-widths");
return saved ? JSON.parse(saved) : {};
},
);
const [columns] = useState<GridColDef[]>([
{ field: "host", headerName: t("Host"), flex: 220, minWidth: 220 },
{
field: "host",
headerName: t("Host"),
width: columnWidths["host"] || 220,
minWidth: 180,
},
{
field: "download",
headerName: t("Downloaded"),
width: 88,
width: columnWidths["download"] || 88,
align: "right",
headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" "),
@@ -34,7 +46,7 @@ export const ConnectionTable = (props: Props) => {
{
field: "upload",
headerName: t("Uploaded"),
width: 88,
width: columnWidths["upload"] || 88,
align: "right",
headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" "),
@@ -42,7 +54,7 @@ export const ConnectionTable = (props: Props) => {
{
field: "dlSpeed",
headerName: t("DL Speed"),
width: 88,
width: columnWidths["dlSpeed"] || 88,
align: "right",
headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
@@ -50,18 +62,33 @@ export const ConnectionTable = (props: Props) => {
{
field: "ulSpeed",
headerName: t("UL Speed"),
width: 88,
width: columnWidths["ulSpeed"] || 88,
align: "right",
headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
},
{ field: "chains", headerName: t("Chains"), flex: 360, minWidth: 360 },
{ field: "rule", headerName: t("Rule"), flex: 300, minWidth: 250 },
{ field: "process", headerName: t("Process"), flex: 240, minWidth: 120 },
{
field: "chains",
headerName: t("Chains"),
width: columnWidths["chains"] || 340,
minWidth: 180,
},
{
field: "rule",
headerName: t("Rule"),
width: columnWidths["rule"] || 280,
minWidth: 180,
},
{
field: "process",
headerName: t("Process"),
width: columnWidths["process"] || 220,
minWidth: 180,
},
{
field: "time",
headerName: t("Time"),
flex: 120,
width: columnWidths["time"] || 120,
minWidth: 100,
align: "right",
headerAlign: "right",
@@ -69,26 +96,56 @@ export const ConnectionTable = (props: Props) => {
new Date(v2).getTime() - new Date(v1).getTime(),
valueFormatter: (value: number) => dayjs(value).fromNow(),
},
{ field: "source", headerName: t("Source"), flex: 200, minWidth: 130 },
{
field: "destinationIP",
headerName: t("Destination IP"),
flex: 200,
field: "source",
headerName: t("Source"),
width: columnWidths["source"] || 200,
minWidth: 130,
},
{ field: "type", headerName: t("Type"), flex: 160, minWidth: 100 },
{
field: "remoteDestination",
headerName: t("Destination"),
width: columnWidths["remoteDestination"] || 200,
minWidth: 130,
},
{
field: "type",
headerName: t("Type"),
width: columnWidths["type"] || 160,
minWidth: 100,
},
]);
useEffect(() => {
console.log("Saving column widths:", columnWidths);
localStorage.setItem(
"connection-table-widths",
JSON.stringify(columnWidths),
);
}, [columnWidths]);
const handleColumnResize = (params: GridColumnResizeParams) => {
const { colDef, width } = params;
console.log("Column resize:", colDef.field, width);
setColumnWidths((prev) => ({
...prev,
[colDef.field]: width,
}));
};
const connRows = useMemo(() => {
return connections.map((each) => {
const { metadata, rulePayload } = each;
const chains = [...each.chains].reverse().join(" / ");
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
const Destination = metadata.destinationIP
? `${metadata.destinationIP}:${metadata.destinationPort}`
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
return {
id: each.id,
host: metadata.host
? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`,
: `${metadata.remoteDestination}:${metadata.destinationPort}`,
download: each.download,
upload: each.upload,
dlSpeed: each.curDownload,
@@ -98,7 +155,7 @@ export const ConnectionTable = (props: Props) => {
process: truncateStr(metadata.process || metadata.processPath),
time: each.start,
source: `${metadata.sourceIP}:${metadata.sourcePort}`,
destinationIP: metadata.destinationIP,
remoteDestination: Destination,
type: `${metadata.type}(${metadata.network})`,
connectionData: each,
};
@@ -115,12 +172,14 @@ export const ConnectionTable = (props: Props) => {
sx={{
border: "none",
"div:focus": { outline: "none !important" },
"& div[aria-rowindex]": {
backgroundColor: `${backgroundColor} !important`,
"& .MuiDataGrid-columnHeader": {
userSelect: "none",
},
}}
columnVisibilityModel={columnVisible}
onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
onColumnResize={handleColumnResize}
disableColumnMenu={false}
/>
);
};
+6 -6
View File
@@ -48,7 +48,7 @@ export const LayoutTraffic = () => {
const { server = "", secret = "" } = clashInfo!;
const s = createSockette(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`,
`ws://${server}${secret ? `/traffic?token=${encodeURIComponent(secret)}` : "/traffic"}`,
{
onmessage(event) {
const data = JSON.parse(event.data) as ITrafficItem;
@@ -59,7 +59,7 @@ export const LayoutTraffic = () => {
this.close();
next(event, { up: 0, down: 0 });
},
}
},
);
return () => {
@@ -69,7 +69,7 @@ export const LayoutTraffic = () => {
{
fallbackData: { up: 0, down: 0 },
keepPreviousData: true,
}
},
);
/* --------- meta memory information --------- */
@@ -86,7 +86,7 @@ export const LayoutTraffic = () => {
const { server = "", secret = "" } = clashInfo!;
const s = createSockette(
`ws://${server}/memory?token=${encodeURIComponent(secret)}`,
`ws://${server}${secret ? `/memory?token=${encodeURIComponent(secret)}` : "/memory"}`,
{
onmessage(event) {
const data = JSON.parse(event.data) as MemoryUsage;
@@ -96,7 +96,7 @@ export const LayoutTraffic = () => {
this.close();
next(event, { inuse: 0 });
},
}
},
);
return () => {
@@ -106,7 +106,7 @@ export const LayoutTraffic = () => {
{
fallbackData: { inuse: 0 },
keepPreviousData: true,
}
},
);
const [up, upUnit] = parseTraffic(traffic.up);
+4 -2
View File
@@ -1,12 +1,13 @@
import { IconButton, Fade } from "@mui/material";
import { IconButton, Fade, SxProps, Theme } from "@mui/material";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
interface Props {
onClick: () => void;
show: boolean;
sx?: SxProps<Theme>;
}
export const ScrollTopButton = ({ onClick, show }: Props) => {
export const ScrollTopButton = ({ onClick, show, sx }: Props) => {
return (
<Fade in={show}>
<IconButton
@@ -26,6 +27,7 @@ export const ScrollTopButton = ({ onClick, show }: Props) => {
: "rgba(0,0,0,0.2)",
},
visibility: show ? "visible" : "hidden",
...sx,
}}
>
<KeyboardArrowUpIcon />
+2 -2
View File
@@ -90,7 +90,7 @@ export const useCustomTheme = () => {
}
// css
const backgroundColor = mode === "light" ? "#f0f0f0" : "#2e303d";
const backgroundColor = mode === "light" ? "#ECECEC" : "#2e303d";
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
const scrollColor = mode === "light" ? "#90939980" : "#54545480";
const dividerColor =
@@ -104,7 +104,7 @@ export const useCustomTheme = () => {
rootEle.style.setProperty("--primary-main", theme.palette.primary.main);
rootEle.style.setProperty(
"--background-color-alpha",
alpha(theme.palette.primary.main, 0.1)
alpha(theme.palette.primary.main, 0.1),
);
// inject css
+2 -2
View File
@@ -85,9 +85,9 @@ const LogItem = ({ value, searchState }: Props) => {
return (
<Item>
<div>
<span className="time">{value.time}</span>
<span className="time">{renderHighlightText(value.time || "")}</span>
<span className="type" data-type={value.type.toLowerCase()}>
{value.type}
{renderHighlightText(value.type)}
</span>
</div>
<div>
+2 -2
View File
@@ -106,7 +106,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
};
const editorDidMount = async (
editor: monaco.editor.IStandaloneCodeEditor
editor: monaco.editor.IStandaloneCodeEditor,
) => {
editorRef.current = editor;
@@ -203,7 +203,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : ""
}`,
fontLigatures: true, // 连字符
fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动
}}
editorWillMount={editorWillMount}
+3 -3
View File
@@ -34,7 +34,7 @@ export const ProviderButton = () => {
const hasProvider = Object.keys(data || {}).length > 0;
const [updating, setUpdating] = useState(
Object.keys(data || {}).map(() => false)
Object.keys(data || {}).map(() => false),
);
const setUpdatingAt = (status: boolean, index: number) => {
@@ -107,7 +107,7 @@ export const ProviderButton = () => {
const expire = sub?.Expire || 0;
const progress = Math.min(
Math.round(((download + upload) * 100) / (total + 0.01)) + 1,
100
100,
);
return (
<>
@@ -213,7 +213,7 @@ const StyledTypeBox = styled(Box)<{ component?: React.ElementType }>(
marginRight: "4px",
padding: "0 2px",
lineHeight: 1.25,
})
}),
);
const boxStyle = {
+384 -26
View File
@@ -1,4 +1,4 @@
import { useRef, useState, useEffect } from "react";
import { useRef, useState, useEffect, useCallback, useMemo } from "react";
import { useLockFn } from "ahooks";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import {
@@ -16,6 +16,173 @@ import { ProxyRender } from "./proxy-render";
import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next";
import { ScrollTopButton } from "../layout/scroll-top-button";
import { Box, styled } from "@mui/material";
import { memo } from "react";
import { createPortal } from "react-dom";
// 将选择器组件抽离出来,避免主组件重渲染时重复创建样式
const AlphabetSelector = styled(Box)(({ theme }) => ({
position: "fixed",
right: 4,
top: "50%",
transform: "translateY(-50%)",
display: "flex",
flexDirection: "column",
background: "transparent",
zIndex: 1000,
gap: "2px",
// padding: "4px 2px",
willChange: "transform",
"&:hover": {
background: theme.palette.background.paper,
boxShadow: theme.shadows[2],
borderRadius: "8px",
},
"& .scroll-container": {
overflow: "hidden",
maxHeight: "inherit",
},
"& .letter-container": {
display: "flex",
flexDirection: "column",
gap: "2px",
transition: "transform 0.2s ease",
},
"& .letter": {
padding: "1px 4px",
fontSize: "12px",
cursor: "pointer",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
color: theme.palette.text.secondary,
position: "relative",
width: "1.5em",
height: "1.5em",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1)",
transform: "scale(1) translateZ(0)",
backfaceVisibility: "hidden",
borderRadius: "6px",
"&:hover": {
color: theme.palette.primary.main,
transform: "scale(1.4) translateZ(0)",
backgroundColor: theme.palette.action.hover,
},
},
}));
// 创建一个单独的 Tooltip 组件
const Tooltip = styled("div")(({ theme }) => ({
position: "fixed",
background: theme.palette.background.paper,
padding: "4px 8px",
borderRadius: "6px",
boxShadow: theme.shadows[3],
whiteSpace: "nowrap",
fontSize: "16px",
color: theme.palette.text.primary,
pointerEvents: "none",
"&::after": {
content: '""',
position: "absolute",
right: "-4px",
top: "50%",
transform: "translateY(-50%)",
width: 0,
height: 0,
borderTop: "4px solid transparent",
borderBottom: "4px solid transparent",
borderLeft: `4px solid ${theme.palette.background.paper}`,
},
}));
// 抽离字母选择器子组件
const LetterItem = memo(
({
name,
onClick,
getFirstChar,
}: {
name: string;
onClick: (name: string) => void;
getFirstChar: (str: string) => string;
}) => {
const [showTooltip, setShowTooltip] = useState(false);
const letterRef = useRef<HTMLDivElement>(null);
const [tooltipPosition, setTooltipPosition] = useState({
top: 0,
right: 0,
});
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const updateTooltipPosition = useCallback(() => {
if (!letterRef.current) return;
const rect = letterRef.current.getBoundingClientRect();
setTooltipPosition({
top: rect.top + rect.height / 2,
right: window.innerWidth - rect.left + 8,
});
}, []);
useEffect(() => {
if (showTooltip) {
updateTooltipPosition();
}
}, [showTooltip, updateTooltipPosition]);
const handleMouseEnter = useCallback(() => {
setShowTooltip(true);
// 添加 200ms 的延迟,避免鼠标快速划过时触发滚动
hoverTimeoutRef.current = setTimeout(() => {
onClick(name);
}, 100);
}, [name, onClick]);
const handleMouseLeave = useCallback(() => {
setShowTooltip(false);
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
}, []);
useEffect(() => {
return () => {
if (hoverTimeoutRef.current) {
clearTimeout(hoverTimeoutRef.current);
}
};
}, []);
return (
<>
<div
ref={letterRef}
className="letter"
onClick={() => onClick(name)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<span>{getFirstChar(name)}</span>
</div>
{showTooltip &&
createPortal(
<Tooltip
style={{
top: tooltipPosition.top,
right: tooltipPosition.right,
transform: "translateY(-50%)",
}}
>
{name}
</Tooltip>,
document.body,
)}
</>
);
},
);
interface Props {
mode: string;
@@ -32,22 +199,128 @@ export const ProxyGroups = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000;
const virtuosoRef = useRef<VirtuosoHandle>(null);
const scrollPositionRef = useRef<Record<string, number>>({});
const [showScrollTop, setShowScrollTop] = useState(false);
const scrollerRef = useRef<Element | null>(null);
const letterContainerRef = useRef<HTMLDivElement>(null);
const alphabetSelectorRef = useRef<HTMLDivElement>(null);
const [maxHeight, setMaxHeight] = useState("auto");
// 添加滚动处理函数
const handleScroll = (e: any) => {
const scrollTop = e.target.scrollTop;
setShowScrollTop(scrollTop > 100);
};
// 使用useMemo缓存字母索引数据
const { groupFirstLetters, letterIndexMap } = useMemo(() => {
const letters = new Set<string>();
const indexMap: Record<string, number> = {};
renderList.forEach((item, index) => {
if (item.type === 0) {
const fullName = item.group.name;
letters.add(fullName);
if (!(fullName in indexMap)) {
indexMap[fullName] = index;
}
}
});
return {
groupFirstLetters: Array.from(letters),
letterIndexMap: indexMap,
};
}, [renderList]);
// 缓存getFirstChar函数
const getFirstChar = useCallback((str: string) => {
const regex = /\p{Extended_Pictographic}|\p{L}|\p{N}|./u;
const match = str.match(regex);
return match ? match[0] : str.charAt(0);
}, []);
// 从 localStorage 恢复滚动位置
useEffect(() => {
if (renderList.length === 0) return;
try {
const savedPositions = localStorage.getItem("proxy-scroll-positions");
if (savedPositions) {
const positions = JSON.parse(savedPositions);
scrollPositionRef.current = positions;
const savedPosition = positions[mode];
if (savedPosition !== undefined) {
setTimeout(() => {
virtuosoRef.current?.scrollTo({
top: savedPosition,
behavior: "auto",
});
}, 100);
}
}
} catch (e) {
console.error("Error restoring scroll position:", e);
}
}, [mode, renderList]);
// 使用防抖保存滚动位置
const saveScrollPosition = useCallback(
(scrollTop: number) => {
try {
scrollPositionRef.current[mode] = scrollTop;
localStorage.setItem(
"proxy-scroll-positions",
JSON.stringify(scrollPositionRef.current),
);
} catch (e) {
console.error("Error saving scroll position:", e);
}
},
[mode],
);
// 优化滚动处理函数,使用防抖
const handleScroll = useCallback(
debounce((e: any) => {
const scrollTop = e.target.scrollTop;
setShowScrollTop(scrollTop > 100);
saveScrollPosition(scrollTop);
}, 16),
[saveScrollPosition],
);
// 添加和清理滚动事件监听器
useEffect(() => {
const currentScroller = scrollerRef.current;
if (currentScroller) {
currentScroller.addEventListener("scroll", handleScroll, {
passive: true,
});
return () => {
currentScroller.removeEventListener("scroll", handleScroll);
};
}
}, [handleScroll]);
// 滚动到顶部
const scrollToTop = () => {
const scrollToTop = useCallback(() => {
virtuosoRef.current?.scrollTo?.({
top: 0,
behavior: "smooth",
});
};
saveScrollPosition(0);
}, [saveScrollPosition]);
// 处理字母点击,使用useCallback
const handleLetterClick = useCallback(
(name: string) => {
const index = letterIndexMap[name];
if (index !== undefined) {
virtuosoRef.current?.scrollToIndex({
index,
align: "start",
behavior: "smooth",
});
}
},
[letterIndexMap],
);
// 切换分组的节点代理
const handleChangeProxy = useLockFn(
@@ -134,6 +407,64 @@ export const ProxyGroups = (props: Props) => {
}
};
// 添加滚轮事件处理函数
const handleWheel = useCallback((e: WheelEvent) => {
e.preventDefault();
if (!letterContainerRef.current) return;
const container = letterContainerRef.current;
const scrollAmount = e.deltaY;
const currentTransform = new WebKitCSSMatrix(container.style.transform);
const currentY = currentTransform.m42 || 0;
const containerHeight = container.getBoundingClientRect().height;
const parentHeight =
container.parentElement?.getBoundingClientRect().height || 0;
const maxScroll = Math.max(0, containerHeight - parentHeight);
let newY = currentY - scrollAmount;
newY = Math.min(0, Math.max(-maxScroll, newY));
container.style.transform = `translateY(${newY}px)`;
}, []);
// 添加和移除滚轮事件监听
useEffect(() => {
const container = letterContainerRef.current?.parentElement;
if (container) {
container.addEventListener("wheel", handleWheel, { passive: false });
return () => {
container.removeEventListener("wheel", handleWheel);
};
}
}, [handleWheel]);
// 添加窗口大小变化监听和最大高度计算
const updateMaxHeight = useCallback(() => {
if (!alphabetSelectorRef.current) return;
const windowHeight = window.innerHeight;
const bottomMargin = 60; // 底部边距
const topMargin = bottomMargin * 2; // 顶部边距是底部的2倍
const availableHeight = windowHeight - (topMargin + bottomMargin);
// 调整选择器的位置,使其偏下
const offsetPercentage =
(((topMargin - bottomMargin) / windowHeight) * 100) / 2;
alphabetSelectorRef.current.style.top = `calc(48% + ${offsetPercentage}vh)`;
setMaxHeight(`${availableHeight}px`);
}, []);
// 监听窗口大小变化
useEffect(() => {
updateMaxHeight();
window.addEventListener("resize", updateMaxHeight);
return () => {
window.removeEventListener("resize", updateMaxHeight);
};
}, [updateMaxHeight]);
if (mode === "direct") {
return <BaseEmpty text={t("clash_mode_direct")} />;
}
@@ -142,30 +473,57 @@ export const ProxyGroups = (props: Props) => {
<div style={{ position: "relative", height: "100%" }}>
<Virtuoso
ref={virtuosoRef}
style={{ height: "calc(100% - 16px)" }}
style={{ height: "calc(100% - 14px)" }}
totalCount={renderList.length}
increaseViewportBy={256}
increaseViewportBy={{ top: 256, bottom: 256 }}
overscan={150}
defaultItemHeight={56}
scrollerRef={(ref) => {
if (ref) {
ref.addEventListener("scroll", handleScroll);
}
scrollerRef.current = ref as Element;
}}
components={{
Footer: () => <div style={{ height: "8px" }} />,
}}
itemContent={(index) => (
<>
<ProxyRender
key={renderList[index].key}
item={renderList[index]}
indent={mode === "rule" || mode === "script"}
onLocation={handleLocation}
onCheckAll={handleCheckAll}
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
/>
</>
<ProxyRender
key={renderList[index].key}
item={renderList[index]}
indent={mode === "rule" || mode === "script"}
onLocation={handleLocation}
onCheckAll={handleCheckAll}
onHeadState={onHeadState}
onChangeProxy={handleChangeProxy}
/>
)}
/>
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} />
<AlphabetSelector ref={alphabetSelectorRef} style={{ maxHeight }}>
<div className="scroll-container">
<div ref={letterContainerRef} className="letter-container">
{groupFirstLetters.map((name) => (
<LetterItem
key={name}
name={name}
onClick={handleLetterClick}
getFirstChar={getFirstChar}
/>
))}
</div>
</div>
</AlphabetSelector>
</div>
);
};
// 简单的防抖函数
function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number,
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;
return (...args: Parameters<T>) => {
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
+10 -11
View File
@@ -100,7 +100,7 @@ export const ProxyRender = (props: RenderProps) => {
<ListItemText
primary={<StyledPrimary>{group.name}</StyledPrimary>}
secondary={
<ListItemTextChild
<Box
sx={{
overflow: "hidden",
display: "flex",
@@ -108,16 +108,19 @@ export const ProxyRender = (props: RenderProps) => {
pt: "2px",
}}
>
<Box sx={{ marginTop: "2px" }}>
<Box component="span" sx={{ marginTop: "2px" }}>
<StyledTypeBox>{group.type}</StyledTypeBox>
<StyledSubtitle sx={{ color: "text.secondary" }}>
{group.now}
</StyledSubtitle>
</Box>
</ListItemTextChild>
</Box>
}
secondaryTypographyProps={{
sx: { display: "flex", alignItems: "center", color: "#ccc" },
slotProps={{
secondary: {
component: "div",
sx: { display: "flex", alignItems: "center", color: "#ccc" },
},
}}
/>
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
@@ -204,7 +207,7 @@ export const ProxyRender = (props: RenderProps) => {
};
const StyledPrimary = styled("span")`
font-size: 15px;
font-size: 16px;
font-weight: 700;
line-height: 1.5;
overflow: hidden;
@@ -219,11 +222,7 @@ const StyledSubtitle = styled("span")`
white-space: nowrap;
`;
const ListItemTextChild = styled("span")`
display: block;
`;
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
const StyledTypeBox = styled(Box)(({ theme }) => ({
display: "inline-block",
border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5),
+85 -56
View File
@@ -1,5 +1,5 @@
import useSWR from "swr";
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useCallback } from "react";
import { getProxies } from "@/services/api";
import { useVerge } from "@/hooks/use-verge";
import { filterSort } from "./use-filter-sort";
@@ -11,7 +11,7 @@ import {
} from "./use-head-state";
export interface IRenderItem {
// 组 | head | item | empty | item col
// 组 | head | item | empty | item col
type: 0 | 1 | 2 | 3 | 4;
key: string;
group: IProxyGroupItem;
@@ -19,32 +19,59 @@ export interface IRenderItem {
col?: number;
proxyCol?: IProxyItem[];
headState?: HeadState;
// 新增支持图标和其他元数据
icon?: string;
provider?: string;
testUrl?: string;
}
// 优化列布局计算
const calculateColumns = (width: number, configCol: number): number => {
if (configCol > 0 && configCol < 6) return configCol;
if (width > 1920) return 5;
if (width > 1450) return 4;
if (width > 1024) return 3;
if (width > 900) return 2;
if (width >= 600) return 2;
return 1;
};
// 优化分组逻辑
const groupProxies = <T = any>(list: T[], size: number): T[][] => {
return list.reduce((acc, item) => {
const lastGroup = acc[acc.length - 1];
if (!lastGroup || lastGroup.length >= size) {
acc.push([item]);
} else {
lastGroup.push(item);
}
return acc;
}, [] as T[][]);
};
export const useRenderList = (mode: string) => {
const { data: proxiesData, mutate: mutateProxies } = useSWR(
"getProxies",
getProxies,
{ refreshInterval: 45000 }
{
refreshInterval: 2000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
},
);
const { verge } = useVerge();
const { width } = useWindowWidth();
let col = Math.floor(verge?.proxy_layout_column || 6);
// 自适应
if (col >= 6 || col <= 0) {
if (width > 1450) col = 4;
else if (width > 1024) col = 3;
else if (width > 900) col = 2;
else if (width >= 600) col = 2;
else col = 1;
}
const [headStates, setHeadState] = useHeadStateNew();
// make sure that fetch the proxies successfully
// 计算列数
const col = useMemo(
() => calculateColumns(width, verge?.proxy_layout_column || 6),
[width, verge?.proxy_layout_column],
);
// 确保代理数据加载
useEffect(() => {
if (!proxiesData) return;
const { groups, proxies } = proxiesData;
@@ -55,22 +82,29 @@ export const useRenderList = (mode: string) => {
) {
setTimeout(() => mutateProxies(), 500);
}
}, [proxiesData, mode]);
}, [proxiesData, mode, mutateProxies]);
// 处理渲染列表
const renderList: IRenderItem[] = useMemo(() => {
if (!proxiesData) return [];
// global 和 direct 使用展开的样式
const useRule = mode === "rule" || mode === "script";
const renderGroups =
(useRule && proxiesData.groups.length
useRule && proxiesData.groups.length
? proxiesData.groups
: [proxiesData.global!]) || [];
: [proxiesData.global!];
const retList = renderGroups.flatMap((group) => {
const headState = headStates[group.name] || DEFAULT_STATE;
const ret: IRenderItem[] = [
{ type: 0, key: group.name, group, headState },
{
type: 0,
key: group.name,
group,
headState,
icon: group.icon,
testUrl: group.testUrl,
},
];
if (headState?.open || !useRule) {
@@ -78,64 +112,59 @@ export const useRenderList = (mode: string) => {
group.all,
group.name,
headState.filterText,
headState.sortType
headState.sortType,
);
ret.push({ type: 1, key: `head-${group.name}`, group, headState });
ret.push({
type: 1,
key: `head-${group.name}`,
group,
headState,
});
if (!proxies.length) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState });
}
// 支持多列布局
if (col > 1) {
ret.push({
type: 3,
key: `empty-${group.name}`,
group,
headState,
});
} else if (col > 1) {
return ret.concat(
groupList(proxies, col).map((proxyCol) => ({
groupProxies(proxies, col).map((proxyCol) => ({
type: 4,
key: `col-${group.name}-${proxyCol[0].name}`,
group,
headState,
col,
proxyCol,
}))
provider: proxyCol[0].provider,
})),
);
} else {
return ret.concat(
proxies.map((proxy) => ({
type: 2,
key: `${group.name}-${proxy!.name}`,
group,
proxy,
headState,
provider: proxy.provider,
})),
);
}
return ret.concat(
proxies.map((proxy) => ({
type: 2,
key: `${group.name}-${proxy!.name}`,
group,
proxy,
headState,
}))
);
}
return ret;
});
if (!useRule) return retList.slice(1);
return retList.filter((item) => item.group.hidden === false);
return retList.filter((item) => !item.group.hidden);
}, [headStates, proxiesData, mode, col]);
return {
renderList,
onProxies: mutateProxies,
onHeadState: setHeadState,
currentColumns: col,
};
};
function groupList<T = any>(list: T[], size: number): T[][] {
return list.reduce((p, n) => {
if (!p.length) return [[n]];
const i = p.length - 1;
if (p[i].length < size) {
p[i].push(n);
return p;
}
p.push([n]);
return p;
}, [] as T[][]);
}
+1 -1
View File
@@ -32,7 +32,7 @@ export const ProviderButton = () => {
const hasProvider = Object.keys(data || {}).length > 0;
const [updating, setUpdating] = useState(
Object.keys(data || {}).map(() => false)
Object.keys(data || {}).map(() => false),
);
const setUpdatingAt = (status: boolean, index: number) => {
@@ -38,7 +38,7 @@ export interface BackupTableViewerProps {
page: number;
onPageChange: (
event: React.MouseEvent<HTMLButtonElement> | null,
page: number
page: number,
) => void;
total: number;
onRefresh: () => Promise<void>;
@@ -109,7 +109,7 @@ export const BackupTableViewer = memo(
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await window.confirm(
t("Confirm to delete this backup file?")
t("Confirm to delete this backup file?"),
);
if (confirmed) {
await handleDelete(file.filename);
@@ -132,7 +132,7 @@ export const BackupTableViewer = memo(
onClick={async (e: React.MouseEvent) => {
e.preventDefault();
const confirmed = await window.confirm(
t("Confirm to restore this backup file?")
t("Confirm to restore this backup file?"),
);
if (confirmed) {
await handleRestore(file.filename);
@@ -181,7 +181,7 @@ export const BackupTableViewer = memo(
/>
</TableContainer>
);
}
},
);
function LinuxIcon(props: SVGProps<SVGSVGElement>) {
@@ -48,7 +48,7 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
(_: React.MouseEvent<HTMLButtonElement> | null, page: number) => {
setPage(page);
},
[]
[],
);
const fetchAndSetBackupFiles = async () => {
@@ -95,8 +95,8 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
setDataSource(
backupFiles.slice(
page * DEFAULT_ROWS_PER_PAGE,
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE
)
page * DEFAULT_ROWS_PER_PAGE + DEFAULT_ROWS_PER_PAGE,
),
);
}, [page, backupFiles]);
@@ -104,14 +104,14 @@ export const BackupViewer = forwardRef<DialogRef>((props, ref) => {
<BaseDialog
open={open}
title={t("Backup Setting")}
contentSx={{ width: 600, maxHeight: 800 }}
// contentSx={{ width: 600, maxHeight: 800 }}
okBtn={t("")}
cancelBtn={t("Close")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
disableOk
>
<Box sx={{ maxWidth: 800 }}>
<Box>
<BaseLoadingOverlay isLoading={isLoading} />
<Paper elevation={2} sx={{ padding: 2 }}>
<BackupConfigViewer
@@ -51,7 +51,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
mutate("getClashConfig");
mutate("getVersion");
}, 100);
Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000);
// Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000);
} catch (err: any) {
Notice.error(err?.message || err.toString());
}
+18 -3
View File
@@ -1,7 +1,7 @@
import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks";
import { styled, Typography } from "@mui/material";
import { styled, Typography, Switch } from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { HotkeyInput } from "./hotkey-input";
@@ -29,6 +29,9 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, patchVerge } = useVerge();
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
const [enableGlobalHotkey, setEnableHotkey] = useState(
verge?.enable_global_hotkey ?? true,
);
useImperativeHandle(ref, () => ({
open: () => {
@@ -69,7 +72,10 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
.filter(Boolean);
try {
await patchVerge({ hotkeys });
await patchVerge({
hotkeys,
enable_global_hotkey: enableGlobalHotkey,
});
setOpen(false);
} catch (err: any) {
Notice.error(err.message || err.toString());
@@ -80,13 +86,22 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
<BaseDialog
open={open}
title={t("Hotkey Setting")}
contentSx={{ width: 450, maxHeight: 330 }}
contentSx={{ width: 450, maxHeight: 380 }}
okBtn={t("Save")}
cancelBtn={t("Cancel")}
onClose={() => setOpen(false)}
onCancel={() => setOpen(false)}
onOk={onSave}
>
<ItemWrapper style={{ marginBottom: 16 }}>
<Typography>{t("Enable Global Hotkey")}</Typography>
<Switch
edge="end"
checked={enableGlobalHotkey}
onChange={(e) => setEnableHotkey(e.target.checked)}
/>
</ItemWrapper>
{HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func}>
<Typography>{t(func)}</Typography>
@@ -8,6 +8,7 @@ import {
styled,
ListItem,
ListItemText,
Box,
} from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
@@ -163,6 +164,21 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</GuardState>
</Item>
)}
{OS === "macos" && (
<Item>
<ListItemText primary={t("Enable Tray Speed")} />
<GuardState
value={verge?.enable_tray_speed ?? true}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_tray_speed: e })}
onGuard={(e) => patchVerge({ enable_tray_speed: e })}
>
<Switch edge="end" />
</GuardState>
</Item>
)}
<Item>
<ListItemText primary={t("Common Tray Icon")} />
+12 -3
View File
@@ -103,7 +103,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Auto Close Connections")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Auto Close Connections Info")} />
<TooltipIcon
title={t("Auto Close Connections Info")}
sx={{ opacity: "0.7" }}
/>
<Switch
edge="end"
checked={values.autoCloseConnection}
@@ -130,7 +133,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Enable Builtin Enhanced")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Enable Builtin Enhanced Info")} />
<TooltipIcon
title={t("Enable Builtin Enhanced Info")}
sx={{ opacity: "0.7" }}
/>
<Switch
edge="end"
checked={values.enableBuiltinEnhanced}
@@ -196,7 +202,10 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Default Latency Test")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Default Latency Test Info")} />
<TooltipIcon
title={t("Default Latency Test Info")}
sx={{ opacity: "0.7" }}
/>
<TextField
autoComplete="new-password"
size="small"
+40 -24
View File
@@ -1,7 +1,13 @@
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { BaseFieldset } from "@/components/base/base-fieldset";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { useVerge } from "@/hooks/use-verge";
import { getAutotemProxy, getSystemProxy } from "@/services/cmds";
import getSystem from "@/utils/get-system";
import { EditRounded } from "@mui/icons-material";
import {
Button,
InputAdornment,
List,
ListItem,
@@ -9,16 +15,10 @@ import {
styled,
TextField,
Typography,
Button,
} from "@mui/material";
import { useVerge } from "@/hooks/use-verge";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { EditRounded } from "@mui/icons-material";
import { EditorViewer } from "@/components/profile/editor-viewer";
import { BaseFieldset } from "@/components/base/base-fieldset";
import getSystem from "@/utils/get-system";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { useLockFn } from "ahooks";
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;";
}`;
@@ -90,6 +90,16 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
pac_content: pac_file_content ?? DEFAULT_PAC,
});
const defaultBypass = () => {
if (isWindows) {
return "localhost;127.*;192.168.*;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.*;<local>";
}
if (getSystem() === "linux") {
return "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,::1";
}
return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,<local>";
};
useImperativeHandle(ref, () => ({
open: () => {
setOpen(true);
@@ -112,6 +122,10 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
Notice.error(t("Proxy Daemon Duration Cannot be Less than 1 Second"));
return;
}
if (value.bypass && !validReg.test(value.bypass)) {
Notice.error(t("Invalid Bypass Format"));
return;
}
const patch: Partial<IVergeConfig> = {};
@@ -124,6 +138,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (value.bypass !== system_proxy_bypass) {
patch.system_proxy_bypass = value.bypass;
}
if (value.pac !== proxy_auto_config) {
patch.proxy_auto_config = value.pac;
}
@@ -133,10 +148,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (value.pac_content !== pac_file_content) {
patch.pac_file_content = value.pac_content;
}
if (value.bypass && !validReg.test(value.bypass)) {
Notice.error(t("Invalid Bypass Format"));
return;
}
try {
await patchVerge(patch);
setOpen(false);
@@ -166,8 +178,8 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
? t("Enabled")
: t("Disabled")
: sysproxy?.enable
? t("Enabled")
: t("Disabled")}
? t("Enabled")
: t("Disabled")}
</Typography>
</FlexBox>
{!value.pac && (
@@ -202,7 +214,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
primary={t("Proxy Guard")}
sx={{ maxWidth: "fit-content" }}
/>
<TooltipIcon title={t("Proxy Guard Info")} />
<TooltipIcon title={t("Proxy Guard Info")} sx={{ opacity: "0.7" }} />
<Switch
edge="end"
disabled={!enabled}
@@ -215,7 +227,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} />
<TextField
autoComplete="new-password"
disabled={!enabled}
size="small"
value={value.duration}
@@ -242,11 +253,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/>
</ListItem>
)}
{!value.pac && (
{!value.pac && !value.use_default && (
<>
<ListItemText primary={t("Proxy Bypass")} />
<TextField
autoComplete="new-password"
error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled}
size="small"
@@ -258,20 +269,25 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setValue((v) => ({ ...v, bypass: e.target.value }));
}}
/>
</>
)}
{!value.pac && value.use_default && (
<>
<ListItemText primary={t("Bypass")} />
<FlexBox>
<TextField
autoComplete="new-password"
disabled={true}
size="small"
multiline
rows={4}
sx={{ width: "100%" }}
value={sysproxy?.bypass || "-"}
value={defaultBypass()}
/>
</FlexBox>
</>
)}
{value.pac && (
<>
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
+3 -3
View File
@@ -22,7 +22,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
const [open, setOpen] = useState(false);
const [values, setValues] = useState({
stack: "gvisor",
stack: "mixed",
device: "Mihomo",
autoRoute: true,
autoDetectInterface: true,
@@ -64,7 +64,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
...(old! || {}),
tun,
}),
false
false,
);
try {
await enhanceProfiles();
@@ -112,7 +112,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
...(old! || {}),
tun,
}),
false
false,
);
}}
>
@@ -76,10 +76,10 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
setDownloaded((a) => {
return a + e.payload.chunkLength;
});
}
},
);
try {
await updateInfo.install();
await updateInfo.downloadAndInstall();
await relaunch();
} catch (err: any) {
Notice.error(err?.message || err.toString());
@@ -100,7 +100,7 @@ export const UpdateViewer = forwardRef<DialogRef>((props, ref) => {
size="small"
onClick={() => {
openUrl(
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`
`https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`,
);
}}
>
+8 -3
View File
@@ -139,7 +139,7 @@ const SettingClash = ({ onError }: Props) => {
>
<GuardState
// clash premium 2022.08.26 值为warn
value={logLevel === "warn" ? "warning" : logLevel ?? "info"}
value={logLevel === "warn" ? "warning" : (logLevel ?? "info")}
onCatch={onError}
onFormat={(e: any) => e.target.value}
onChange={(e) => onChangeData({ "log-level": e })}
@@ -165,7 +165,7 @@ const SettingClash = ({ onError }: Props) => {
onClick={() => {
Notice.success(
t("Restart Application to Apply Modifications"),
1000
1000,
);
onChangeVerge({ enable_random_port: !enable_random_port });
patchVerge({ enable_random_port: !enable_random_port });
@@ -209,7 +209,12 @@ const SettingClash = ({ onError }: Props) => {
<SettingItem
onClick={invoke_uwp_tool}
label={t("Open UWP tool")}
extra={<TooltipIcon title={t("Open UWP tool Info")} />}
extra={
<TooltipIcon
title={t("Open UWP tool Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
)}
+31 -3
View File
@@ -1,7 +1,11 @@
import useSWR from "swr";
import useSWR, { mutate } from "swr";
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { SettingsRounded } from "@mui/icons-material";
import {
SettingsRounded,
PlayArrowRounded,
PauseRounded,
} from "@mui/icons-material";
import { useVerge } from "@/hooks/use-verge";
import { DialogRef, Notice, Switch } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
@@ -9,6 +13,7 @@ import { GuardState } from "./mods/guard-state";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
interface Props {
onError?: (err: Error) => void;
@@ -19,6 +24,9 @@ const SettingSystem = ({ onError }: Props) => {
const { verge, mutateVerge, patchVerge } = useVerge();
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null);
@@ -27,13 +35,25 @@ const SettingSystem = ({ onError }: Props) => {
enable_auto_launch,
enable_silent_start,
enable_system_proxy,
proxy_auto_config,
} = verge ?? {};
const isProxyEnabled = proxy_auto_config
? autoproxy?.enable
: sysproxy?.enable;
const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false);
};
const updateProxyStatus = async () => {
// 等待一小段时间让系统代理状态变化
await new Promise((resolve) => setTimeout(resolve, 100));
await mutate("getSystemProxy");
await mutate("getAutotemProxy");
};
return (
<SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} />
@@ -73,6 +93,11 @@ const SettingSystem = ({ onError }: Props) => {
icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()}
/>
{isProxyEnabled ? (
<PlayArrowRounded sx={{ color: "success.main", mr: 1 }} />
) : (
<PauseRounded sx={{ color: "error.main", mr: 1 }} />
)}
</>
}
>
@@ -82,7 +107,10 @@ const SettingSystem = ({ onError }: Props) => {
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_system_proxy: e })}
onGuard={(e) => patchVerge({ enable_system_proxy: e })}
onGuard={async (e) => {
await patchVerge({ enable_system_proxy: e });
await updateProxyStatus();
}}
>
<Switch edge="end" />
</GuardState>
@@ -0,0 +1,121 @@
import { useRef } from "react";
import { useTranslation } from "react-i18next";
import { Typography } from "@mui/material";
import {
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
} from "@/services/cmds";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
import { DialogRef, Notice } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { ConfigViewer } from "./mods/config-viewer";
import { HotkeyViewer } from "./mods/hotkey-viewer";
import { MiscViewer } from "./mods/misc-viewer";
import { ThemeViewer } from "./mods/theme-viewer";
import { LayoutViewer } from "./mods/layout-viewer";
import { UpdateViewer } from "./mods/update-viewer";
import { BackupViewer } from "./mods/backup-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
interface Props {
onError?: (err: Error) => void;
}
const SettingVergeAdvanced = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
const configRef = useRef<DialogRef>(null);
const hotkeyRef = useRef<DialogRef>(null);
const miscRef = useRef<DialogRef>(null);
const themeRef = useRef<DialogRef>(null);
const layoutRef = useRef<DialogRef>(null);
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const onCheckUpdate = async () => {
try {
const info = await checkUpdate();
if (!info?.available) {
Notice.success(t("Currently on the Latest Version"));
} else {
updateRef.current?.open();
}
} catch (err: any) {
Notice.error(err.message || err.toString());
}
};
return (
<SettingList title={t("Verge Advanced Setting")}>
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} />
<MiscViewer ref={miscRef} />
<LayoutViewer ref={layoutRef} />
<UpdateViewer ref={updateRef} />
<BackupViewer ref={backupRef} />
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
extra={
<TooltipIcon
title={t("Backup Setting Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
/>
<SettingItem
onClick={openAppDir}
label={t("Open Conf Dir")}
extra={
<TooltipIcon
title={t("Open Conf Dir Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
<SettingItem
label={t("Lite Mode")}
extra={
<TooltipIcon title={t("Lite Mode Info")} sx={{ opacity: "0.7" }} />
}
onClick={() => patchVerge({ enable_lite_mode: true })}
/>
<SettingItem
onClick={() => {
exitApp();
}}
label={t("Exit")}
/>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
</SettingItem>
</SettingList>
);
};
export default SettingVergeAdvanced;
@@ -1,18 +1,9 @@
import { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/plugin-dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material";
import {
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
copyClashEnv,
} from "@/services/cmds";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { Button, MenuItem, Select, Input } from "@mui/material";
import { copyClashEnv } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
import { DialogRef, Notice } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { ThemeModeSwitch } from "./mods/theme-mode-switch";
@@ -28,6 +19,7 @@ import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { ContentCopyRounded } from "@mui/icons-material";
import { languages } from "@/services/i18n";
interface Props {
onError?: (err: Error) => void;
@@ -35,7 +27,20 @@ interface Props {
const OS = getSystem();
const SettingVerge = ({ onError }: Props) => {
const languageOptions = Object.entries(languages).map(([code, _]) => {
const labels: { [key: string]: string } = {
en: "English",
ru: "Русский",
zh: "中文",
fa: "فارسی",
tt: "Татар",
id: "Bahasa Indonesia",
ar: "العربية",
};
return { code, label: labels[code] };
});
const SettingVergeBasic = ({ onError }: Props) => {
const { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge();
@@ -55,30 +60,17 @@ const SettingVerge = ({ onError }: Props) => {
const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null);
const onChangeData = (patch: Partial<IVergeConfig>) => {
const onChangeData = (patch: any) => {
mutateVerge({ ...verge, ...patch }, false);
};
const onCheckUpdate = async () => {
try {
const info = await checkUpdate();
if (!info?.available) {
Notice.success(t("Currently on the Latest Version"));
} else {
updateRef.current?.open();
}
} catch (err: any) {
Notice.error(err.message || err.toString());
}
};
const onCopyClashEnv = useCallback(async () => {
await copyClashEnv();
Notice.success(t("Copy Success"), 1000);
}, []);
return (
<SettingList title={t("Verge Setting")}>
<SettingList title={t("Verge Basic Setting")}>
<ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} />
@@ -96,10 +88,11 @@ const SettingVerge = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ language: e })}
>
<Select size="small" sx={{ width: 110, "> div": { py: "7.5px" } }}>
<MenuItem value="zh">中文</MenuItem>
<MenuItem value="en">English</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
<MenuItem value="fa">فارسی</MenuItem>
{languageOptions.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</GuardState>
</SettingItem>
@@ -150,6 +143,7 @@ const SettingVerge = ({ onError }: Props) => {
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
<MenuItem value="bash">Bash</MenuItem>
<MenuItem value="cmd">CMD</MenuItem>
<MenuItem value="nushell">Nushell</MenuItem>
<MenuItem value="powershell">PowerShell</MenuItem>
</Select>
</GuardState>
@@ -165,7 +159,11 @@ const SettingVerge = ({ onError }: Props) => {
>
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; path: string }) => {
return <MenuItem value={page.path}>{t(page.label)}</MenuItem>;
return (
<MenuItem key={page.path} value={page.path}>
{t(page.label)}
</MenuItem>
);
})}
</Select>
</GuardState>
@@ -240,45 +238,8 @@ const SettingVerge = ({ onError }: Props) => {
onClick={() => hotkeyRef.current?.open()}
label={t("Hotkey Setting")}
/>
<SettingItem
onClick={() => backupRef.current?.open()}
label={t("Backup Setting")}
extra={
<TooltipIcon
title={t("Backup Setting Info")}
sx={{ opacity: "0.7" }}
/>
}
/>
<SettingItem
onClick={() => configRef.current?.open()}
label={t("Runtime Config")}
/>
<SettingItem onClick={openAppDir} label={t("Open Conf Dir")} />
<SettingItem onClick={openCoreDir} label={t("Open Core Dir")} />
<SettingItem onClick={openLogsDir} label={t("Open Logs Dir")} />
<SettingItem onClick={onCheckUpdate} label={t("Check for Updates")} />
<SettingItem onClick={openDevTools} label={t("Open Dev Tools")} />
<SettingItem
onClick={() => {
exitApp();
}}
label={t("Exit")}
/>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
</SettingItem>
</SettingList>
);
};
export default SettingVerge;
export default SettingVergeBasic;
+7 -2
View File
@@ -64,13 +64,18 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
try {
if (!form.name) throw new Error("`Name` should not be null");
if (!form.url) throw new Error("`Url` should not be null");
let newList;
let uid;
if (form.icon && form.icon.startsWith("<svg")) {
// 移除 icon 中的注释
if (form.icon) {
form.icon = form.icon.replace(/<!--[\s\S]*?-->/g, "");
}
const doc = new DOMParser().parseFromString(
form.icon,
"image/svg+xml"
"image/svg+xml",
);
if (doc.querySelector("parsererror")) {
throw new Error("`Icon`svg format error");
@@ -97,7 +102,7 @@ export const TestViewer = forwardRef<TestViewerRef, Props>((props, ref) => {
Notice.error(err.message || err.toString());
setLoading(false);
}
})
}),
);
const handleClose = () => {
+6 -6
View File
@@ -10,12 +10,12 @@ import {
export const useClash = () => {
const { data: clash, mutate: mutateClash } = useSWR(
"getRuntimeConfig",
getRuntimeConfig
getRuntimeConfig,
);
const { data: versionData, mutate: mutateVersion } = useSWR(
"getVersion",
getVersion
getVersion,
);
const patchClash = useLockFn(async (patch: Partial<IConfigData>) => {
@@ -26,8 +26,8 @@ export const useClash = () => {
const version = versionData?.premium
? `${versionData.version} Premium`
: versionData?.meta
? `${versionData.version} Mihomo`
: versionData?.version || "-";
? `${versionData.version} Mihomo`
: versionData?.version || "-";
return {
clash,
@@ -41,7 +41,7 @@ export const useClash = () => {
export const useClashInfo = () => {
const { data: clashInfo, mutate: mutateInfo } = useSWR(
"getClashInfo",
getClashInfo
getClashInfo,
);
const patchInfo = async (
@@ -56,7 +56,7 @@ export const useClashInfo = () => {
| "external-controller"
| "secret"
>
>
>,
) => {
const hasInfo =
patch["redir-port"] != null ||
+1 -1
View File
@@ -7,7 +7,7 @@ export const useListen = () => {
const addListener = async <T>(
eventName: string,
handler: EventCallback<T>
handler: EventCallback<T>,
) => {
const unlisten = await listen(eventName, handler);
unlistenFns.current.push(unlisten);
+23 -27
View File
@@ -4,6 +4,7 @@ import { createSockette } from "../utils/websocket";
import { useClashInfo } from "./use-clash";
import dayjs from "dayjs";
import { create } from "zustand";
import { useVisibility } from "./use-visibility";
const MAX_LOG_NUM = 1000;
@@ -21,7 +22,7 @@ const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
const params = new URLSearchParams();
if (secret) {
params.append("token", encodeURIComponent(secret));
params.append("token", secret);
}
if (logLevel === "all") {
params.append("level", "debug");
@@ -33,34 +34,25 @@ const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
};
interface LogStore {
logs: Record<LogLevel, ILogItem[]>;
clearLogs: (level?: LogLevel) => void;
appendLog: (level: LogLevel, log: ILogItem) => void;
logs: ILogItem[];
clearLogs: () => void;
appendLog: (log: ILogItem) => void;
}
const useLogStore = create<LogStore>(
(set: (fn: (state: LogStore) => Partial<LogStore>) => void) => ({
logs: {
warning: [],
info: [],
debug: [],
error: [],
all: [],
},
clearLogs: (level?: LogLevel) =>
set((state: LogStore) => ({
logs: level
? { ...state.logs, [level]: [] }
: { warning: [], info: [], debug: [], error: [], all: [] },
logs: [],
clearLogs: () =>
set(() => ({
logs: [],
})),
appendLog: (level: LogLevel, log: ILogItem) =>
appendLog: (log: ILogItem) =>
set((state: LogStore) => {
const currentLogs = state.logs[level];
const newLogs =
currentLogs.length >= MAX_LOG_NUM
? [...currentLogs.slice(1), log]
: [...currentLogs, log];
return { logs: { ...state.logs, [level]: newLogs } };
state.logs.length >= MAX_LOG_NUM
? [...state.logs.slice(1), log]
: [...state.logs, log];
return { logs: newLogs };
}),
}),
);
@@ -69,9 +61,10 @@ export const useLogData = (logLevel: LogLevel) => {
const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog();
const { logs, appendLog } = useLogStore();
const pageVisible = useVisibility();
useEffect(() => {
if (!enableLog || !clashInfo) return;
if (!enableLog || !clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo;
const wsUrl = buildWSUrl(server, secret, logLevel);
@@ -82,7 +75,7 @@ export const useLogData = (logLevel: LogLevel) => {
if (!isActive) return;
const data = JSON.parse(event.data) as ILogItem;
const time = dayjs().format("MM-DD HH:mm:ss");
appendLog(logLevel, { ...data, time });
appendLog({ ...data, time });
},
onerror() {
if (!isActive) return;
@@ -96,10 +89,13 @@ export const useLogData = (logLevel: LogLevel) => {
};
}, [clashInfo, enableLog, logLevel]);
return logs[logLevel];
// 根据当前选择的日志等级过滤日志
return logLevel === "all"
? logs
: logs.filter((log) => log.type.toLowerCase() === logLevel);
};
// 导出清空日志的方法
export const clearLogs = (level?: LogLevel) => {
useLogStore.getState().clearLogs(level);
export const clearLogs = () => {
useLogStore.getState().clearLogs();
};
+453
View File
@@ -0,0 +1,453 @@
{
"millis": "ميلي ثانية",
"seconds": "ثواني",
"mins": "دقائق",
"Back": "رجوع",
"Close": "إغلاق",
"Cancel": "إلغاء",
"Confirm": "تأكيد",
"Maximize": "تكبير",
"Minimize": "تصغير",
"Format document": "تنسيق المستند",
"Empty": "فارغ",
"New": "جديد",
"Edit": "تعديل",
"Save": "حفظ",
"Delete": "حذف",
"Enable": "تمكين",
"Disable": "تعطيل",
"Label-Proxies": "الوكلاء",
"Label-Profiles": "الملفات الشخصية",
"Label-Connections": "الاتصالات",
"Label-Rules": "القواعد",
"Label-Logs": "السجلات",
"Label-Test": "اختبار",
"Label-Settings": "الإعدادات",
"Proxies": "الوكلاء",
"Proxy Groups": "مجموعات الوكلاء",
"Proxy Provider": "مزود الوكيل",
"Update All": "تحديث الكل",
"Update At": "التحديث عند",
"rule": "قاعدة",
"global": "عالمي",
"direct": "مباشر",
"script": "سكريبت",
"Location": "الموقع",
"Delay check": "فحص التأخير",
"Sort by default": "الترتيب الافتراضي",
"Sort by delay": "الترتيب حسب التأخير",
"Sort by name": "الترتيب حسب الاسم",
"Delay check URL": "رابط فحص التأخير",
"Delay check to cancel fixed": "فحص التأخير لإلغاء الثابت",
"Proxy basic": "إعدادات الوكيل الأساسية",
"Proxy detail": "تفاصيل الوكيل",
"Profiles": "الملفات الشخصية",
"Update All Profiles": "تحديث جميع الملفات الشخصية",
"View Runtime Config": "عرض تكوين وقت التشغيل",
"Reactivate Profiles": "إعادة تنشيط الملفات الشخصية",
"Paste": "لصق",
"Profile URL": "رابط الملف الشخصي",
"Import": "استيراد",
"From": "من",
"Update Time": "وقت التحديث",
"Used / Total": "المستخدم / الإجمالي",
"Expire Time": "وقت الانتهاء",
"Create Profile": "إنشاء ملف شخصي",
"Edit Profile": "تعديل الملف الشخصي",
"Edit Proxies": "تعديل الوكلاء",
"Use newlines for multiple uri": "استخدم أسطرًا جديدة لعدّة عناوين URI (يدعم التشفير Base64)",
"Edit Rules": "تعديل القواعد",
"Rule Type": "نوع القاعدة",
"Rule Content": "محتوى القاعدة",
"Proxy Policy": "سياسة الوكيل",
"No Resolve": "لا يوجد حل",
"Prepend Rule": "إضافة قاعدة في البداية",
"Append Rule": "إضافة قاعدة في النهاية",
"Prepend Group": "إضافة مجموعة في البداية",
"Append Group": "إضافة مجموعة في النهاية",
"Prepend Proxy": "إضافة وكيل في البداية",
"Append Proxy": "إضافة وكيل في النهاية",
"Rule Condition Required": "شرط القاعدة مطلوب",
"Invalid Rule": "قاعدة غير صالحة",
"Advanced": "متقدم",
"Visualization": "تصور",
"DOMAIN": "مطابقة اسم المجال الكامل",
"DOMAIN-SUFFIX": "مطابقة لاحقة المجال",
"DOMAIN-KEYWORD": "مطابقة كلمة مفتاحية في المجال",
"DOMAIN-REGEX": "مطابقة المجال باستخدام التعبيرات العادية",
"GEOSITE": "مطابقة المجالات ضمن Geosite",
"GEOIP": "مطابقة رمز البلد لعنوان IP",
"SRC-GEOIP": "مطابقة رمز البلد لعنوان IP المصدر",
"IP-ASN": "مطابقة ASN لعنوان IP",
"SRC-IP-ASN": "مطابقة ASN لعنوان IP المصدر",
"IP-CIDR": "مطابقة نطاق عنوان IP",
"IP-CIDR6": "مطابقة نطاق عناوين IPv6",
"SRC-IP-CIDR": "مطابقة نطاق عنوان IP المصدر",
"IP-SUFFIX": "مطابقة لاحقة عنوان IP",
"SRC-IP-SUFFIX": "مطابقة لاحقة عنوان IP المصدر",
"SRC-PORT": "مطابقة نطاق المنفذ المصدر",
"DST-PORT": "مطابقة نطاق المنفذ الوجهة",
"IN-PORT": "مطابقة المنفذ الوارد",
"DSCP": "علامة DSCP (لـ tproxy على UDP فقط)",
"PROCESS-NAME": "مطابقة اسم العملية (اسم حزمة Android)",
"PROCESS-PATH": "مطابقة المسار الكامل للعملية",
"PROCESS-NAME-REGEX": "مطابقة اسم العملية باستخدام التعبيرات العادية (اسم حزمة Android)",
"PROCESS-PATH-REGEX": "مطابقة المسار الكامل للعملية باستخدام التعبيرات العادية",
"NETWORK": "مطابقة بروتوكول النقل (TCP/UDP)",
"UID": "مطابقة معرف المستخدم في Linux",
"IN-TYPE": "مطابقة نوع الإدخال",
"IN-USER": "مطابقة اسم المستخدم للإدخال",
"IN-NAME": "مطابقة اسم الإدخال",
"SUB-RULE": "قاعدة فرعية",
"RULE-SET": "مطابقة مجموعة القواعد",
"AND": "منطقي AND",
"OR": "منطقي OR",
"NOT": "منطقي NOT",
"MATCH": "مطابقة جميع الطلبات",
"DIRECT": "البيانات تخرج مباشرة",
"REJECT": "رفض الطلبات",
"REJECT-DROP": "تجاهل الطلبات",
"PASS": "تخطي هذه القاعدة عند المطابقة",
"Edit Groups": "تعديل مجموعات الوكلاء",
"Group Type": "نوع المجموعة",
"select": "اختيار الوكيل يدويًا",
"url-test": "اختيار الوكيل بناءً على تأخير اختبار الرابط",
"fallback": "التبديل إلى وكيل آخر عند حدوث خطأ",
"load-balance": "توزيع التحميل بين الوكلاء",
"relay": "التمرير عبر سلسلة الوكلاء المحددة",
"Group Name": "اسم المجموعة",
"Use Proxies": "استخدام الوكلاء",
"Use Provider": "استخدام المزود",
"Health Check Url": "رابط فحص الصحة",
"Expected Status": "الحالة المتوقعة",
"Interval": "الفاصل الزمني",
"Lazy": "كسول",
"Timeout": "مهلة",
"Max Failed Times": "الحد الأقصى لمحاولات الفشل",
"Interface Name": "اسم الواجهة",
"Routing Mark": "علامة التوجيه",
"Include All": "تضمين جميع الوكلاء والمزودين",
"Include All Providers": "تضمين جميع المزودين",
"Include All Proxies": "تضمين جميع الوكلاء",
"Exclude Filter": "استبعاد المرشح",
"Exclude Type": "استبعاد النوع",
"Disable UDP": "تعطيل UDP",
"Hidden": "مخفي",
"Group Name Required": "اسم المجموعة مطلوب",
"Group Name Already Exists": "اسم المجموعة موجود بالفعل",
"Extend Config": "توسيع الإعدادات",
"Extend Script": "توسيع السكربت",
"Global Merge": "دمج عالمي للإعدادات",
"Global Script": "سكريبت عالمي",
"Type": "النوع",
"Name": "الاسم",
"Descriptions": "الوصف",
"Subscription URL": "رابط الاشتراك",
"Update Interval": "فاصل التحديث",
"Choose File": "اختر ملف",
"Use System Proxy": "استخدام وكيل النظام",
"Use Clash Proxy": "استخدام وكيل Clash",
"Accept Invalid Certs (Danger)": "قبول الشهادات غير الصالحة (خطر)",
"Refresh": "تحديث",
"Home": "الصفحة الرئيسية",
"Select": "اختيار",
"Edit Info": "تعديل المعلومات",
"Edit File": "تعديل الملف",
"Open File": "فتح الملف",
"Update": "تحديث",
"Update(Proxy)": "تحديث (الوكيل)",
"Confirm deletion": "تأكيد الحذف",
"This operation is not reversible": "لا يمكن التراجع عن هذه العملية",
"Script Console": "وحدة التحكم للسكريبت",
"To Top": "إلى الأعلى",
"To End": "إلى النهاية",
"Connections": "الاتصالات",
"Table View": "عرض الجدول",
"List View": "عرض القائمة",
"Close All": "إغلاق الكل",
"Default": "افتراضي",
"Download Speed": "سرعة التنزيل",
"Upload Speed": "سرعة الرفع",
"Host": "المضيف",
"Downloaded": "تم التنزيل",
"Uploaded": "تم الرفع",
"DL Speed": "سرعة التنزيل",
"UL Speed": "سرعة الرفع",
"Chains": "السلاسل",
"Rule": "قاعدة",
"Process": "عملية",
"Time": "الوقت",
"Source": "المصدر",
"Destination": "عنوان IP الوجهة",
"DestinationPort": "ميناء الوجهة",
"Close Connection": "إغلاق الاتصال",
"Rules": "القواعد",
"Rule Provider": "مزود القواعد",
"Logs": "السجلات",
"Pause": "إيقاف مؤقت",
"Clear": "مسح",
"Test": "اختبار",
"Test All": "اختبار الكل",
"Create Test": "إنشاء اختبار",
"Edit Test": "تعديل الاختبار",
"Icon": "أيقونة",
"Test URL": "رابط الاختبار",
"Settings": "الإعدادات",
"System Setting": "إعدادات النظام",
"Tun Mode": "وضع TUN",
"Reset to Default": "إعادة تعيين إلى الافتراضي",
"Tun Mode Info": "وضع TUN (بطاقة شبكة افتراضية): يلتقط كل حركة المرور في النظام. عند تمكينه، لا حاجة لتفعيل وكيل النظام.",
"Stack": "مكدس TUN",
"System and Mixed Can Only be Used in Service Mode": "لا يمكن استخدام النظام والمختلط إلا في وضع الخدمة",
"Device": "اسم الجهاز",
"Auto Route": "توجيه تلقائي",
"Strict Route": "توجيه صارم",
"Auto Detect Interface": "الكشف التلقائي عن الواجهة",
"DNS Hijack": "اختطاف DNS",
"MTU": "وحدة الإرسال القصوى",
"Service Mode": "وضع الخدمة",
"Service Mode Info": "يرجى تثبيت وضع الخدمة قبل تمكين وضع TUN. يمكن للعمليات الأساسية التي يبدأها وضع الخدمة الحصول على إذن لتثبيت بطاقة الشبكة الافتراضية (TUN).",
"Current State": "الحالة الحالية",
"pending": "معلق",
"installed": "مثبت",
"uninstall": "إلغاء التثبيت",
"active": "نشط",
"unknown": "غير معروف",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "معلومة: يرجى التأكد من تثبيت وتشغيل خدمة Clash Verge",
"Install": "تثبيت",
"Uninstall": "إلغاء التثبيت",
"Disable Service Mode": "تعطيل وضع الخدمة",
"System Proxy": "وكيل النظام",
"System Proxy Info": "عند التمكين، سيتم تعديل إعدادات الوكيل في نظام التشغيل. إذا فشل التمكين، فقم بتعديل إعدادات الوكيل في النظام يدويًا.",
"System Proxy Setting": "إعداد وكيل النظام",
"Current System Proxy": "الوكيل الحالي للنظام",
"Enable status": "حالة التمكين:",
"Enabled": "ممكّن",
"Disabled": "معطّل",
"Server Addr": "عنوان الخادم:",
"Not available": "غير متوفر",
"Proxy Guard": "حماية الوكيل",
"Proxy Guard Info": "عند التمكين، يمنع برامج أخرى من تعديل إعدادات وكيل النظام",
"Guard Duration": "مدة الحماية",
"Always use Default Bypass": "استخدام التخطي الافتراضي دائمًا",
"Proxy Bypass": "إعدادات تخطي الوكيل:",
"Bypass": "تخطي:",
"Use PAC Mode": "استخدام وضع PAC",
"PAC Script Content": "محتوى سكريبت PAC",
"PAC URL": "رابط PAC:",
"Auto Launch": "إطلاق تلقائي",
"Silent Start": "بدء صامت",
"Silent Start Info": "بدء البرنامج في الخلفية دون عرض الواجهة",
"TG Channel": "قناة تيليجرام",
"Manual": "دليل",
"Github Repo": "مستودع Github",
"Clash Setting": "إعدادات Clash",
"Allow Lan": "السماح بالشبكة المحلية",
"Network Interface": "واجهة الشبكة",
"Ip Address": "عنوان IP",
"Mac Address": "عنوان MAC",
"IPv6": "IPv6",
"Unified Delay": "تأخير موحد",
"Unified Delay Info": "عند تفعيل التأخير الموحد، سيتم إجراء اختبارين للتأخير لتقليل الفروقات الناتجة عن مفاوضات الاتصال",
"Log Level": "مستوى السجلات",
"Port Config": "تكوين المنافذ",
"Random Port": "منفذ عشوائي",
"Mixed Port": "منفذ مختلط",
"Socks Port": "منفذ SOCKS",
"Http Port": "منفذ HTTP(S)",
"Redir Port": "منفذ إعادة التوجيه",
"Tproxy Port": "منفذ Tproxy",
"External": "خارجي",
"External Controller": "وحدة التحكم الخارجية",
"Core Secret": "المفتاح السري للنواة",
"Recommended": "موصى به",
"Open URL": "فتح الرابط",
"Replace host, port, secret with %host, %port, %secret": "استبدل المضيف والمنفذ والمفتاح بـ %host و%port و%secret",
"Support %host, %port, %secret": "يدعم %host و%port و%secret",
"Clash Core": "نواة Clash",
"Upgrade": "ترقية",
"Restart": "إعادة التشغيل",
"Release Version": "إصدار مستقر",
"Alpha Version": "إصدار ألفا",
"Please Enable Service Mode": "يرجى تثبيت وتفعيل وضع الخدمة أولاً",
"Please enter your root password": "يرجى إدخال كلمة مرور الرووت",
"Grant": "منح الصلاحيات",
"Open UWP tool": "فتح أداة UWP",
"Open UWP tool Info": "منذ نظام ويندوز 8، يتم تقييد تطبيقات UWP من الوصول المباشر إلى المضيف المحلي. هذه الأداة تتيح تجاوز هذا التقييد",
"Update GeoData": "تحديث البيانات الجغرافية",
"Verge Setting": "إعدادات Verge",
"Language": "اللغة",
"Theme Mode": "وضع السمة",
"theme.light": "سمة فاتحة",
"theme.dark": "سمة داكنة",
"theme.system": "سمة النظام",
"Tray Click Event": "حدث النقر على الأيقونة في شريط المهام",
"Show Main Window": "إظهار النافذة الرئيسية",
"Copy Env Type": "نسخ نوع البيئة",
"Copy Success": "تم النسخ بنجاح",
"Start Page": "صفحة البدء",
"Startup Script": "سكريبت بدء التشغيل",
"Browse": "استعراض",
"Theme Setting": "إعدادات السمة",
"Primary Color": "اللون الأساسي",
"Secondary Color": "اللون الثانوي",
"Primary Text": "النص الأساسي",
"Secondary Text": "النص الثانوي",
"Info Color": "لون المعلومات",
"Warning Color": "لون التحذير",
"Error Color": "لون الخطأ",
"Success Color": "لون النجاح",
"Font Family": "عائلة الخط",
"CSS Injection": "حقن CSS",
"Layout Setting": "إعدادات التخطيط",
"Traffic Graph": "مخطط حركة المرور",
"Memory Usage": "استهلاك الذاكرة",
"Memory Cleanup": "انقر لتنظيف الذاكرة",
"Proxy Group Icon": "أيقونة مجموعة الوكلاء",
"Nav Icon": "أيقونة التنقل",
"Monochrome": "أحادي اللون",
"Colorful": "ملون",
"Tray Icon": "أيقونة شريط المهام",
"Common Tray Icon": "أيقونة شريط مهام عامة",
"System Proxy Tray Icon": "أيقونة شريط المهام لوكيل النظام",
"Tun Tray Icon": "أيقونة شريط المهام لـ TUN",
"Miscellaneous": "متفرقات",
"App Log Level": "مستوى سجلات التطبيق",
"Auto Close Connections": "إغلاق الاتصالات تلقائيًا",
"Auto Close Connections Info": "إنهاء الاتصالات القائمة عند تغيير اختيار مجموعة الوكيل أو وضع الوكيل",
"Auto Check Update": "فحص التحديث تلقائيًا",
"Enable Builtin Enhanced": "تفعيل التحسين المدمج",
"Enable Builtin Enhanced Info": "معالجة توافق ملف التكوين",
"Proxy Layout Columns": "أعمدة عرض الوكيل",
"Auto Columns": "أعمدة تلقائية",
"Auto Log Clean": "تنظيف السجلات تلقائيًا",
"Never Clean": "عدم التنظيف أبدًا",
"Retain _n Days": "الاحتفاظ لمدة {{n}} يومًا",
"Default Latency Test": "اختبار التأخير الافتراضي",
"Default Latency Test Info": "يُستخدم فقط لاختبار طلب HTTP العميل. لن يؤثر على ملف التكوين",
"Default Latency Timeout": "مهلة التأخير الافتراضية",
"Hotkey Setting": "إعدادات الاختصارات",
"Enable Global Hotkey": "تمكين مفتاح التشغيل السريع العالمي",
"open_or_close_dashboard": "فتح/إغلاق لوحة التحكم",
"clash_mode_rule": "وضع القواعد",
"clash_mode_global": "الوضع العالمي",
"clash_mode_direct": "الوضع المباشر",
"toggle_system_proxy": "تفعيل/تعطيل وكيل النظام",
"toggle_tun_mode": "تفعيل/تعطيل وضع TUN",
"Backup Setting": "إعداد النسخ الاحتياطي",
"Runtime Config": "تكوين وقت التشغيل",
"Open Conf Dir": "فتح مجلد التكوين",
"Open Conf Dir Info": "إذا عمل البرنامج بشكل غير طبيعي، قم بالنسخ الاحتياطي ثم حذف جميع الملفات في هذا المجلد ثم أعد تشغيل البرنامج",
"Open Core Dir": "فتح مجلد النواة",
"Open Logs Dir": "فتح مجلد السجلات",
"Check for Updates": "التحقق من وجود تحديثات",
"Go to Release Page": "الانتقال إلى صفحة الإصدارات",
"Portable Updater Error": "الإصدار المحمول لا يدعم التحديث داخل التطبيق. يرجى التنزيل والاستبدال يدويًا",
"Break Change Update Error": "هذا الإصدار هو تحديث رئيسي ولا يدعم التحديث داخل التطبيق. يرجى إلغاء التثبيت وتنزيل الإصدار الجديد وتثبيته يدويًا",
"Open Dev Tools": "أدوات المطور",
"Exit": "خروج",
"Verge Version": "إصدار Verge",
"ReadOnly": "للقراءة فقط",
"ReadOnlyMessage": "لا يمكن التعديل في محرر القراءة فقط",
"Filter": "تصفية",
"Filter conditions": "شروط التصفية",
"Match Case": "مطابقة الحالة",
"Match Whole Word": "مطابقة الكلمة بأكملها",
"Use Regular Expression": "استخدام التعبيرات العادية",
"Profile Imported Successfully": "تم استيراد الملف الشخصي بنجاح",
"Profile Switched": "تم التبديل إلى الملف الشخصي",
"Profile Reactivated": "تم إعادة تنشيط الملف الشخصي",
"Only YAML Files Supported": "لا يتم دعم سوى ملفات YAML",
"Settings Applied": "تم تطبيق الإعدادات",
"Service Installed Successfully": "تم تثبيت الخدمة بنجاح",
"Service Uninstalled Successfully": "تم إلغاء تثبيت الخدمة بنجاح",
"Proxy Daemon Duration Cannot be Less than 1 Second": "لا يمكن أن تقل مدة خادم الوكيل عن ثانية واحدة",
"Invalid Bypass Format": "تنسيق التخطي غير صالح",
"Clash Port Modified": "تم تعديل منفذ Clash",
"Port Conflict": "تعارض في المنفذ",
"Restart Application to Apply Modifications": "أعد تشغيل التطبيق لتطبيق التعديلات",
"External Controller Address Modified": "تم تعديل عنوان وحدة التحكم الخارجية",
"Permissions Granted Successfully for _clash Core": "تم منح الأذونات بنجاح لـ {{core}} Core",
"Core Version Updated": "تم تحديث إصدار النواة",
"Clash Core Restarted": "تم إعادة تشغيل نواة Clash",
"GeoData Updated": "تم تحديث البيانات الجغرافية",
"Currently on the Latest Version": "أنت على أحدث إصدار حاليًا",
"Import Subscription Successful": "تم استيراد الاشتراك بنجاح",
"WebDAV Server URL": "عنوان خادم WebDAV",
"Username": "اسم المستخدم",
"Password": "كلمة المرور",
"Backup": "نسخ احتياطي",
"Filename": "اسم الملف",
"Actions": "الإجراءات",
"Restore": "استعادة",
"No Backups": "لا توجد نسخ احتياطية متاحة",
"WebDAV URL Required": "لا يمكن ترك رابط WebDAV فارغًا",
"Invalid WebDAV URL": "تنسيق رابط WebDAV غير صالح",
"Username Required": "لا يمكن ترك اسم المستخدم فارغًا",
"Password Required": "لا يمكن ترك كلمة المرور فارغة",
"Failed to Fetch Backups": "فشل في جلب ملفات النسخ الاحتياطي",
"WebDAV Config Saved": "تم حفظ إعدادات WebDAV بنجاح",
"WebDAV Config Save Failed": "فشل حفظ إعدادات WebDAV: {{error}}",
"Backup Created": "تم إنشاء النسخة الاحتياطية بنجاح",
"Backup Failed": "فشل في النسخ الاحتياطي: {{error}}",
"Delete Backup": "حذف النسخة الاحتياطية",
"Restore Backup": "استعادة النسخة الاحتياطية",
"Backup Time": "وقت النسخ الاحتياطي",
"Confirm to delete this backup file?": "هل تريد بالتأكيد حذف ملف النسخة الاحتياطية هذا؟",
"Confirm to restore this backup file?": "هل تريد بالتأكيد استعادة ملف النسخة الاحتياطية هذا؟",
"Restore Success, App will restart in 1s": "تمت الاستعادة بنجاح، سيعاد تشغيل التطبيق خلال ثانية واحدة",
"Failed to fetch backup files": "فشل في جلب ملفات النسخ الاحتياطي",
"Profile": "الملف الشخصي",
"Help": "مساعدة",
"About": "حول",
"Theme": "السمة",
"TUN Mode": "وضع TUN",
"Main Window": "النافذة الرئيسية",
"Group Icon": "أيقونة المجموعة",
"Menu Icon": "أيقونة القائمة",
"System Proxy Bypass": "تخطي وكيل النظام",
"PAC File": "ملف PAC",
"Web UI": "واجهة الويب",
"Hotkeys": "اختصارات لوحة المفاتيح",
"Auto Close Connection": "إغلاق الاتصال تلقائيًا",
"Enable Built-in Enhanced": "تفعيل التحسين المدمج",
"Proxy Layout Column": "عمود عرض الوكيل",
"Test List": "قائمة الاختبارات",
"Enable Random Port": "تفعيل المنفذ العشوائي",
"Verge Mixed Port": "منفذ Verge المختلط",
"Verge Socks Port": "منفذ Verge SOCKS",
"Verge Redir Port": "منفذ إعادة التوجيه لـ Verge",
"Verge Tproxy Port": "منفذ Tproxy لـ Verge",
"Verge Port": "منفذ Verge",
"Verge HTTP Enabled": "تمكين Verge HTTP",
"WebDAV URL": "رابط WebDAV",
"WebDAV Username": "اسم المستخدم لـ WebDAV",
"WebDAV Password": "كلمة مرور WebDAV",
"Copy Env": "نسخ البيئة",
"Conf Dir": "مجلد الإعدادات",
"Core Dir": "مجلد النواة",
"Logs Dir": "مجلد السجلات",
"Open Dir": "فتح المجلد",
"Restart Clash Core": "إعادة تشغيل نواة Clash",
"Restart App": "إعادة تشغيل التطبيق",
"More": "المزيد",
"Dashboard": "لوحة التحكم",
"Rule Mode": "وضع القواعد",
"Global Mode": "الوضع العالمي",
"Direct Mode": "الوضع المباشر",
"Enable Tray Speed": "تفعيل سرعة التراي",
"Lite Mode": "وضع الأداء الخفيف",
"Lite Mode Info": "إيقاف الواجهة الرسومية والإبقاء على تشغيل النواة",
"Config Validation Failed": "فشل التحقق من تكوين الاشتراك، يرجى فحص ملف التكوين، تم التراجع عن التغييرات، تفاصيل الخطأ:",
"Boot Config Validation Failed": "فشل التحقق من التكوين عند الإقلاع، تم استخدام التكوين الافتراضي، يرجى فحص ملف التكوين، تفاصيل الخطأ:",
"Core Change Config Validation Failed": "فشل التحقق من التكوين عند تغيير النواة، تم استخدام التكوين الافتراضي، يرجى فحص ملف التكوين، تفاصيل الخطأ:",
"Config Validation Process Terminated": "تم إنهاء عملية التحقق",
"Script Syntax Error": "خطأ في بناء جملة السكريبت، تم التراجع عن التغييرات",
"Script Missing Main": "خطأ في السكريبت، تم التراجع عن التغييرات",
"File Not Found": "الملف غير موجود، تم التراجع عن التغييرات",
"Script File Error": "خطأ في ملف السكريبت، تم التراجع عن التغييرات",
"Core Changed Successfully": "تم تغيير النواة بنجاح",
"Failed to Change Core": "فشل تغيير النواة",
"Verge Basic Setting": "الإعدادات الأساسية Verge",
"Verge Advanced Setting": "الإعدادات الأساسية Verge"
}
+74 -4
View File
@@ -178,7 +178,8 @@
"Process": "Process",
"Time": "Time",
"Source": "Source",
"Destination IP": "Destination IP",
"Destination": "Destination",
"DestinationPort": "Destination Port",
"Close Connection": "Close Connection",
"Rules": "Rules",
"Rule Provider": "Rule Provider",
@@ -229,6 +230,7 @@
"Proxy Guard Info": "Enable to prevent other software from modifying the operating system's proxy settings",
"Guard Duration": "Guard Duration",
"Always use Default Bypass": "Always use Default Bypass",
"Use Bypass Check": "Use Bypass Check",
"Proxy Bypass": "Proxy Bypass Settings: ",
"Bypass": "Bypass: ",
"Use PAC Mode": "Use PAC Mode",
@@ -249,6 +251,7 @@
"Unified Delay": "Unified Delay",
"Unified Delay Info": "When unified delay is turned on, two delay tests will be performed to eliminate the delay differences between different types of nodes caused by connection handshakes, etc",
"Log Level": "Log Level",
"Log Level Info": "This parameter is valid only for kernel log files in the log directory Service folder",
"Port Config": "Port Config",
"Random Port": "Random Port",
"Mixed Port": "Mixed Port",
@@ -274,7 +277,8 @@
"Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",
"Update GeoData": "Update GeoData",
"Verge Setting": "Verge Setting",
"Verge Basic Setting": "Verge Basic Setting",
"Verge Advanced Setting": "Verge Advanced Setting",
"Language": "Language",
"Theme Mode": "Theme Mode",
"theme.light": "Light",
@@ -326,6 +330,7 @@
"Default Latency Test Info": "Used for HTTP client request testing only and won't make a difference to the configuration file",
"Default Latency Timeout": "Default Latency Timeout",
"Hotkey Setting": "Hotkey Setting",
"Enable Global Hotkey": "Enable Global Hotkey",
"open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Rule Mode",
"clash_mode_global": "Global Mode",
@@ -333,8 +338,10 @@
"toggle_system_proxy": "Enable/Disable System Proxy",
"toggle_tun_mode": "Enable/Disable Tun Mode",
"Backup Setting": "Backup Setting",
"Backup Setting Info": "Support WebDAV backup configuration files",
"Runtime Config": "Runtime Config",
"Open Conf Dir": "Open Conf Dir",
"Open Conf Dir Info": "If the software runs abnormally, BACKUP and delete all files in this folder than restart the software",
"Open Core Dir": "Open Core Dir",
"Open Logs Dir": "Open Logs Dir",
"Check for Updates": "Check for Updates",
@@ -352,7 +359,6 @@
"Match Whole Word": "Match Whole Word",
"Use Regular Expression": "Use Regular Expression",
"Profile Imported Successfully": "Profile Imported Successfully",
"Clash Config Updated": "Clash Config Updated",
"Profile Switched": "Profile Switched",
"Profile Reactivated": "Profile Reactivated",
"Only YAML Files Supported": "Only YAML Files Supported",
@@ -395,5 +401,69 @@
"Confirm to delete this backup file?": "Confirm to delete this backup file?",
"Confirm to restore this backup file?": "Confirm to restore this backup file?",
"Restore Success, App will restart in 1s": "Restore Success, App will restart in 1s",
"Failed to fetch backup files": "Failed to fetch backup files"
"Failed to fetch backup files": "Failed to fetch backup files",
"Profile": "Profile",
"Help": "Help",
"About": "About",
"Theme": "Theme",
"TUN Mode": "TUN Mode",
"Main Window": "Main Window",
"Group Icon": "Group Icon",
"Menu Icon": "Menu Icon",
"System Proxy Bypass": "System Proxy Bypass",
"PAC File": "PAC File",
"Web UI": "Web UI",
"Hotkeys": "Hotkeys",
"Auto Close Connection": "Auto Close Connection",
"Enable Built-in Enhanced": "Enable Built-in Enhanced",
"Proxy Layout Column": "Proxy Layout Column",
"Test List": "Test List",
"Enable Random Port": "Enable Random Port",
"Verge Mixed Port": "Verge Mixed Port",
"Verge Socks Port": "Verge Socks Port",
"Verge Redir Port": "Verge Redir Port",
"Verge Tproxy Port": "Verge Tproxy Port",
"Verge Port": "Verge Port",
"Verge HTTP Enabled": "Verge HTTP Enabled",
"WebDAV URL": "WebDAV URL",
"WebDAV Username": "WebDAV Username",
"WebDAV Password": "WebDAV Password",
"Copy Env": "Copy Env",
"Conf Dir": "Conf Dir",
"Core Dir": "Core Dir",
"Logs Dir": "Logs Dir",
"Open Dir": "Open Dir",
"Restart Clash Core": "Restart Clash Core",
"Restart App": "Restart App",
"More": "More",
"Dashboard": "Dashboard",
"Rule Mode": "Rule Mode",
"Global Mode": "Global Mode",
"Direct Mode": "Direct Mode",
"Enable Tray Speed": "Enable Tray Speed",
"Lite Mode": "Lightweight Mode",
"Lite Mode Info": "Close the GUI and keep only the kernel running",
"Config Validation Failed": "Subscription configuration validation failed. Please check the subscription configuration file; modifications have been rolled back.",
"Boot Config Validation Failed": "Boot subscription configuration validation failed. Started with the default configuration; please check the subscription configuration file.",
"Core Change Config Validation Failed": "Configuration validation failed when switching the kernel. Started with the default configuration; please check the subscription configuration file.",
"Config Validation Process Terminated": "The validation process has been terminated.",
"Script Syntax Error": "Script syntax error, changes reverted",
"Script Missing Main": "Script error, changes reverted",
"File Not Found": "File missing, changes reverted",
"Script File Error": "Script file error, changes reverted",
"Core Changed Successfully": "Core changed successfully",
"Failed to Change Core": "Failed to change core",
"YAML Syntax Error": "YAML syntax error, changes reverted",
"YAML Read Error": "YAML read error, changes reverted",
"YAML Mapping Error": "YAML mapping error, changes reverted",
"YAML Key Error": "YAML key error, changes reverted",
"YAML Error": "YAML error, changes reverted",
"Merge File Syntax Error": "Merge file syntax error, changes reverted",
"Merge File Mapping Error": "Merge file mapping error, changes reverted",
"Merge File Key Error": "Merge file key error, changes reverted",
"Merge File Error": "Merge file error, changes reverted",
"Validate YAML File": "Validate YAML File",
"Validate Merge File": "Validate Merge File",
"Validation Success": "Validation Success",
"Validation Failed": "Validation Failed"
}
+54 -5
View File
@@ -113,7 +113,7 @@
"select": "انتخاب پروکسی به صورت دستی",
"url-test": "انتخاب پروکسی بر اساس تأخیر آزمایش URL",
"fallback": "تعویض به پروکسی دیگر در صورت بروز خطا",
"load-balance": "توزیع ��روکسی بر اساس توازن بار",
"load-balance": "توزیع پراکسی بر اساس توازن بار",
"relay": "عبور از زنجیره پروکسی تعریف شده",
"Group Name": "نام گروه",
"Use Proxies": "استفاده از پروکسی‌ها",
@@ -178,7 +178,8 @@
"Process": "فرآیند",
"Time": "زمان",
"Source": "منبع",
"Destination IP": "آدرس IP مقصد",
"Destination": "آدرس IP مقصد",
"DestinationPort": "بندر هدف",
"Close Connection": "بستن اتصال",
"Rules": "قوانین",
"Rule Provider": "تأمین‌کننده قانون",
@@ -229,6 +230,7 @@
"Proxy Guard Info": "امکان جلوگیری از نرم‌افزارهای دیگر از تغییر تنظیمات پروکسی سیستم عامل را فعال کنید",
"Guard Duration": "مدت محافظت",
"Always use Default Bypass": "همیشه از دور زدن پیش‌فرض استفاده کنید",
"Use Bypass Check": "استخدم التحقق من التحايل",
"Proxy Bypass": "دور زدن پراکسی: ",
"Bypass": "دور زدن: ",
"Use PAC Mode": "استفاده از حالت PAC",
@@ -327,6 +329,7 @@
"Default Latency Test Info": "فقط برای تست درخواست‌های کلاینت HTTP استفاده می‌شود و بر فایل پیکربندی تأثیری نخواهد داشت",
"Default Latency Timeout": "زمان انتظار تأخیر پیش‌فرض",
"Hotkey Setting": "تنظیمات کلیدهای میانبر",
"Enable Global Hotkey": "فعال کردن کلید میانبر سراسری",
"open_or_close_dashboard": "باز/بستن داشبورد",
"clash_mode_rule": "حالت قانون",
"clash_mode_global": "حالت جهانی",
@@ -337,6 +340,7 @@
"Backup Setting Info": "از فایل های پیکربندی پشتیبان WebDAV پشتیبانی می کند",
"Runtime Config": "پیکربندی زمان اجرا",
"Open Conf Dir": "باز کردن پوشه برنامه",
"Open Conf Dir Info": "اگر نرم‌افزار به‌طور غیرعادی اجرا می‌شود، از تمام فایل‌های موجود در این پوشه نسخه پشتیبان تهیه و پاک کنید تا نرم‌افزار را مجدداً راه‌اندازی کنید",
"Open Core Dir": "باز کردن پوشه هسته",
"Open Logs Dir": "باز کردن پوشه لاگ‌ها",
"Check for Updates": "بررسی برای به‌روزرسانی‌ها",
@@ -354,7 +358,6 @@
"Match Whole Word": "تطبیق کل کلمه",
"Use Regular Expression": "استفاده از عبارت منظم",
"Profile Imported Successfully": "پروفایل با موفقیت وارد شد",
"Clash Config Updated": "پیکربندی Clash به‌روزرسانی شد",
"Profile Switched": "پروفایل تغییر یافت",
"Profile Reactivated": "پروفایل مجدداً فعال شد",
"Only YAML Files Supported": "فقط فایل‌های YAML پشتیبانی می‌شوند",
@@ -370,7 +373,6 @@
"Permissions Granted Successfully for _clash Core": "مجوزها با موفقیت برای هسته {{core}} اعطا شد",
"Core Version Updated": "نسخه هسته به‌روزرسانی شد",
"Clash Core Restarted": "هسته Clash مجدداً راه‌اندازی شد",
"Switched to _clash Core": "تغییر به هسته {{core}}",
"GeoData Updated": "GeoData به‌روزرسانی شد",
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
"Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد",
@@ -397,5 +399,52 @@
"Confirm to delete this backup file?": "آیا از حذف این فایل پشتیبان اطمینان دارید؟",
"Confirm to restore this backup file?": "آیا از بازیابی این فایل پشتیبان اطمینان دارید؟",
"Restore Success, App will restart in 1s": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راه‌اندازی مجدد می‌شود",
"Failed to fetch backup files": "دریافت فایل‌های پشتیبان ناموفق بود"
"Failed to fetch backup files": "دریافت فایل‌های پشتیبان ناموفق بود",
"Profile": "پروفایل",
"Help": "راهنما",
"About": "درباره",
"Theme": "پوسته",
"Main Window": "پنجره اصلی",
"Group Icon": "آیکون گروه",
"Menu Icon": "آیکون منو",
"PAC File": "فایل PAC",
"Web UI": "رابط وب",
"Hotkeys": "کلیدهای میانبر",
"Verge Mixed Port": "پورت ترکیبی Verge",
"Verge Socks Port": "پورت Socks Verge",
"Verge Redir Port": "پورت تغییر مسیر Verge",
"Verge Tproxy Port": "پورت Tproxy Verge",
"Verge Port": "پورت Verge",
"Verge HTTP Enabled": "HTTP Verge فعال",
"WebDAV URL": "آدرس WebDAV",
"WebDAV Username": "نام کاربری WebDAV",
"WebDAV Password": "رمز عبور WebDAV",
"Dashboard": "داشبورد",
"Restart App": "راه‌اندازی مجدد برنامه",
"Restart Clash Core": "راه‌اندازی مجدد هسته Clash",
"TUN Mode": "حالت TUN",
"Copy Env": "کپی متغیرهای محیطی",
"Conf Dir": "پوشه پیکربندی",
"Core Dir": "پوشه هسته",
"Logs Dir": "پوشه لاگ‌ها",
"Open Dir": "باز کردن پوشه",
"More": "بیشتر",
"Rule Mode": "حالت قوانین",
"Global Mode": "حالت جهانی",
"Direct Mode": "حالت مستقیم",
"Enable Tray Speed": "فعال کردن سرعت ترای",
"Lite Mode": "در فارسی",
"Lite Mode Info": "رابط کاربری گرافیکی را ببندید و فقط هسته را در حال اجرا نگه دارید",
"Config Validation Failed": "اعتبارسنجی پیکربندی اشتراک ناموفق بود، فایل پیکربندی را بررسی کنید، تغییرات برگشت داده شد، جزئیات خطا:",
"Boot Config Validation Failed": "اعتبارسنجی پیکربندی هنگام راه‌اندازی ناموفق بود، پیکربندی پیش‌فرض استفاده شد، فایل پیکربندی را بررسی کنید، جزئیات خطا:",
"Core Change Config Validation Failed": "اعتبارسنجی پیکربندی هنگام تغییر هسته ناموفق بود، پیکربندی پیش‌فرض استفاده شد، فایل پیکربندی را بررسی کنید، جزئیات خطا:",
"Config Validation Process Terminated": "فرآیند اعتبارسنجی متوقف شد",
"Script Syntax Error": "خطای نحوی اسکریپت، تغییرات برگشت داده شد",
"Script Missing Main": "خطای اسکریپت، تغییرات برگشت داده شد",
"File Not Found": "فایل یافت نشد، تغییرات برگشت داده شد",
"Script File Error": "خطای فایل اسکریپت، تغییرات برگشت داده شد",
"Core Changed Successfully": "هسته با موفقیت تغییر کرد",
"Failed to Change Core": "تغییر هسته ناموفق بود",
"Verge Basic Setting": "تنظیمات پایه Verge",
"Verge Advanced Setting": "تنظیمات پیشرفته Verge"
}
+449
View File
@@ -0,0 +1,449 @@
{
"millis": "milidetik",
"seconds": "detik",
"mins": "menit",
"Back": "Kembali",
"Close": "Tutup",
"Cancel": "Batal",
"Confirm": "Konfirmasi",
"Maximize": "Maksimalkan",
"Minimize": "Minimalkan",
"Format document": "Format dokumen",
"Empty": "Kosong",
"New": "Baru",
"Edit": "Ubah",
"Save": "Simpan",
"Delete": "Hapus",
"Enable": "Aktifkan",
"Disable": "Nonaktifkan",
"Label-Proxies": "Proksi",
"Label-Profiles": "Profil",
"Label-Connections": "Koneksi",
"Label-Rules": "Aturan",
"Label-Logs": "Log",
"Label-Test": "Tes",
"Label-Settings": "Pengaturan",
"Dashboard": "Dasbor",
"Profile": "Profil",
"Help": "Bantuan",
"About": "Tentang",
"Theme": "Tema",
"Main Window": "Jendela Utama",
"Group Icon": "Ikon Grup",
"Menu Icon": "Ikon Menu",
"PAC File": "Berkas PAC",
"Web UI": "Antarmuka Web",
"Hotkeys": "Pintasan",
"Verge Mixed Port": "Port Campuran Verge",
"Verge Socks Port": "Port Socks Verge",
"Verge Redir Port": "Port Pengalihan Verge",
"Verge Tproxy Port": "Port Tproxy Verge",
"Verge Port": "Port Verge",
"Verge HTTP Enabled": "HTTP Verge Diaktifkan",
"WebDAV URL": "URL WebDAV",
"WebDAV Username": "Nama Pengguna WebDAV",
"WebDAV Password": "Kata Sandi WebDAV",
"Restart App": "Mulai Ulang Aplikasi",
"Restart Clash Core": "Mulai Ulang Core Clash",
"TUN Mode": "Mode TUN",
"Copy Env": "Salin Env",
"Conf Dir": "Direktori Konfigurasi",
"Core Dir": "Direktori Core",
"Logs Dir": "Direktori Log",
"Open Dir": "Buka Direktori",
"More": "Lainnya",
"Rule Mode": "Mode Aturan",
"Global Mode": "Mode Global",
"Direct Mode": "Mode Langsung",
"Proxies": "Proksi",
"Proxy Groups": "Grup Proksi",
"Proxy Provider": "Penyedia Proksi",
"Update All": "Perbarui Semua",
"Update At": "Diperbarui Pada",
"rule": "aturan",
"global": "global",
"direct": "langsung",
"script": "skrip",
"Location": "Lokasi",
"Delay check": "Periksa Keterlambatan",
"Sort by default": "Urutkan secara default",
"Sort by delay": "Urutkan berdasarkan keterlambatan",
"Sort by name": "Urutkan berdasarkan nama",
"Delay check URL": "URL Periksa Keterlambatan",
"Delay check to cancel fixed": "Periksa keterlambatan untuk membatalkan tetap",
"Proxy basic": "Dasar Proksi",
"Proxy detail": "Detail Proksi",
"Profiles": "Profil",
"Update All Profiles": "Perbarui Semua Profil",
"View Runtime Config": "Lihat Konfigurasi Runtime",
"Reactivate Profiles": "Reaktivasi Profil",
"Paste": "Tempel",
"Profile URL": "URL Profil",
"Import": "Impor",
"From": "Dari",
"Update Time": "Waktu Pembaruan",
"Used / Total": "Digunakan / Total",
"Expire Time": "Waktu Kedaluwarsa",
"Create Profile": "Buat Profil",
"Edit Profile": "Ubah Profil",
"Edit Proxies": "Ubah Proksi",
"Use newlines for multiple uri": "Gunakan baris baru untuk beberapa URI (mendukung pengkodean Base64)",
"Edit Rules": "Ubah Aturan",
"Rule Type": "Jenis Aturan",
"Rule Content": "Konten Aturan",
"Proxy Policy": "Kebijakan Proksi",
"No Resolve": "Tidak Menyelesaikan",
"Prepend Rule": "Tambahkan Aturan di Awal",
"Append Rule": "Tambahkan Aturan di Akhir",
"Prepend Group": "Tambahkan Grup di Awal",
"Append Group": "Tambahkan Grup di Akhir",
"Prepend Proxy": "Tambahkan Proksi di Awal",
"Append Proxy": "Tambahkan Proksi di Akhir",
"Rule Condition Required": "Kondisi Aturan Diperlukan",
"Invalid Rule": "Aturan Tidak Valid",
"Advanced": "Lanjutan",
"Visualization": "Visualisasi",
"DOMAIN": "Cocok dengan nama domain lengkap",
"DOMAIN-SUFFIX": "Cocok dengan sufiks domain",
"DOMAIN-KEYWORD": "Cocok dengan kata kunci domain",
"DOMAIN-REGEX": "Cocok dengan domain menggunakan ekspresi reguler",
"GEOSITE": "Cocok dengan domain dalam Geosite",
"GEOIP": "Cocok dengan kode negara alamat IP",
"SRC-GEOIP": "Cocok dengan kode negara alamat IP sumber",
"IP-ASN": "Cocok dengan ASN alamat IP",
"SRC-IP-ASN": "Cocok dengan ASN alamat IP sumber",
"IP-CIDR": "Cocok dengan rentang alamat IP",
"IP-CIDR6": "Cocok dengan rentang alamat IPv6",
"SRC-IP-CIDR": "Cocok dengan rentang alamat IP sumber",
"IP-SUFFIX": "Cocok dengan rentang sufiks alamat IP",
"SRC-IP-SUFFIX": "Cocok dengan rentang sufiks alamat IP sumber",
"SRC-PORT": "Cocok dengan rentang port sumber",
"DST-PORT": "Cocok dengan rentang port tujuan",
"IN-PORT": "Cocok dengan port masuk",
"DSCP": "Penandaan DSCP (hanya untuk tproxy UDP masuk)",
"PROCESS-NAME": "Cocok dengan nama proses (nama paket Android)",
"PROCESS-PATH": "Cocok dengan jalur proses lengkap",
"PROCESS-NAME-REGEX": "Cocok dengan nama proses lengkap menggunakan ekspresi reguler (nama paket Android)",
"PROCESS-PATH-REGEX": "Cocok dengan jalur proses lengkap menggunakan ekspresi reguler",
"NETWORK": "Cocok dengan protokol transportasi (tcp/udp)",
"UID": "Cocok dengan ID PENGGUNA Linux",
"IN-TYPE": "Cocok dengan jenis masuk",
"IN-USER": "Cocok dengan nama pengguna masuk",
"IN-NAME": "Cocok dengan nama masuk",
"SUB-RULE": "Sub-aturan",
"RULE-SET": "Cocok dengan set aturan",
"AND": "Logika DAN",
"OR": "Logika ATAU",
"NOT": "Logika TIDAK",
"MATCH": "Cocok dengan semua permintaan",
"DIRECT": "Data langsung keluar",
"REJECT": "Mencegat permintaan",
"REJECT-DROP": "Membuang permintaan",
"PASS": "Lewati aturan ini saat cocok",
"Edit Groups": "Ubah Grup Proksi",
"Group Type": "Jenis Grup",
"select": "Pilih proksi secara manual",
"url-test": "Pilih proksi berdasarkan keterlambatan tes URL",
"fallback": "Beralih ke proksi lain saat terjadi kesalahan",
"load-balance": "Distribusikan proksi berdasarkan penyeimbangan beban",
"relay": "Lewatkan melalui rantai proksi yang ditentukan",
"Group Name": "Nama Grup",
"Use Proxies": "Gunakan Proksi",
"Use Provider": "Gunakan Penyedia",
"Health Check Url": "URL Pemeriksaan Kesehatan",
"Expected Status": "Status yang Diharapkan",
"Interval": "Interval",
"Lazy": "Malas",
"Timeout": "Waktu Habis",
"Max Failed Times": "Jumlah Gagal Maksimal",
"Interface Name": "Nama Antarmuka",
"Routing Mark": "Tanda Routing",
"Include All": "Sertakan Semua Proksi dan Penyedia",
"Include All Providers": "Sertakan Semua Penyedia",
"Include All Proxies": "Sertakan Semua Proksi",
"Exclude Filter": "Kecualikan Filter",
"Exclude Type": "Kecualikan Jenis",
"Disable UDP": "Nonaktifkan UDP",
"Hidden": "Tersembunyi",
"Group Name Required": "Nama Grup Diperlukan",
"Group Name Already Exists": "Nama Grup Sudah Ada",
"Extend Config": "Perluas Konfigurasi",
"Extend Script": "Perluas Skrip",
"Global Merge": "Perluas Konfigurasi Global",
"Global Script": "Perluas Skrip Global",
"Type": "Jenis",
"Name": "Nama",
"Descriptions": "Deskripsi",
"Subscription URL": "URL Langganan",
"Update Interval": "Interval Pembaruan",
"Choose File": "Pilih Berkas",
"Use System Proxy": "Gunakan Proksi Sistem",
"Use Clash Proxy": "Gunakan Proksi Clash",
"Accept Invalid Certs (Danger)": "Terima Sertifikat Tidak Valid (Bahaya)",
"Refresh": "Segarkan",
"Home": "Beranda",
"Select": "Pilih",
"Edit Info": "Ubah Info",
"Edit File": "Ubah Berkas",
"Open File": "Buka Berkas",
"Update": "Perbarui",
"Update(Proxy)": "Perbarui (Proksi)",
"Confirm deletion": "Konfirmasi penghapusan",
"This operation is not reversible": "Operasi ini tidak dapat dibatalkan",
"Script Console": "Konsol Skrip",
"To Top": "Ke Atas",
"To End": "Ke Bawah",
"Connections": "Koneksi",
"Table View": "Tampilan Tabel",
"List View": "Tampilan Daftar",
"Close All": "Tutup Semua",
"Default": "Default",
"Download Speed": "Kecepatan Unduh",
"Upload Speed": "Kecepatan Unggah",
"Host": "Host",
"Downloaded": "Diunduh",
"Uploaded": "Diunggah",
"DL Speed": "Kecepatan Unduh",
"UL Speed": "Kecepatan Unggah",
"Chains": "Rantai",
"Rule": "Aturan",
"Process": "Proses",
"Time": "Waktu",
"Source": "Sumber",
"Destination": "IP Tujuan",
"DestinationPort": "Port Tujuan",
"Close Connection": "Tutup Koneksi",
"Rules": "Aturan",
"Rule Provider": "Penyedia Aturan",
"Logs": "Log",
"Pause": "Jeda",
"Clear": "Bersihkan",
"Test": "Tes",
"Test All": "Tes Semua",
"Create Test": "Buat Tes",
"Edit Test": "Ubah Tes",
"Icon": "Ikon",
"Test URL": "URL Tes",
"Settings": "Pengaturan",
"System Setting": "Pengaturan Sistem",
"Tun Mode": "Mode Tun (NIC Virtual)",
"Reset to Default": "Setel Ulang ke Default",
"Tun Mode Info": "Mode Tun (NIC Virtual): Menangkap semua lalu lintas sistem, saat diaktifkan, tidak perlu mengaktifkan proksi sistem.",
"Stack": "Tumpukan Tun",
"System and Mixed Can Only be Used in Service Mode": "Sistem dan Campuran Hanya Dapat Digunakan dalam Mode Layanan",
"Device": "Nama Perangkat",
"Auto Route": "Rute Otomatis",
"Strict Route": "Rute Ketat",
"Auto Detect Interface": "Deteksi Antarmuka Otomatis",
"DNS Hijack": "Pembajakan DNS",
"MTU": "Unit Transmisi Maksimum",
"Service Mode": "Mode Layanan",
"Service Mode Info": "Harap instal mode layanan sebelum mengaktifkan mode TUN. Proses kernel yang dimulai oleh layanan dapat memperoleh izin untuk menginstal kartu jaringan virtual (mode TUN)",
"Current State": "Status Saat Ini",
"pending": "tertunda",
"installed": "terinstal",
"uninstall": "dicopot",
"active": "aktif",
"unknown": "tidak diketahui",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Informasi: Harap pastikan bahwa Layanan Clash Verge terinstal dan diaktifkan",
"Install": "Instal",
"Uninstall": "Copot",
"Disable Service Mode": "Nonaktifkan Mode Layanan",
"System Proxy": "Proksi Sistem",
"System Proxy Info": "Aktifkan untuk mengubah pengaturan proksi sistem operasi. Jika pengaktifan gagal, ubah pengaturan proksi sistem operasi secara manual",
"System Proxy Setting": "Pengaturan Proksi Sistem",
"Current System Proxy": "Proksi Sistem Saat Ini",
"Enable status": "Status Pengaktifan:",
"Enabled": "Diaktifkan",
"Disabled": "Dinonaktifkan",
"Server Addr": "Alamat Server: ",
"Not available": "Tidak tersedia",
"Proxy Guard": "Penjaga Proksi",
"Proxy Guard Info": "Aktifkan untuk mencegah perangkat lunak lain mengubah pengaturan proksi sistem operasi",
"Guard Duration": "Durasi Penjagaan",
"Always use Default Bypass": "Selalu gunakan Bypass Default",
"Proxy Bypass": "Pengaturan Bypass Proksi: ",
"Bypass": "Bypass: ",
"Use PAC Mode": "Gunakan Mode PAC",
"PAC Script Content": "Konten Skrip PAC",
"PAC URL": "URL PAC: ",
"Auto Launch": "Peluncuran Otomatis",
"Silent Start": "Mulai Senyap",
"Silent Start Info": "Mulai program dalam mode latar belakang tanpa menampilkan panel",
"TG Channel": "Saluran Telegram",
"Manual": "Manual",
"Github Repo": "Repositori Github",
"Clash Setting": "Pengaturan Clash",
"Allow Lan": "Izinkan LAN",
"Network Interface": "Antarmuka Jaringan",
"Ip Address": "Alamat IP",
"Mac Address": "Alamat MAC",
"IPv6": "IPv6",
"Unified Delay": "Keterlambatan Terpadu",
"Unified Delay Info": "Saat keterlambatan terpadu diaktifkan, dua tes keterlambatan akan dilakukan untuk menghilangkan perbedaan keterlambatan antara berbagai jenis node yang disebabkan oleh jabat tangan koneksi, dll.",
"Log Level": "Tingkat Log",
"Log Level Info": "Ini hanya berlaku untuk file log kernel di folder layanan di direktori log.",
"Port Config": "Konfigurasi Port",
"Random Port": "Port Acak",
"Mixed Port": "Port Campuran",
"Socks Port": "Port Socks",
"Http Port": "Port Http(s)",
"Redir Port": "Port Redir",
"Tproxy Port": "Port Tproxy",
"External": "Eksternal",
"External Controller": "Alamat Pengendali Eksternal",
"Core Secret": "Rahasia Inti",
"Recommended": "Direkomendasikan",
"Open URL": "Buka URL",
"Replace host, port, secret with %host, %port, %secret": "Ganti host, port, rahasia dengan %host, %port, %secret",
"Support %host, %port, %secret": "Dukung %host, %port, %secret",
"Clash Core": "Inti Clash",
"Upgrade": "Tingkatkan",
"Restart": "Mulai Ulang",
"Release Version": "Versi Rilis",
"Alpha Version": "Versi Alpha",
"Please Enable Service Mode": "Harap Instal dan Aktifkan Mode Layanan Terlebih Dahulu",
"Please enter your root password": "Harap masukkan kata sandi root Anda",
"Grant": "Izinkan",
"Open UWP tool": "Buka alat UWP",
"Open UWP tool Info": "Sejak Windows 8, aplikasi UWP (seperti Microsoft Store) dibatasi dari mengakses layanan jaringan host lokal secara langsung, dan alat ini dapat digunakan untuk melewati pembatasan ini",
"Update GeoData": "Perbarui GeoData",
"Verge Setting": "Pengaturan Verge",
"Language": "Bahasa",
"Theme Mode": "Mode Tema",
"theme.light": "Terang",
"theme.dark": "Gelap",
"theme.system": "Sistem",
"Tray Click Event": "Acara Klik Tray",
"Show Main Window": "Tampilkan Jendela Utama",
"Copy Env Type": "Salin Jenis Env",
"Copy Success": "Salin Berhasil",
"Start Page": "Halaman Mulai",
"Startup Script": "Skrip Startup",
"Browse": "Jelajahi",
"Theme Setting": "Pengaturan Tema",
"Primary Color": "Warna Utama",
"Secondary Color": "Warna Sekunder",
"Primary Text": "Teks Utama",
"Secondary Text": "Teks Sekunder",
"Info Color": "Warna Info",
"Warning Color": "Warna Peringatan",
"Error Color": "Warna Kesalahan",
"Success Color": "Warna Keberhasilan",
"Font Family": "Keluarga Font",
"CSS Injection": "Injeksi CSS",
"Layout Setting": "Pengaturan Tata Letak",
"Traffic Graph": "Grafik Lalu Lintas",
"Memory Usage": "Penggunaan Memori",
"Memory Cleanup": "Ketuk untuk membersihkan memori",
"Proxy Group Icon": "Ikon Grup Proksi",
"Nav Icon": "Ikon Navigasi",
"Monochrome": "Monokrom",
"Colorful": "Berwarna",
"Tray Icon": "Ikon Tray",
"Common Tray Icon": "Ikon Tray Umum",
"System Proxy Tray Icon": "Ikon Tray Proksi Sistem",
"Tun Tray Icon": "Ikon Tray Tun",
"Miscellaneous": "Lain-lain",
"App Log Level": "Tingkat Log Aplikasi",
"Auto Close Connections": "Tutup Koneksi Otomatis",
"Auto Close Connections Info": "Hentikan koneksi yang sudah ada saat pemilihan grup proksi atau mode proksi berubah",
"Auto Check Update": "Periksa Pembaruan Otomatis",
"Enable Builtin Enhanced": "Aktifkan Peningkatan Bawaan",
"Enable Builtin Enhanced Info": "Penanganan kompatibilitas untuk file konfigurasi",
"Proxy Layout Columns": "Kolom Tata Letak Proksi",
"Auto Columns": "Kolom Otomatis",
"Auto Log Clean": "Pembersihan Log Otomatis",
"Never Clean": "Jangan Pernah Bersihkan",
"Retain _n Days": "Simpan {{n}} Hari",
"Default Latency Test": "Tes Latensi Default",
"Default Latency Test Info": "Digunakan hanya untuk pengujian permintaan klien HTTP dan tidak akan mempengaruhi file konfigurasi",
"Default Latency Timeout": "Waktu Habis Latensi Default",
"Hotkey Setting": "Pengaturan Pintasan",
"Enable Global Hotkey": "Aktifkan Hotkey Global",
"open_or_close_dashboard": "Buka/Tutup Dasbor",
"clash_mode_rule": "Mode Aturan",
"clash_mode_global": "Mode Global",
"clash_mode_direct": "Mode Langsung",
"toggle_system_proxy": "Aktifkan/Nonaktifkan Proksi Sistem",
"toggle_tun_mode": "Aktifkan/Nonaktifkan Mode Tun",
"Backup Setting": "Pengaturan Cadangan",
"Backup Setting Info": "Mendukung file konfigurasi cadangan WebDAV",
"Runtime Config": "Konfigurasi Runtime",
"Open Conf Dir": "Buka Direktori Konfigurasi",
"Open Conf Dir Info": "Jika perangkat lunak berjalan tidak normal, CADANGKAN dan hapus semua file di folder ini lalu mulai ulang perangkat lunak",
"Open Core Dir": "Buka Direktori Core",
"Open Logs Dir": "Buka Direktori Log",
"Check for Updates": "Periksa Pembaruan",
"Go to Release Page": "Pergi ke Halaman Rilis",
"Portable Updater Error": "Versi portabel tidak mendukung pembaruan dalam aplikasi. Harap unduh dan ganti secara manual",
"Break Change Update Error": "Versi ini adalah pembaruan besar dan tidak mendukung pembaruan dalam aplikasi. Harap hapus instalasi dan unduh serta instal versi baru secara manual",
"Open Dev Tools": "Buka Alat Pengembang",
"Exit": "Keluar",
"Verge Version": "Versi Verge",
"ReadOnly": "Hanya Baca",
"ReadOnlyMessage": "Tidak dapat mengedit di editor hanya baca",
"Filter": "Filter",
"Filter conditions": "Kondisi Filter",
"Match Case": "Cocokkan Kasus",
"Match Whole Word": "Cocokkan Kata Utuh",
"Use Regular Expression": "Gunakan Ekspresi Reguler",
"Profile Imported Successfully": "Profil Berhasil Diimpor",
"Profile Switched": "Profil Beralih",
"Profile Reactivated": "Profil Diaktifkan Kembali",
"Only YAML Files Supported": "Hanya File YAML yang Didukung",
"Settings Applied": "Pengaturan Diterapkan",
"Service Installed Successfully": "Layanan Berhasil Diinstal",
"Service Uninstalled Successfully": "Layanan Berhasil Dicopot",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Durasi Daemon Proksi Tidak Boleh Kurang dari 1 Detik",
"Invalid Bypass Format": "Format Bypass Tidak Valid",
"Clash Port Modified": "Port Clash Diubah",
"Port Conflict": "Konflik Port",
"Restart Application to Apply Modifications": "Mulai Ulang Aplikasi untuk Menerapkan Modifikasi",
"External Controller Address Modified": "Alamat Pengendali Eksternal Diubah",
"Permissions Granted Successfully for _clash Core": "Izin Berhasil Diberikan untuk Core {{core}}",
"Core Version Updated": "Versi Core Diperbarui",
"Clash Core Restarted": "Core Clash Dimulai Ulang",
"GeoData Updated": "GeoData Diperbarui",
"Currently on the Latest Version": "Saat ini pada Versi Terbaru",
"Import Subscription Successful": "Berlangganan Berhasil Diimpor",
"WebDAV Server URL": "URL Server WebDAV",
"Username": "Nama Pengguna",
"Password": "Kata Sandi",
"Backup": "Cadangan",
"Filename": "Nama Berkas",
"Actions": "Tindakan",
"Restore": "Pulihkan",
"No Backups": "Tidak ada cadangan yang tersedia",
"WebDAV URL Required": "URL WebDAV tidak boleh kosong",
"Invalid WebDAV URL": "Format URL WebDAV tidak valid",
"Username Required": "Nama pengguna tidak boleh kosong",
"Password Required": "Kata sandi tidak boleh kosong",
"Failed to Fetch Backups": "Gagal mengambil file cadangan",
"WebDAV Config Saved": "Konfigurasi WebDAV berhasil disimpan",
"WebDAV Config Save Failed": "Gagal menyimpan konfigurasi WebDAV: {{error}}",
"Backup Created": "Cadangan berhasil dibuat",
"Backup Failed": "Cadangan gagal: {{error}}",
"Delete Backup": "Hapus Cadangan",
"Restore Backup": "Pulihkan Cadangan",
"Backup Time": "Waktu Cadangan",
"Confirm to delete this backup file?": "Konfirmasi untuk menghapus file cadangan ini?",
"Confirm to restore this backup file?": "Konfirmasi untuk memulihkan file cadangan ini?",
"Restore Success, App will restart in 1s": "Pemulihan Berhasil, Aplikasi akan dimulai ulang dalam 1 detik",
"Failed to fetch backup files": "Gagal mengambil file cadangan",
"Enable Tray Speed": "Aktifkan Tray Speed",
"Lite Mode": "Mode Ringan",
"Lite Mode Info": "Tutup GUI dan biarkan hanya kernel yang berjalan",
"Config Validation Failed": "Validasi konfigurasi langganan gagal, periksa file konfigurasi, perubahan dibatalkan, detail kesalahan:",
"Boot Config Validation Failed": "Validasi konfigurasi saat boot gagal, menggunakan konfigurasi default, periksa file konfigurasi, detail kesalahan:",
"Core Change Config Validation Failed": "Validasi konfigurasi saat ganti inti gagal, menggunakan konfigurasi default, periksa file konfigurasi, detail kesalahan:",
"Config Validation Process Terminated": "Proses validasi dihentikan",
"Script Syntax Error": "Kesalahan sintaks skrip, perubahan dibatalkan",
"Script Missing Main": "Kesalahan skrip, perubahan dibatalkan",
"File Not Found": "File tidak ditemukan, perubahan dibatalkan",
"Script File Error": "Kesalahan file skrip, perubahan dibatalkan",
"Core Changed Successfully": "Inti berhasil diubah",
"Failed to Change Core": "Gagal mengubah inti",
"Verge Basic Setting": "Pengaturan Dasar Verge",
"Verge Advanced Setting": "Pengaturan Lanjutan Verge"
}
+53 -4
View File
@@ -178,7 +178,8 @@
"Process": "Процесс",
"Time": "Время подключения",
"Source": "Исходный адрес",
"Destination IP": "IP-адрес назначения",
"Destination": "IP-адрес назначения",
"DestinationPort": "Целевой порт",
"Close Connection": "Закрыть соединение",
"Rules": "Правила",
"Rule Provider": "Провайдер правило",
@@ -229,6 +230,7 @@
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим программным обеспечением",
"Guard Duration": "Период защиты",
"Always use Default Bypass": "Всегда использовать стандартное обходное решение",
"Use Bypass Check": "Используйте проверку обхода",
"Proxy Bypass": "Игнорирование прокси: ",
"Bypass": "Игнорирование: ",
"Use PAC Mode": "Используйте режим PAC",
@@ -327,6 +329,7 @@
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
"Default Latency Timeout": "Таймаут задержки по умолчанию",
"Hotkey Setting": "Настройки клавиатурных сокращений",
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
"open_or_close_dashboard": "Открыть/Закрыть панель управления",
"clash_mode_rule": "Режим правил",
"clash_mode_global": "Глобальный режим",
@@ -337,6 +340,7 @@
"Backup Setting Info": "Поддерживает файлы конфигурации резервного копирования WebDAV",
"Runtime Config": "Используемый конфиг",
"Open Conf Dir": "Открыть папку приложения",
"Open Conf Dir Info": "Если программное обеспечение работает ненормально, сделайте резервную копию и удалите все файлы в этой папке, а затем перезапустите программное обеспечение",
"Open Core Dir": "Открыть папку ядра",
"Open Logs Dir": "Открыть папку логов",
"Check for Updates": "Проверить обновления",
@@ -354,7 +358,6 @@
"Match Whole Word": "Полное совпадение слова",
"Use Regular Expression": "Использовать регулярные выражения",
"Profile Imported Successfully": "Профиль успешно импортирован",
"Clash Config Updated": "Clash конфигурация Обновлена",
"Profile Switched": "Профиль изменен",
"Profile Reactivated": "Профиль повторно активирован",
"Only YAML Files Supported": "Поддерживаются только файлы YAML",
@@ -370,7 +373,6 @@
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
"Core Version Updated": "Обновлена версия ядра",
"Clash Core Restarted": "Clash ядра перезапущено",
"Switched to _clash Core": "Переключено на ядра {{core}}",
"GeoData Updated": "GeoData Обновлена",
"Currently on the Latest Version": "В настоящее время используется последняя версия",
"Import subscription successful": "Импорт подписки успешно",
@@ -397,5 +399,52 @@
"Confirm to delete this backup file?": "Вы уверены, что хотите удалить этот файл резервной копии?",
"Confirm to restore this backup file?": "Вы уверены, что хотите восстановить этот файл резервной копии?",
"Restore Success, App will restart in 1s": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду",
"Failed to fetch backup files": "Не удалось получить файлы резервных копий"
"Failed to fetch backup files": "Не удалось получить файлы резервных копий",
"Profile": "Профиль",
"Help": "Помощь",
"About": "О программе",
"Theme": "Тема",
"Main Window": "Главное окно",
"Group Icon": "Иконка группы",
"Menu Icon": "Иконка меню",
"PAC File": "PAC файл",
"Web UI": "Веб-интерфейс",
"Hotkeys": "Горячие клавиши",
"Verge Mixed Port": "Смешанный порт Verge",
"Verge Socks Port": "Порт Verge Socks",
"Verge Redir Port": "Порт перенаправления Verge",
"Verge Tproxy Port": "Порт Verge Tproxy",
"Verge Port": "Порт Verge",
"Verge HTTP Enabled": "HTTP Verge включен",
"WebDAV URL": "URL WebDAV",
"WebDAV Username": "Имя пользователя WebDAV",
"WebDAV Password": "Пароль WebDAV",
"Dashboard": "Панель управления",
"Restart App": "Перезапустить приложение",
"Restart Clash Core": "Перезапустить ядро Clash",
"TUN Mode": "Режим TUN",
"Copy Env": "Копировать переменные окружения",
"Conf Dir": "Директория конфигурации",
"Core Dir": "Директория ядра",
"Logs Dir": "Директория логов",
"Open Dir": "Открыть директорию",
"More": "Ещё",
"Rule Mode": "Режим правил",
"Global Mode": "Глобальный режим",
"Direct Mode": "Прямой режим",
"Enable Tray Speed": "Включить скорость в лотке",
"Lite Mode": "Облегченный режим",
"Lite Mode Info": "Закройте графический интерфейс и оставьте работать только ядро",
"Config Validation Failed": "Ошибка проверки конфигурации подписки, проверьте файл конфигурации, изменения отменены, ошибка:",
"Boot Config Validation Failed": "Ошибка проверки конфигурации при запуске, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
"Core Change Config Validation Failed": "Ошибка проверки конфигурации при смене ядра, используется конфигурация по умолчанию, проверьте файл конфигурации, ошибка:",
"Config Validation Process Terminated": "Процесс проверки прерван",
"Script Syntax Error": "Ошибка синтаксиса скрипта, изменения отменены",
"Script Missing Main": "Ошибка скрипта, изменения отменены",
"File Not Found": "Файл не найден, изменения отменены",
"Script File Error": "Ошибка файла скрипта, изменения отменены",
"Core Changed Successfully": "Ядро успешно сменено",
"Failed to Change Core": "Не удалось сменить ядро",
"Verge Basic Setting": "Основные настройки Verge",
"Verge Advanced Setting": "Расширенные настройки Verge"
}
+449
View File
@@ -0,0 +1,449 @@
{
"millis": "Миллисекундлар",
"seconds": "Секундлар",
"mins": "Минутлар",
"Back": "Кире",
"Close": "Ябу",
"Cancel": "Баш тарту",
"Confirm": "Растау",
"Maximize": "Зурайту",
"Minimize": "Кечерәйтү",
"Format document": "Документны форматлау",
"Empty": "Буш",
"New": "Яңа",
"Edit": "Үзгәртү",
"Save": "Саклау",
"Delete": "Бетерү",
"Enable": "Кушу",
"Disable": "Сүндерү",
"Label-Proxies": "Прокси",
"Label-Profiles": "Профильләр",
"Label-Connections": "Тоташулар",
"Label-Rules": "Кагыйдәләр",
"Label-Logs": "Логлар",
"Label-Test": "Тест",
"Label-Settings": "Көйләүләр",
"Proxies": "Прокси",
"Proxy Groups": "Прокси төркемнәре",
"Proxy Provider": "Прокси провайдеры",
"Update All": "Барысын да яңарту",
"Update At": "Яңартылган вакыт",
"rule": "кагыйдә",
"global": "глобаль",
"direct": "туры",
"script": "скриптлы",
"Location": "Урын",
"Delay check": "Задержканы тикшерү",
"Sort by default": "Башлангыч итеп сортлау",
"Sort by delay": "Задержка буенча сортлау",
"Sort by name": "Исем буенча сортлау",
"Delay check URL": "Задержканы тикшерү URL-ы",
"Delay check to cancel fixed": "Беркетелгәнне гамәлдән чыгару өчен задержканы тикшерү",
"Proxy basic": "Прокси турында кыскача мәгълүмат",
"Proxy detail": "Прокси турында тулы мәгълүмат",
"Profiles": "Профильләр",
"Update All Profiles": "Барлык профильләрне яңарту",
"View Runtime Config": "Кулланылган конфигурацияне карау",
"Reactivate Profiles": "Профильләрне янәдән активлаштыру",
"Paste": "Кую",
"Profile URL": "Профиль URL-ы",
"Import": "Импорт",
"From": "Каян",
"Update Time": "Яңарту вакыты",
"Used / Total": "Кулланылган / Барлыгы",
"Expire Time": "Тамамлану вакыты",
"Create Profile": "Профиль булдыру",
"Edit Profile": "Профильне үзгәртү",
"Edit Proxies": "Проксины үзгәртү",
"Use newlines for multiple uri": "Берничә URI өчен яңа юл символын кулланыгыз (Base64 кодлавы ярдәм ителә)",
"Edit Rules": "Кагыйдәләрне үзгәртү",
"Rule Type": "Кагыйдә төре",
"Rule Content": "Кагыйдә эчтәлеге",
"Proxy Policy": "Прокси сәясәте",
"No Resolve": "Резолвсыз",
"Prepend Rule": "Кагыйдәне өскә өстәү",
"Append Rule": "Кагыйдәне аска өстәү",
"Prepend Group": "Төркемне өскә өстәү",
"Append Group": "Төркемне аска өстәү",
"Prepend Proxy": "Проксины өскә өстәү",
"Append Proxy": "Проксины аска өстәү",
"Rule Condition Required": "Кагыйдә шарты кирәк",
"Invalid Rule": "Яраксыз кагыйдә",
"Advanced": "Өстәмә",
"Visualization": "Визуализация",
"DOMAIN": "Домен исеменең тулы туры килүе",
"DOMAIN-SUFFIX": "Домен суффиксына туры килү",
"DOMAIN-KEYWORD": "Доменда төп сүзгә туры килү",
"DOMAIN-REGEX": "Доменны регекс аша туры китерү",
"GEOSITE": "Geosite исемлегендәге доменга туры килү",
"GEOIP": "IP-адресның ил коды буенча туры килү",
"SRC-GEOIP": "Чыганак IP-адресның ил коды буенча туры килү",
"IP-ASN": "IP-адрес ASN'ы буенча туры килү",
"SRC-IP-ASN": "Чыганак IP-адрес ASN'ы буенча туры килү",
"IP-CIDR": "IP-адреслар диапазонына туры килү",
"IP-CIDR6": "IPv6 адреслар диапазонына туры килү",
"SRC-IP-CIDR": "Чыганак IP-адреслар диапазонына туры килү",
"IP-SUFFIX": "IP-адрес суффиксына туры килү",
"SRC-IP-SUFFIX": "Чыганак IP-адрес суффиксына туры килү",
"SRC-PORT": "Чыганак портлар диапазонына туры килү",
"DST-PORT": "Максат портлар диапазонына туры килү",
"IN-PORT": "Керүче портка туры килү",
"DSCP": "DSCP тамгалавы (tproxy UDP өчен)",
"PROCESS-NAME": "Процесс исеменә туры килү (Android пакет исеме)",
"PROCESS-PATH": "Процесс юлына туры килү",
"PROCESS-NAME-REGEX": "Процесс исемен регекс белән туры китерү (Android пакет исеме)",
"PROCESS-PATH-REGEX": "Процесс юлын регекс белән туры китерү",
"NETWORK": "Транспорт протоколына (tcp/udp) туры килү",
"UID": "Linux USER ID'га туры килү",
"IN-TYPE": "Керүче тоташу төренә туры килү",
"IN-USER": "Керүче тоташу кулланучысына туры килү",
"IN-NAME": "Керүче тоташу исеменә туры килү",
"SUB-RULE": "Кушымча кагыйдә",
"RULE-SET": "Кагыйдәләр тупланмасына туры килү",
"AND": "Логик ҺӘМ",
"OR": "Логик ЯКИ",
"NOT": "Логик ТҮГЕЛ",
"MATCH": "Барлык сорауларга туры килә",
"DIRECT": "Туры чыгу",
"REJECT": "Сорауларны тоткарлау",
"REJECT-DROP": "Сорауларны кире кагу",
"PASS": "Туры килсә дә, бу кагыйдәне урап узу",
"Edit Groups": "Прокси төркемнәрен үзгәртү",
"Group Type": "Төркем төре",
"select": "Проксины кулдан сайлау",
"url-test": "URL-тест задержкасына карап прокси сайлау",
"fallback": "Хата булган очракта башка проксига күчү",
"load-balance": "Трафикны баланслау нигезендә прокси тарату",
"relay": "Билгеле прокси чылбыры аша тапшыру",
"Group Name": "Төркем исеме",
"Use Proxies": "Прокси куллану",
"Use Provider": "Провайдер куллану",
"Health Check Url": "Сәламәтлекне тикшерү URL-ы",
"Expected Status": "Көтелгән статус коды",
"Interval": "Интервал",
"Lazy": "Сак режим (lazy)",
"Timeout": "Таймаут",
"Max Failed Times": "Иң күп хаталы тикшерү саны",
"Interface Name": "Интерфейс исеме",
"Routing Mark": "Маршрут билгесе",
"Include All": "Барлык прокси һәм провайдерларны кертү",
"Include All Providers": "Барлык провайдерларны кертү",
"Include All Proxies": "Барлык проксины кертү",
"Exclude Filter": "Фильтр аша чыгару",
"Exclude Type": "Чыгару төре",
"Disable UDP": "UDP'ны сүндерү",
"Hidden": "Яшерен",
"Group Name Required": "Төркем исеме кирәк",
"Group Name Already Exists": "Әлеге төркем исеме бар инде",
"Extend Config": "Merge-ны үзгәртергә",
"Extend Script": "Script-ны үзгәртергә",
"Global Merge": "Гомумкеңәйтелгән көйләүләр",
"Global Script": "Гомумкеңәйтелгән скрипт",
"Type": "Төр",
"Name": "Исем",
"Descriptions": "Тасвирламалар",
"Subscription URL": "Подписка URL-ы",
"Update Interval": "Яңарту интервалы",
"Choose File": "Файл сайлау",
"Use System Proxy": "Системалы проксины кулланып яңарту",
"Use Clash Proxy": "Clash прокси кулланып яңарту",
"Accept Invalid Certs (Danger)": "Дөрес булмаган сертификатларны кабул итү (Куркыныч)",
"Refresh": "Яңарту",
"Home": "Баш бит",
"Select": "Сайлау",
"Edit Info": "Мәгълүматны үзгәртү",
"Edit File": "Файлны үзгәртү",
"Open File": "Файлны ачу",
"Update": "Яңарту",
"Update(Proxy)": "Яңарту (прокси аша)",
"Confirm deletion": "Бетерүне раслагыз",
"This operation is not reversible": "Бу гамәлне кире кайтарып булмый",
"Script Console": "Скрипт консоле",
"To Top": "Өскә",
"To End": "Аска",
"Connections": "Тоташулар",
"Table View": "Таблица күзаллау",
"List View": "Исемлек күзаллау",
"Close All": "Барысын да ябу",
"Default": "Башлангыч",
"Download Speed": "Йөкләү тизлеге",
"Upload Speed": "Йөкләү (чыгару) тизлеге",
"Host": "Хост",
"Downloaded": "Йөкләнгән",
"Uploaded": "Чыгарылган",
"DL Speed": "Йөкләү тизл.",
"UL Speed": "Чыгару тизл.",
"Chains": "Чылбырлар",
"Rule": "Кагыйдә",
"Process": "Процесс",
"Time": "Тоташу вакыты",
"Source": "Чыганак адресы",
"Destination": "Максат IP-адресы",
"DestinationPort": "Барасы порты",
"Close Connection": "Тоташуны ябу",
"Rules": "Кагыйдәләр",
"Rule Provider": "Кагыйдә провайдеры",
"Logs": "Логлар",
"Pause": "Туктау",
"Clear": "Чистарту",
"Test": "Тест",
"Test All": "Барчасын тестлау",
"Create Test": "Тест булдыру",
"Edit Test": "Тестны үзгәртү",
"Icon": "Иконка",
"Test URL": "Тест URL-ы",
"Settings": "Көйләүләр",
"System Setting": "Система көйләүләре",
"Tun Mode": "Tun режимы (виртуаль челтәр адаптеры)",
"Reset to Default": "Башлангычка кайтару",
"Tun Mode Info": "Tun режимы бөтен системаның трафигын тотып ала. Аны кабызган очракта системалы проксины аерым кабызу таләп ителми.",
"Stack": "Стек",
"System and Mixed Can Only be Used in Service Mode": "Система яки кушылган режимнар бары тик сервис режимында гына активлаштырыла ала",
"Device": "Җайланма исеме",
"Auto Route": "Авто-маршрутлау",
"Strict Route": "Катгый маршрутлау",
"Auto Detect Interface": "Интерфейсны автоматик ачыклау",
"DNS Hijack": "DNS'ны үзгәртеп тоту (hijack)",
"MTU": "MTU (макс. тапшыру берәмлеге)",
"Service Mode": "Сервис режимы",
"Service Mode Info": "Tun режимын кабызганчы сервис режимын урнаштыру сорала. Сервис буларак эшләтелгән Clash ядросына виртуаль челтәр адаптеры (TUN) куллану рөхсәт ителә.",
"Current State": "Агымдагы торыш",
"pending": "Көтә",
"installed": "Урнаштырылган",
"uninstall": "Урнаштырылмаган",
"active": "Актив",
"unknown": "Билгесез",
"Information: Please make sure that the Clash Verge Service is installed and enabled": "Игътибар: Clash Verge сервисы урнаштырылган һәм активлаштырылган булырга тиеш",
"Install": "Урнаштыру",
"Uninstall": "Салдыру",
"Disable Service Mode": "Сервис режимын сүндерү",
"System Proxy": "Системалы прокси",
"System Proxy Info": "Системалы прокси көйләүләрен үзгәртү рөхсәтен бирегез. Әгәр рөхсәт алу мөмкин түгел икән, прокси көйләүләрен кулдан үзгәртегез",
"System Proxy Setting": "Системалы прокси көйләүләре",
"Current System Proxy": "Агымдагы системалы прокси",
"Enable status": "Активлаштыру статусы",
"Enabled": "Кушылган",
"Disabled": "Сүнгән",
"Server Addr": "Сервер адресы",
"Not available": "Мөмкин түгел",
"Proxy Guard": "Прокси саклаучы",
"Proxy Guard Info": "Системалы прокси көйләүләрен чит программа үзгәртмәсен өчен шушы функцияне кабызыгыз",
"Guard Duration": "Саклау вакыты",
"Always use Default Bypass": "Һәрвакыт төп Bypass-ны куллану",
"Proxy Bypass": "Проксины әйләнеп узу:",
"Bypass": "Әйләнеп узу:",
"Use PAC Mode": "PAC режимын куллану",
"PAC Script Content": "PAC скрипты эчтәлеге",
"PAC URL": "PAC адресы",
"Auto Launch": "Автостарт",
"Silent Start": "Фон режимында башлау",
"Silent Start Info": "Программаны фоновый режимда, тәрәзәсез эшләтеп җибәрү",
"TG Channel": "Telegram каналы",
"Manual": "Документация",
"Github Repo": "GitHub репозиториясе",
"Clash Setting": "Clash көйләүләре",
"Allow Lan": "Локаль челтәргә рөхсәт",
"Network Interface": "Челтәр интерфейсы",
"Ip Address": "IP адресы",
"Mac Address": "MAC адресы",
"IPv6": "IPv6",
"Unified Delay": "Бердәм задержка",
"Unified Delay Info": "Бердәм задержка актив булганда, төрле типтагы узеллар өчен икеләтә тест башкарыла, TCP установканы раслау аермаларын тигезләү максатында",
"Log Level": "Лог дәрәҗәсе",
"Log Level Info": "Бу фәкать сервис режимында эшләгән вакытта системалы журнал файлларына кагыла",
"Port Config": "Порт көйләүләре",
"Random Port": "Очраклы порт",
"Mixed Port": "Катнаш прокси порты",
"Socks Port": "Socks прокси порты",
"Http Port": "HTTP(s) прокси порты",
"Redir Port": "Redir — үтә күренмәле прокси порты",
"Tproxy Port": "Tproxy — үтә күренмәле прокси порты",
"External": "Тышкы",
"External Controller": "Тышкы контроллер адресы",
"Core Secret": "Серсүз",
"Recommended": "Тавсия ителә",
"Open URL": "URL ачарга",
"Replace host, port, secret with %host, %port, %secret": "Хост, порт, серсүзне %host, %port, %secret белән алмаштырыгыз",
"Support %host, %port, %secret": "%host, %port, %secret макросларын хуплау",
"Clash Core": "Clash ядросы",
"Upgrade": "Яңарту",
"Restart": "Перезапуск",
"Release Version": "Рәсми версия",
"Alpha Version": "Альфа-версия",
"Please Enable Service Mode": "Башта сервис режимын кабызырга кирәк",
"Please enter your root password": "root паролен языгыз",
"Grant": "Рөхсәт бирү",
"Open UWP tool": "UWP инструментын ачу",
"Open UWP tool Info": "Windows 8'дән башлап UWP кушымталары (Microsoft Store кебек) локаль хосттагы челтәр хезмәтләренә турыдан-туры тоташа алмый. Бу инструмент әлеге чикләүне әйләнеп узарга ярдәм итә",
"Update GeoData": "GeoData яңарту",
"Verge Setting": "Verge көйләүләре",
"Language": "Тел",
"Theme Mode": "Теманың режимы",
"theme.light": "Якты",
"theme.dark": "Караңгы",
"theme.system": "Система",
"Tray Click Event": "Трейдагы басу вакыйгасы",
"Show Main Window": "Төп тәрәзәне күрсәтү",
"Copy Env Type": "Env төрен күчереп алу",
"Copy Success": "Күчерелде",
"Start Page": "Баш бит",
"Startup Script": "Башлану скрипты",
"Browse": "Карау",
"Theme Setting": "Тема көйләүләре",
"Primary Color": "Төп төс",
"Secondary Color": "Икенче төс",
"Primary Text Color": "Төп текст төсе",
"Secondary Text Color": "Икенче текст төсе",
"Info Color": "Мәгълүмат төсе",
"Warning Color": "Кисәтү төсе",
"Error Color": "Хата төсе",
"Success Color": "Уңыш төсе",
"Font Family": "Шрифтлар гаиләсе",
"CSS Injection": "CSS кертү",
"Layout Setting": "Расположение көйләүләре",
"Traffic Graph": "Трафик графигы",
"Memory Usage": "Хәтер куллану",
"Memory Cleanup": "Хәтерне чистарту өчен басыгыз",
"Proxy Group Icon": "Прокси төркеме иконкасы",
"Nav Icon": "Навигация иконкасы",
"Monochrome": "Монохром",
"Colorful": "Төсле",
"Tray Icon": "Трей иконкасы",
"Common Tray Icon": "Гомуми трей иконкасы",
"System Proxy Tray Icon": "Системалы прокси иконкасы",
"Tun Tray Icon": "Tun (виртуаль адаптер) иконкасы",
"Miscellaneous": "Өстәмә көйләүләр",
"App Log Level": "Кушымта журналы дәрәҗәсе",
"Auto Close Connections": "Тоташуларны автоматик ябу",
"Auto Close Connections Info": "Прокси төркеме яисә режимын үзгәрткәндә актив тоташуларны өзү",
"Auto Check Update": "Яңартуларны автоматик тикшерү",
"Enable Builtin Enhanced": "Эчке камилләштерүне кабызу",
"Enable Builtin Enhanced Info": "Конфигурация файлы белән туры килә торган өстәмә оптимизация",
"Proxy Layout Columns": "Прокси күрсәтү баганалары саны",
"Auto Columns": "Авто баганалар",
"Auto Log Clean": "Логларны автоматик чистарту",
"Never Clean": "Беркайчан чистартмаска",
"Retain _n Days": "{{n}} көн саклау",
"Default Latency Test": "Тоткарлануны тикшерү сылтамасы (defaults)",
"Default Latency Test Info": "Бу фәкать клиентның HTTP сораулары тесты өчен кулланыла, конфигурация файлына йогынты ясамый",
"Default Latency Timeout": "Тоткарлануның стандарт таймауты",
"Hotkey Setting": "Клавиатура төймәләре (hotkey) көйләүләре",
"Enable Global Hotkey": "Глобаль Хоткейны кушу",
"open_or_close_dashboard": "Панельне ачу/ябу",
"clash_mode_rule": "Кагыйдәләр режимы",
"clash_mode_global": "Глобаль режим",
"clash_mode_direct": "Туры режим",
"toggle_system_proxy": "Системалы проксины кабызу/сүндерү",
"toggle_tun_mode": "Tun режимын кабызу/сүндерү",
"Backup Setting": "Резерв копия көйләүләре",
"Backup Setting Info": "WebDAV аша конфигурация файлын саклауны хуплый",
"Runtime Config": "Агымдагы конфигурация",
"Open Conf Dir": "Кушымта папкасын ачу",
"Open Conf Dir Info": "Әгәр программада хаталар чыкса, бу папкадагы файлларны саклап калыгыз да, аннары барысын да бетереп, программаны яңадан башлагыз",
"Open Core Dir": "Ядро сакланган папканы ачу",
"Open Logs Dir": "Логлар папкасын ачу",
"Check for Updates": "Яңартуларны тикшерү",
"Go to Release Page": "Релизлар битенә күчү",
"Portable Updater Error": "Портатив версиядә кушымта эчендә яңарту хупланмый, кулдан төшереп алыгыз",
"Break Change Update Error": "Бу зур яңарту, ул кушымта эчендә яңартылмый. Борып алып ташлап, яңадан урнаштыру сорала.",
"Open Dev Tools": "Разработчик коралларын ачу",
"Exit": "Чыгу",
"Verge Version": "Verge версиясе",
"ReadOnly": "Уку режимы гына",
"ReadOnlyMessage": "Уку режимында үзгәртү мөмкин түгел",
"Filter": "Фильтр",
"Filter conditions": "Фильтр шартлары",
"Match Case": "Регистрны исәпкә алу",
"Match Whole Word": "Сүзнең тулы туры килүе",
"Use Regular Expression": "Регуляр выражениеләр куллану",
"Profile Imported Successfully": "Профиль уңышлы импортланды",
"Profile Switched": "Профиль алмаштырылды",
"Profile Reactivated": "Профиль яңадан активлаштырылды",
"Only YAML Files Supported": "Фәкать YAML-файллар гына хуплана",
"Settings Applied": "Көйләүләр кулланылды",
"Service Installed Successfully": "Сервис уңышлы урнаштырылды",
"Service Uninstalled Successfully": "Сервис уңышлы салдырылды",
"Proxy Daemon Duration Cannot be Less than 1 Second": "Прокси-демон эш вакыты 1 секундтан ким була алмый",
"Invalid Bypass Format": "Дөрес булмаган Bypass форматы",
"Clash Port Modified": "Clash порты үзгәртелде",
"Port Conflict": "Порт конфликтлары",
"Restart Application to Apply Modifications": "Үзгәрешләрне куллану өчен кушымтаны яңадан ачарга кирәк",
"External Controller Address Modified": "Тышкы контроллер адресы үзгәртелде",
"Permissions Granted Successfully for _clash Core": "{{core}} ядросы өчен рөхсәтләр бирелде",
"Core Version Updated": "Ядро версиясе яңартылды",
"Clash Core Restarted": "Clash ядросы яңадан башланды",
"GeoData Updated": "GeoData яңартылды",
"Currently on the Latest Version": "Сездә иң соңгы версия урнаштырылган",
"Import subscription successful": "Подписка уңышлы импортланды",
"WebDAV Server URL": "WebDAV сервер URL-ы (http(s)://)",
"Username": "Кулланучы исеме",
"Password": "Пароль",
"Backup": "Резерв копия",
"Filename": "Файл исеме",
"Actions": "Гамәлләр",
"Restore": "Кайтару",
"No Backups": "Резерв копияләр юк",
"WebDAV URL Required": "WebDAV адресы буш булырга тиеш түгел",
"Invalid WebDAV URL": "WebDAV адресы дөрес түгел",
"Username Required": "Кулланучы исеме буш булмаска тиеш",
"Password Required": "Пароль буш булмаска тиеш",
"Failed to Fetch Backups": "Резерв копия файлларын алуда хата",
"WebDAV Config Saved": "WebDAV көйләүләре сакланды",
"WebDAV Config Save Failed": "WebDAV көйләүләрен саклап булмады: {{error}}",
"Backup Created": "Резерв копия уңышлы ясалды",
"Backup Failed": "Резерв копия хата белән төгәлләнде: {{error}}",
"Delete Backup": "Резерв копияне бетерү",
"Restore Backup": "Резерв копияне кайтару",
"Backup Time": "Резерв копия вакыты",
"Confirm to delete this backup file?": "Бу резерв копия файлын бетерергә телисезме?",
"Confirm to restore this backup file?": "Бу резерв копия файлын кире кайтарырга телисезме?",
"Restore Success, App will restart in 1s": "Уңышлы кайтарылды, кушымта 1 секундтан яңадан башланачак",
"Failed to fetch backup files": "Резерв копия файлларын алуда хата",
"Profile": "Профиль",
"Help": "Ярдәм",
"About": "Турында",
"Theme": "Тема",
"Main Window": "Төп тәрәзә",
"Group Icon": "Төркем иконкасы",
"Menu Icon": "Меню иконкасы",
"PAC File": "PAC файлы",
"Web UI": "Веб-интерфейс",
"Hotkeys": "Кызу төймәләр",
"Verge Mixed Port": "Verge катнаш порты",
"Verge Socks Port": "Verge Socks порты",
"Verge Redir Port": "Verge кире юнәлтү порты",
"Verge Tproxy Port": "Verge Tproxy порты",
"Verge Port": "Verge порты",
"Verge HTTP Enabled": "Verge HTTP кушылган",
"WebDAV URL": "WebDAV URL",
"WebDAV Username": "WebDAV кулланучы исеме",
"WebDAV Password": "WebDAV серсүзе",
"Dashboard": "Панель",
"Restart App": "Кушымтаны яңадан ачу",
"Restart Clash Core": "Clash ядрони яңадан башлап ачу",
"TUN Mode": "TUN режимы",
"Copy Env": "Env күчереп алу",
"Conf Dir": "Кушымта папкасы",
"Core Dir": "Ядро папкасы",
"Logs Dir": "Логлар папкасы",
"Open Dir": "Папканы ачу",
"More": "Башҡа",
"Rule Mode": "Кагыйдә режимы",
"Global Mode": "Глобаль режим",
"Direct Mode": "Туры режим",
"Enable Tray Speed": "Трей скоростьне үстерү",
"Lite Mode": "Җиңел Режим",
"Lite Mode Info": "GUI-ны ябыгыз һәм бары тик төшне генә эшләтеп калдырыгыз",
"Config Validation Failed": "Язылу көйләү тикшерүе уңышсыз, көйләү файлын тикшерегез, үзгәрешләр кире кайтарылды, хата:",
"Boot Config Validation Failed": "Йөкләү вакытында көйләү тикшерүе уңышсыз, стандарт көйләү кулланылды, көйләү файлын тикшерегез, хата:",
"Core Change Config Validation Failed": "Ядро алыштырганда көйләү тикшерүе уңышсыз, стандарт көйләү кулланылды, көйләү файлын тикшерегез, хата:",
"Config Validation Process Terminated": "Тикшерү процессы туктатылды",
"Script Syntax Error": "Скрипт синтаксик хатасы, үзгәрешләр кире кайтарылды",
"Script Missing Main": "Скрипт хатасы, үзгәрешләр кире кайтарылды",
"File Not Found": "Файл табылмады, үзгәрешләр кире кайтарылды",
"Script File Error": "Скрипт файлы хатасы, үзгәрешләр кире кайтарылды",
"Core Changed Successfully": "Ядро уңышлы алыштырылды",
"Failed to Change Core": "Ядро алыштыру уңышсыз булды",
"Verge Basic Setting": "Verge Төп көйләүләр",
"Verge Advanced Setting": "Verge Киңәйтелгән көйләүләр"
}
+113 -51
View File
@@ -1,7 +1,7 @@
{
"millis": "毫秒",
"mins": "分钟",
"seconds": "秒",
"mins": "分钟",
"Back": "返回",
"Close": "关闭",
"Cancel": "取消",
@@ -55,12 +55,12 @@
"Create Profile": "新建配置",
"Edit Profile": "编辑配置",
"Edit Proxies": "编辑节点",
"Use newlines for multiple uri": "多条URI请使用换行分隔(支持Base64编码)",
"Use newlines for multiple uri": "多条 URI 请使用换行分隔(支持 Base64 编码)",
"Edit Rules": "编辑规则",
"Rule Type": "规则类型",
"Rule Content": "规则内容",
"Proxy Policy": "代理策略",
"No Resolve": "跳过DNS解析",
"No Resolve": "跳过 DNS 解析",
"Prepend Rule": "添加前置规则",
"Append Rule": "添加后置规则",
"Prepend Group": "添加前置代理组",
@@ -75,26 +75,26 @@
"DOMAIN-SUFFIX": "匹配域名后缀",
"DOMAIN-KEYWORD": "匹配域名关键字",
"DOMAIN-REGEX": "匹配域名正则表达式",
"GEOSITE": "匹配Geosite内的域名",
"GEOIP": "匹配IP所属国家代码",
"SRC-GEOIP": "匹配来源IP所属国家代码",
"IP-ASN": "匹配IP所属ASN",
"SRC-IP-ASN": "匹配来源IP所属ASN",
"IP-CIDR": "匹配IP地址范围",
"IP-CIDR6": "匹配IP地址范围",
"SRC-IP-CIDR": "匹配来源IP地址范围",
"IP-SUFFIX": "匹配IP后缀范围",
"SRC-IP-SUFFIX": "匹配来源IP后缀范围",
"GEOSITE": "匹配 Geosite 内的域名",
"GEOIP": "匹配 IP 所属国家代码",
"SRC-GEOIP": "匹配来源 IP 所属国家代码",
"IP-ASN": "匹配 IP 所属 ASN",
"SRC-IP-ASN": "匹配来源 IP 所属 ASN",
"IP-CIDR": "匹配 IP 地址范围",
"IP-CIDR6": "匹配 IP 地址范围",
"SRC-IP-CIDR": "匹配来源 IP 地址范围",
"IP-SUFFIX": "匹配 IP 后缀范围",
"SRC-IP-SUFFIX": "匹配来源 IP 后缀范围",
"SRC-PORT": "匹配请求来源端口范围",
"DST-PORT": "匹配请求目标端口范围",
"IN-PORT": "匹配入站端口",
"DSCP": "DSCP标记(仅限tproxy udp入站)",
"PROCESS-NAME": "匹配进程名称(Android包名)",
"DSCP": "DSCP标记(仅限 TPROXY UDP 入站)",
"PROCESS-NAME": "匹配进程名称(Android 包名)",
"PROCESS-PATH": "匹配完整进程路径",
"PROCESS-NAME-REGEX": "正则匹配完整进程名称(Android包名)",
"PROCESS-NAME-REGEX": "正则匹配完整进程名称(Android 包名)",
"PROCESS-PATH-REGEX": "正则匹配完整进程路径",
"NETWORK": "匹配传输协议(tcp/udp)",
"UID": "匹配Linux USER ID",
"NETWORK": "匹配传输协议 (TCP/UDP)",
"UID": "匹配 Linux USER ID",
"IN-TYPE": "匹配入站类型",
"IN-USER": "匹配入站用户名",
"IN-NAME": "匹配入站名称",
@@ -131,13 +131,13 @@
"Include All Proxies": "引入所有出站代理",
"Exclude Filter": "排除节点",
"Exclude Type": "排除节点类型",
"Disable UDP": "禁用UDP",
"Disable UDP": "禁用 UDP",
"Hidden": "隐藏代理组",
"Group Name Required": "代理组名称不能为空",
"Group Name Already Exists": "代理组名称已存在",
"Extend Config": "扩展配置",
"Extend Config": "扩展覆写配置",
"Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置",
"Global Merge": "全局扩展覆写配置",
"Global Script": "全局扩展脚本",
"Type": "类型",
"Name": "名称",
@@ -147,7 +147,7 @@
"Choose File": "选择文件",
"Use System Proxy": "使用系统代理更新",
"Use Clash Proxy": "使用内核代理更新",
"Accept Invalid Certs (Danger)": "允许无效证书 (危险)",
"Accept Invalid Certs (Danger)": "允许无效证书(危险)",
"Refresh": "刷新",
"Home": "首页",
"Select": "使用",
@@ -155,7 +155,7 @@
"Edit File": "编辑文件",
"Open File": "打开文件",
"Update": "更新",
"Update(Proxy)": "更新(代理)",
"Update(Proxy)": "更新(代理)",
"Confirm deletion": "确认删除",
"This operation is not reversible": "此操作不可逆",
"Script Console": "脚本控制台输出",
@@ -178,7 +178,8 @@
"Process": "进程",
"Time": "连接时间",
"Source": "源地址",
"Destination IP": "目标地址",
"Destination": "目标地址",
"DestinationPort": "目标端口",
"Close Connection": "关闭连接",
"Rules": "规则",
"Rule Provider": "规则集合",
@@ -193,19 +194,19 @@
"Test URL": "测试地址",
"Settings": "设置",
"System Setting": "系统设置",
"Tun Mode": "Tun(虚拟网卡)模式",
"Tun Mode": "TUN(虚拟网卡)模式",
"Reset to Default": "重置为默认值",
"Tun Mode Info": "Tun(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
"Stack": "Tun 模式堆栈",
"Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
"Stack": "TUN 模式堆栈",
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用",
"Device": "Tun 网卡名称",
"Device": "TUN 网卡名称",
"Auto Route": "自动设置全局路由",
"Strict Route": "严格路由",
"Auto Detect Interface": "自动选择流量出口接口",
"DNS Hijack": "DNS 劫持",
"MTU": "最大传输单元",
"Service Mode": "服务模式",
"Service Mode Info": "启用TUN模式前请安装服务模式,该服务启动的内核进程可获得安装虚拟网卡(TUN模式)的权限",
"Service Mode Info": "启用 TUN 模式前请安装服务模式,该服务启动的内核进程可获得安装虚拟网卡(TUN 模式)的权限",
"Current State": "当前状态",
"pending": "等待中",
"installed": "已安装",
@@ -229,11 +230,12 @@
"Proxy Guard Info": "开启以防止其他软件修改操作系统的代理设置",
"Guard Duration": "代理守卫间隔",
"Always use Default Bypass": "始终使用默认绕过",
"Use Bypass Check": "启用代理绕过检查",
"Proxy Bypass": "代理绕过设置:",
"Bypass": "当前绕过:",
"Use PAC Mode": "使用PAC模式",
"PAC Script Content": "PAC脚本内容",
"PAC URL": "PAC地址:",
"Use PAC Mode": "使用 PAC 模式",
"PAC Script Content": "PAC 脚本内容",
"PAC URL": "PAC 地址:",
"Auto Launch": "开机自启",
"Silent Start": "静默启动",
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
@@ -249,14 +251,14 @@
"Unified Delay": "统一延迟",
"Unified Delay Info": "开启统一延迟时,会进行两次延迟测试,以消除连接握手等带来的不同类型节点的延迟差异",
"Log Level": "日志等级",
"Log Level Info": "仅对日志目录Service文件夹下的内核日志文件生效",
"Log Level Info": "仅对日志目录 Service 文件夹下的内核日志文件生效",
"Port Config": "端口设置",
"Random Port": "随机端口",
"Mixed Port": "混合代理端口",
"Socks Port": "Socks代理端口",
"Http Port": "Http(s)代理端口",
"Redir Port": "Redir透明代理端口",
"Tproxy Port": "Tproxy透明代理端口",
"Socks Port": "SOCKS 代理端口",
"Http Port": "HTTP(S) 代理端口",
"Redir Port": "Redir 透明代理端口",
"TPROXY Port": "TPROXY 透明代理端口",
"External": "外部控制",
"External Controller": "外部控制器监听地址",
"Core Secret": "API 访问密钥",
@@ -273,7 +275,7 @@
"Please enter your root password": "请输入您的 root 密码",
"Grant": "授权",
"Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
"Open UWP tool Info": "Windows 8 开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
"Update GeoData": "更新 GeoData",
"Verge Setting": "Verge 设置",
"Language": "语言设置",
@@ -310,9 +312,9 @@
"Tray Icon": "托盘图标",
"Common Tray Icon": "常规托盘图标",
"System Proxy Tray Icon": "系统代理托盘图标",
"Tun Tray Icon": "Tun 模式托盘图标",
"Tun Tray Icon": "TUN 模式托盘图标",
"Miscellaneous": "杂项设置",
"App Log Level": "App日志等级",
"App Log Level": "应用日志等级",
"Auto Close Connections": "自动关闭连接",
"Auto Close Connections Info": "当代理组选中节点或代理模式变动时,关闭已建立的连接",
"Auto Check Update": "自动检查更新",
@@ -322,21 +324,23 @@
"Auto Columns": "自动列数",
"Auto Log Clean": "自动清理日志",
"Never Clean": "不清理",
"Retain _n Days": "保留{{n}}天",
"Retain _n Days": "保留 {{n}} 天",
"Default Latency Test": "默认测试链接",
"Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响",
"Default Latency Timeout": "测试超时时间",
"Hotkey Setting": "热键设置",
"Enable Global Hotkey": "启用全局热键",
"open_or_close_dashboard": "打开/关闭面板",
"clash_mode_rule": "规则模式",
"clash_mode_global": "全局模式",
"clash_mode_direct": "直连模式",
"toggle_system_proxy": "打开/关闭系统代理",
"toggle_tun_mode": "打开/关闭 Tun 模式",
"toggle_tun_mode": "打开/关闭 TUN 模式",
"Backup Setting": "备份设置",
"Backup Setting Info": "支持WebDAV备份配置文件",
"Backup Setting Info": "支持 WebDAV 备份配置文件",
"Runtime Config": "当前配置",
"Open Conf Dir": "配置目录",
"Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
"Open Core Dir": "内核目录",
"Open Logs Dir": "日志目录",
"Check for Updates": "检查更新",
@@ -354,27 +358,25 @@
"Match Whole Word": "全字匹配",
"Use Regular Expression": "使用正则表达式",
"Profile Imported Successfully": "导入订阅成功",
"Clash Config Updated": "Clash 配置已更新",
"Profile Switched": "订阅已切换",
"Profile Reactivated": "订阅已激活",
"Only YAML Files Supported": "仅支持 YAML 文件",
"Settings Applied": "设置已应用",
"Service Installed Successfully": "已成功安装服务",
"Service Uninstalled Successfully": "已成功卸载服务",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于1秒",
"Proxy Daemon Duration Cannot be Less than 1 Second": "代理守护间隔时间不得低于 1 秒",
"Invalid Bypass Format": "无效的代理绕过格式",
"Clash Port Modified": "Clash 端口已修改",
"Port Conflict": "端口冲突",
"Restart Application to Apply Modifications": "重启Verge以应用修改",
"Restart Application to Apply Modifications": "重启 Verge 以应用修改",
"External Controller Address Modified": "外部控制器监听地址已修改",
"Permissions Granted Successfully for _clash Core": "{{core}} 内核授权成功",
"Core Version Updated": "内核版本已更新",
"Clash Core Restarted": "已重启 Clash 内核",
"Switched to _clash Core": "已切换至 {{core}} 内核",
"GeoData Updated": "已更新 GeoData",
"Currently on the Latest Version": "当前已是最新版本",
"Import Subscription Successful": "导入订阅成功",
"WebDAV Server URL": "WebDAV服务器地址 http(s)://",
"WebDAV Server URL": "WebDAV 服务器地址 http(s)://",
"Username": "用户名",
"Password": "密码",
"Backup": "备份",
@@ -395,7 +397,67 @@
"Restore Backup": "恢复备份",
"Backup Time": "备份时间",
"Confirm to delete this backup file?": "确认删除此备份文件吗?",
"Confirm to restore this backup file?": "确认恢复此 份文件吗?",
"Restore Success, App will restart in 1s": "恢复成功,应用将在1秒后重启",
"Failed to fetch backup files": "获取备份文件失败"
"Confirm to restore this backup file?": "确认恢复此份文件吗?",
"Restore Success, App will restart in 1s": "恢复成功,应用将在 1 秒后重启",
"Failed to fetch backup files": "获取备份文件失败",
"Profile": "配置",
"Help": "帮助",
"About": "关于",
"Theme": "主题",
"Main Window": "主窗口",
"Group Icon": "分组图标",
"Menu Icon": "菜单图标",
"PAC File": "PAC 文件",
"Web UI": "网页界面",
"Hotkeys": "快捷键",
"Verge Mixed Port": "Verge 混合端口",
"Verge Socks Port": "Verge SOCKS 端口",
"Verge Redir Port": "Verge 重定向端口",
"Verge Tproxy Port": "Verge 透明代理端口",
"Verge Port": "Verge 端口",
"Verge HTTP Enabled": "Verge HTTP 已启用",
"WebDAV URL": "WebDAV 地址",
"WebDAV Username": "WebDAV 用户名",
"WebDAV Password": "WebDAV 密码",
"Dashboard": "仪表板",
"Restart App": "重启应用",
"Restart Clash Core": "重启 Clash 核心",
"TUN Mode": "TUN 模式",
"Copy Env": "复制环境变量",
"Conf Dir": "配置目录",
"Core Dir": "核心目录",
"Logs Dir": "日志目录",
"Open Dir": "打开目录",
"More": "更多",
"Rule Mode": "规则模式",
"Global Mode": "全局模式",
"Direct Mode": "直连模式",
"Enable Tray Speed": "启用托盘速率",
"Lite Mode": "轻量模式",
"Lite Mode Info": "关闭GUI界面,仅保留内核运行",
"Config Validation Failed": "订阅配置校验失败,请检查订阅配置文件,变更已撤销,错误详情:",
"Boot Config Validation Failed": "启动订阅配置校验失败,已使用默认配置启动;请检查订阅配置文件,错误详情:",
"Core Change Config Validation Failed": "切换内核时配置校验失败,已使用默认配置启动;请检查订阅配置文件,错误详情:",
"Config Validation Process Terminated": "验证进程被终止",
"Script Syntax Error": "脚本语法错误,变更已撤销",
"Script Missing Main": "脚本错误,变更已撤销",
"File Not Found": "文件丢失,变更已撤销",
"Script File Error": "脚本文件错误,变更已撤销",
"Core Changed Successfully": "内核切换成功",
"Failed to Change Core": "无法切换内核",
"YAML Syntax Error": "YAML语法错误,变更已撤销",
"YAML Read Error": "YAML读取错误,变更已撤销",
"YAML Mapping Error": "YAML映射错误,变更已撤销",
"YAML Key Error": "YAML键错误,变更已撤销",
"YAML Error": "YAML错误,变更已撤销",
"Merge File Syntax Error": "覆写文件语法错误,变更已撤销",
"Merge File Mapping Error": "覆写文件映射错误,变更已撤销",
"Merge File Key Error": "覆写文件键错误,变更已撤销",
"Merge File Error": "覆写文件错误,变更已撤销",
"Validate YAML File": "验证YAML文件",
"Validate Merge File": "验证覆写文件",
"Validation Success": "验证成功",
"Validation Failed": "验证失败",
"Verge Basic Setting": "Verge 基础设置",
"Verge Advanced Setting": "Verge 高级设置"
}
+166 -70
View File
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr";
import { useEffect } from "react";
import { useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
@@ -27,6 +27,7 @@ import { getPortableFlag } from "@/services/cmds";
import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group";
import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
const appWindow = getCurrentWebviewWindow();
export let portableFlag = false;
@@ -35,76 +36,178 @@ dayjs.extend(relativeTime);
const OS = getSystem();
// 通知处理函数
const handleNoticeMessage = (
status: string,
msg: string,
t: (key: string) => string,
navigate: (path: string, options?: any) => void,
) => {
console.log("[通知监听] 收到消息:", status, msg);
switch (status) {
case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } });
Notice.success(t("Import Subscription Successful"));
break;
case "import_sub_url::error":
navigate("/profile");
Notice.error(msg);
break;
case "set_config::error":
Notice.error(msg);
break;
case "config_validate::boot_error":
Notice.error(`${t("Boot Config Validation Failed")} ${msg}`);
break;
case "config_validate::core_change":
Notice.error(`${t("Core Change Config Validation Failed")} ${msg}`);
break;
case "config_validate::error":
Notice.error(`${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::process_terminated":
Notice.error(t("Config Validation Process Terminated"));
break;
case "config_validate::stdout_error":
Notice.error(`${t("Config Validation Failed")} ${msg}`);
break;
case "config_validate::script_error":
Notice.error(`${t("Script File Error")} ${msg}`);
break;
case "config_validate::script_syntax_error":
Notice.error(`${t("Script Syntax Error")} ${msg}`);
break;
case "config_validate::script_missing_main":
Notice.error(`${t("Script Missing Main")} ${msg}`);
break;
case "config_validate::file_not_found":
Notice.error(`${t("File Not Found")} ${msg}`);
break;
case "config_validate::yaml_syntax_error":
Notice.error(`${t("YAML Syntax Error")} ${msg}`);
break;
case "config_validate::yaml_read_error":
Notice.error(`${t("YAML Read Error")} ${msg}`);
break;
case "config_validate::yaml_mapping_error":
Notice.error(`${t("YAML Mapping Error")} ${msg}`);
break;
case "config_validate::yaml_key_error":
Notice.error(`${t("YAML Key Error")} ${msg}`);
break;
case "config_validate::yaml_error":
Notice.error(`${t("YAML Error")} ${msg}`);
break;
case "config_validate::merge_syntax_error":
Notice.error(`${t("Merge File Syntax Error")} ${msg}`);
break;
case "config_validate::merge_mapping_error":
Notice.error(`${t("Merge File Mapping Error")} ${msg}`);
break;
case "config_validate::merge_key_error":
Notice.error(`${t("Merge File Key Error")} ${msg}`);
break;
case "config_validate::merge_error":
Notice.error(`${t("Merge File Error")} ${msg}`);
break;
case "config_core::change_success":
Notice.success(`${t("Core Changed Successfully")}: ${msg}`);
break;
case "config_core::change_error":
Notice.error(`${t("Failed to Change Core")}: ${msg}`);
break;
}
};
const Layout = () => {
const mode = useThemeMode();
const isDark = mode === "light" ? false : true;
const { t } = useTranslation();
const { theme } = useCustomTheme();
const { verge } = useVerge();
const { language, start_page } = verge || {};
const { language, start_page } = verge ?? {};
const navigate = useNavigate();
const location = useLocation();
const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen();
if (!routersEles) return null;
setupCloseListener();
const handleNotice = useCallback(
(payload: [string, string]) => {
const [status, msg] = payload;
handleNoticeMessage(status, msg, t, navigate);
},
[t, navigate],
);
// 设置监听器
useEffect(() => {
addListener("verge://refresh-clash-config", async () => {
// the clash info may be updated
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
});
const listeners = [
// 配置更新监听
addListener("verge://refresh-clash-config", async () => {
await getAxios(true);
mutate("getProxies");
mutate("getVersion");
mutate("getClashConfig");
mutate("getProxyProviders");
}),
// update the verge config
addListener("verge://refresh-verge-config", () => mutate("getVergeConfig"));
// verge 配置更新监听
addListener("verge://refresh-verge-config", () =>
mutate("getVergeConfig"),
),
// 设置提示监听
addListener("verge://notice-message", ({ payload }) => {
const [status, msg] = payload as [string, string];
switch (status) {
case "import_sub_url::ok":
navigate("/profile", { state: { current: msg } });
// 通知消息监听
addListener("verge://notice-message", ({ payload }) =>
handleNotice(payload as [string, string]),
),
];
Notice.success(t("Import Subscription Successful"));
break;
case "import_sub_url::error":
navigate("/profile");
Notice.error(msg);
break;
case "set_config::ok":
Notice.success(t("Clash Config Updated"));
break;
case "set_config::error":
Notice.error(msg);
break;
default:
break;
}
});
// 设置窗口显示/隐藏监听
const setupWindowListeners = async () => {
const [hideUnlisten, showUnlisten] = await Promise.all([
listen("verge://hide-window", () => appWindow.hide()),
listen("verge://show-window", () => appWindow.show()),
]);
setTimeout(async () => {
portableFlag = await getPortableFlag();
await appWindow.unminimize();
await appWindow.show();
await appWindow.setFocus();
}, 50);
}, []);
return () => {
hideUnlisten();
showUnlisten();
};
};
// 初始化
setupCloseListener();
const cleanupWindow = setupWindowListeners();
// 清理函数
return () => {
// 清理主要监听器
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten());
}
});
// 清理窗口监听器
cleanupWindow.then((cleanup) => cleanup());
};
}, [handleNotice]);
// 语言和起始页设置
useEffect(() => {
if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language);
i18next.changeLanguage(language);
}
}, [language]);
useEffect(() => {
if (start_page) {
navigate(start_page);
navigate(start_page, { replace: true });
}
}, [language, start_page]);
}, [start_page]);
if (!routersEles) return null;
return (
<SWRConfig value={{ errorRetryCount: 3 }}>
@@ -114,29 +217,24 @@ const Layout = () => {
elevation={0}
className={`${OS} layout`}
onContextMenu={(e) => {
// only prevent it on Windows
const validList = ["input", "textarea"];
const target = e.currentTarget;
if (
OS === "windows" &&
!(
validList.includes(target.tagName.toLowerCase()) ||
target.isContentEditable
)
!["input", "textarea"].includes(
e.currentTarget.tagName.toLowerCase(),
) &&
!e.currentTarget.isContentEditable
) {
e.preventDefault();
}
}}
sx={[
({ palette }) => ({
bgcolor: palette.background.paper,
}),
({ palette }) => ({ bgcolor: palette.background.paper }),
OS === "linux"
? {
borderRadius: "8px",
border: "2px solid var(--divider-color)",
width: "calc(100vw - 4px)",
height: "calc(100vh - 4px)",
border: "1px solid var(--divider-color)",
width: "calc(100vw - 0px)",
height: "calc(100vh - 0px)",
}
: {},
]}
@@ -163,7 +261,7 @@ const Layout = () => {
/>
<LogoSvg fill={isDark ? "white" : "black"} />
</div>
{<UpdateButton className="the-newbtn" />}
<UpdateButton className="the-newbtn" />
</div>
<List className="the-menu">
@@ -184,16 +282,14 @@ const Layout = () => {
</div>
<div className="layout__right">
{
<div className="the-bar">
<div
className="the-dragbar"
data-tauri-drag-region="true"
style={{ width: "100%" }}
></div>
{OS !== "macos" && <LayoutControl />}
</div>
}
<div className="the-bar">
<div
className="the-dragbar"
data-tauri-drag-region="true"
style={{ width: "100%" }}
/>
{OS !== "macos" && <LayoutControl />}
</div>
<TransitionGroup className="the-content">
<CSSTransition
+138 -100
View File
@@ -1,9 +1,14 @@
import { useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState, useCallback } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next";
import { TableChartRounded, TableRowsRounded } from "@mui/icons-material";
import {
TableChartRounded,
TableRowsRounded,
PlayCircleOutlineRounded,
PauseCircleOutlineRounded,
} from "@mui/icons-material";
import { closeAllConnections } from "@/services/api";
import { useConnectionSetting } from "@/services/states";
import { useClashInfo } from "@/hooks/use-clash";
@@ -15,11 +20,15 @@ import {
ConnectionDetailRef,
} from "@/components/connection/connection-detail";
import parseTraffic from "@/utils/parse-traffic";
import { BaseSearchBox } from "@/components/base/base-search-box";
import {
BaseSearchBox,
type SearchState,
} from "@/components/base/base-search-box";
import { BaseStyledSelect } from "@/components/base/base-styled-select";
import useSWRSubscription from "swr/subscription";
import { createSockette } from "@/utils/websocket";
import { useTheme } from "@mui/material/styles";
import { useVisibility } from "@/hooks/use-visibility";
const initConn: IConnections = {
uploadTotal: 0,
@@ -32,7 +41,7 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
const ConnectionsPage = () => {
const { t } = useTranslation();
const { clashInfo } = useClashInfo();
const pageVisible = useVisibility();
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const [match, setMatch] = useState(() => (_: string) => true);
@@ -47,103 +56,128 @@ const ConnectionsPage = () => {
list.sort(
(a, b) =>
new Date(b.start || "0").getTime()! -
new Date(a.start || "0").getTime()!
new Date(a.start || "0").getTime()!,
),
"Upload Speed": (list) => list.sort((a, b) => b.curUpload! - a.curUpload!),
"Download Speed": (list) =>
list.sort((a, b) => b.curDownload! - a.curDownload!),
};
const [isPaused, setIsPaused] = useState(false);
const [frozenData, setFrozenData] = useState<IConnections | null>(null);
const { data: connData = initConn } = useSWRSubscription<
IConnections,
any,
"getClashConnections" | null
>(clashInfo ? "getClashConnections" : null, (_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
>(
clashInfo && pageVisible ? "getClashConnections" : null,
(_key, { next }) => {
const { server = "", secret = "" } = clashInfo!;
const s = createSockette(
`ws://${server}/connections?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
const data = JSON.parse(event.data) as IConnections;
next(null, (old = initConn) => {
const oldConn = old.connections;
const maxLen = data.connections?.length;
const s = createSockette(
`ws://${server}/connections?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
// meta v1.15.0 出现 data.connections 为 null 的情况
const data = JSON.parse(event.data) as IConnections;
// 尽量与前一次 connections 的展示顺序保持一致
next(null, (old = initConn) => {
const oldConn = old.connections;
const maxLen = data.connections?.length;
const connections: IConnectionsItem[] = [];
const connections: IConnectionsItem[] = [];
const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id);
const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id);
if (index >= 0 && index < maxLen) {
const old = oldConn[index];
each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download;
if (index >= 0 && index < maxLen) {
const old = oldConn[index];
each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download;
connections[index] = each;
return false;
}
return true;
});
connections[index] = each;
return false;
for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) {
connections[i] = rest.shift()!;
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
return true;
return { ...data, connections };
});
for (let i = 0; i < maxLen; ++i) {
if (!connections[i] && rest.length > 0) {
connections[i] = rest.shift()!;
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
return { ...data, connections };
});
},
onerror(event) {
next(event);
},
},
onerror(event) {
next(event);
},
},
3
);
3,
);
return () => {
s.close();
};
});
return () => {
s.close();
};
},
);
const [filterConn, download, upload] = useMemo(() => {
const displayData = useMemo(() => {
return isPaused ? (frozenData ?? connData) : connData;
}, [isPaused, frozenData, connData]);
const [filterConn] = useMemo(() => {
const orderFunc = orderOpts[curOrderOpt];
let connections = connData.connections.filter((conn) =>
match(conn.metadata.host || conn.metadata.destinationIP || "")
);
let connections = displayData.connections.filter((conn) => {
const { host, destinationIP, process } = conn.metadata;
return (
match(host || "") || match(destinationIP || "") || match(process || "")
);
});
if (orderFunc) connections = orderFunc(connections);
let download = 0;
let upload = 0;
connections.forEach((x) => {
download += x.download;
upload += x.upload;
});
return [connections, download, upload];
}, [connData, match, curOrderOpt]);
return [connections];
}, [displayData, match, curOrderOpt]);
const onCloseAll = useLockFn(closeAllConnections);
const detailRef = useRef<ConnectionDetailRef>(null!);
const handleSearch = useCallback((match: (content: string) => boolean) => {
setMatch(() => match);
}, []);
const handlePauseToggle = useCallback(() => {
setIsPaused((prev) => {
if (!prev) {
setFrozenData(connData);
} else {
setFrozenData(null);
}
return !prev;
});
}, [connData]);
return (
<BasePage
full
title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>}
contentStyle={{ height: "100%" }}
contentStyle={{
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
borderRadius: "8px",
}}
header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ mx: 1 }}>
{t("Downloaded")}: {parseTraffic(download)}
{t("Downloaded")}: {parseTraffic(displayData.downloadTotal)}
</Box>
<Box sx={{ mx: 1 }}>
{t("Uploaded")}: {parseTraffic(upload)}
{t("Uploaded")}: {parseTraffic(displayData.uploadTotal)}
</Box>
<IconButton
color="inherit"
@@ -152,21 +186,28 @@ const ConnectionsPage = () => {
setSetting((o) =>
o?.layout !== "table"
? { ...o, layout: "table" }
: { ...o, layout: "list" }
: { ...o, layout: "list" },
)
}
>
{isTableLayout ? (
<span title={t("List View")}>
<TableRowsRounded fontSize="inherit" />
</span>
<TableRowsRounded titleAccess={t("List View")} />
) : (
<span title={t("Table View")}>
<TableChartRounded fontSize="inherit" />
</span>
<TableChartRounded titleAccess={t("Table View")} />
)}
</IconButton>
<IconButton
color="inherit"
size="small"
onClick={handlePauseToggle}
title={isPaused ? t("Resume") : t("Pause")}
>
{isPaused ? (
<PlayCircleOutlineRounded />
) : (
<PauseCircleOutlineRounded />
)}
</IconButton>
<Button size="small" variant="contained" onClick={onCloseAll}>
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
</Button>
@@ -182,6 +223,9 @@ const ConnectionsPage = () => {
display: "flex",
alignItems: "center",
userSelect: "text",
position: "sticky",
top: 0,
zIndex: 2,
}}
>
{!isTableLayout && (
@@ -196,37 +240,31 @@ const ConnectionsPage = () => {
))}
</BaseStyledSelect>
)}
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
<BaseSearchBox onSearch={handleSearch} />
</Box>
<Box
height="calc(100% - 65px)"
sx={{
userSelect: "text",
margin: "10px",
borderRadius: "8px",
bgcolor: isDark ? "#282a36" : "#ffffff",
}}
>
{filterConn.length === 0 ? (
<BaseEmpty />
) : isTableLayout ? (
<ConnectionTable
connections={filterConn}
onShowDetail={(detail) => detailRef.current?.open(detail)}
/>
) : (
<Virtuoso
data={filterConn}
itemContent={(_, item) => (
<ConnectionItem
value={item}
onShowDetail={() => detailRef.current?.open(item)}
/>
)}
/>
)}
</Box>
{filterConn.length === 0 ? (
<BaseEmpty />
) : isTableLayout ? (
<ConnectionTable
connections={filterConn}
onShowDetail={(detail) => detailRef.current?.open(detail)}
/>
) : (
<Virtuoso
style={{
flex: 1,
borderRadius: "8px",
}}
data={filterConn}
itemContent={(_, item) => (
<ConnectionItem
value={item}
onShowDetail={() => detailRef.current?.open(item)}
/>
)}
/>
)}
<ConnectionDetail ref={detailRef} />
</BasePage>
);
+31 -28
View File
@@ -32,11 +32,15 @@ const LogPage = () => {
const filterLogs = useMemo(() => {
return logData
? logData.filter((data) =>
logLevel === "all"
? match(data.payload)
: data.type.includes(logLevel) && match(data.payload),
)
? logData.filter((data) => {
// 构建完整的搜索文本,包含时间、类型和内容
const searchText =
`${data.time || ""} ${data.type} ${data.payload}`.toLowerCase();
return logLevel === "all"
? match(searchText)
: data.type.toLowerCase() === logLevel && match(searchText);
})
: [];
}, [logData, logLevel, match]);
@@ -44,7 +48,12 @@ const LogPage = () => {
<BasePage
full
title={t("Logs")}
contentStyle={{ height: "100%" }}
contentStyle={{
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
}}
header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<IconButton
@@ -65,7 +74,7 @@ const LogPage = () => {
size="small"
variant="contained"
onClick={() => {
clearLogs(logLevel);
clearLogs();
}}
>
{t("Clear")}
@@ -102,27 +111,21 @@ const LogPage = () => {
/>
</Box>
<Box
height="calc(100% - 65px)"
sx={{
margin: "10px",
borderRadius: "8px",
bgcolor: isDark ? "#282a36" : "#ffffff",
}}
>
{filterLogs.length > 0 ? (
<Virtuoso
initialTopMostItemIndex={999}
data={filterLogs}
itemContent={(index, item) => (
<LogItem value={item} searchState={searchState} />
)}
followOutput={"smooth"}
/>
) : (
<BaseEmpty />
)}
</Box>
{filterLogs.length > 0 ? (
<Virtuoso
initialTopMostItemIndex={999}
data={filterLogs}
style={{
flex: 1,
}}
itemContent={(index, item) => (
<LogItem value={item} searchState={searchState} />
)}
followOutput={"smooth"}
/>
) : (
<BaseEmpty />
)}
</BasePage>
);
};
+95 -83
View File
@@ -1,7 +1,7 @@
import useSWR, { mutate } from "swr";
import useSWR from "swr";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLockFn } from "ahooks";
import { Box, Button, Grid, IconButton, Stack, Divider } from "@mui/material";
import { Box, Button, IconButton, Stack, Divider, Grid2 } from "@mui/material";
import {
DndContext,
closestCenter,
@@ -25,9 +25,9 @@ import {
} from "@mui/icons-material";
import { useTranslation } from "react-i18next";
import {
getProfiles,
importProfile,
enhanceProfiles,
//restartCore,
getRuntimeLogs,
deleteProfile,
updateProfile,
@@ -45,12 +45,14 @@ import { ProfileMore } from "@/components/profile/profile-more";
import { ProfileItem } from "@/components/profile/profile-item";
import { useProfiles } from "@/hooks/use-profiles";
import { ConfigViewer } from "@/components/setting/mods/config-viewer";
import { throttle } from "lodash-es";
import { add, throttle } from "lodash-es";
import { BaseStyledTextField } from "@/components/base/base-styled-text-field";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { readText } from "@tauri-apps/plugin-clipboard-manager";
import { useLocation } from "react-router-dom";
import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
import { TauriEvent } from "@tauri-apps/api/event";
const ProfilePage = () => {
const { t } = useTranslation();
@@ -64,35 +66,46 @@ const ProfilePage = () => {
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const { current } = location.state || {};
useEffect(() => {
const unlisten = addListener("tauri://file-drop", async (event) => {
const fileList = event.payload as string[];
for (let file of fileList) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
Notice.error(t("Only YAML Files Supported"));
continue;
}
const item = {
type: "local",
name: file.split(/\/|\\/).pop() ?? "New Profile",
desc: "",
url: "",
option: {
with_proxy: false,
self_proxy: false,
},
} as IProfileItem;
let data = await readTextFile(file);
await createProfile(item, data);
await mutateProfiles();
}
});
const handleFileDrop = async () => {
const unlisten = await addListener(
TauriEvent.DRAG_DROP,
async (event: any) => {
const paths = event.payload.paths;
for (let file of paths) {
if (!file.endsWith(".yaml") && !file.endsWith(".yml")) {
Notice.error(t("Only YAML Files Supported"));
continue;
}
const item = {
type: "local",
name: file.split(/\/|\\/).pop() ?? "New Profile",
desc: "",
url: "",
option: {
with_proxy: false,
self_proxy: false,
},
} as IProfileItem;
let data = await readTextFile(file);
await createProfile(item, data);
await mutateProfiles();
}
},
);
return unlisten;
};
const unsubscribe = handleFileDrop();
return () => {
unlisten.then((fn) => fn());
unsubscribe.then((cleanup) => cleanup());
};
}, []);
@@ -105,7 +118,7 @@ const ProfilePage = () => {
const { data: chainLogs = {}, mutate: mutateLogs } = useSWR(
"getRuntimeLogs",
getRuntimeLogs
getRuntimeLogs,
);
const viewerRef = useRef<ProfileViewerRef>(null);
@@ -117,9 +130,7 @@ const ProfilePage = () => {
const type1 = ["local", "remote"];
const profileItems = items.filter((i) => i && type1.includes(i.type!));
return profileItems;
return items.filter((i) => i && type1.includes(i.type!));
}, [profiles]);
const currentActivatings = () => {
@@ -163,11 +174,11 @@ const ProfilePage = () => {
}, 100);
try {
await patchProfiles({ current: profile });
const success = await patchProfiles({ current: profile });
await mutateLogs();
closeAllConnections();
await activateSelected();
if (notifySuccess) {
if (notifySuccess && success) {
Notice.success(t("Profile Switched"), 1000);
}
} catch (err: any) {
@@ -240,7 +251,7 @@ const ProfilePage = () => {
setLoadingCache((cache) => {
// 获取没有正在更新的订阅
const items = profileItems.filter(
(e) => e.type === "remote" && !cache[e.uid]
(e) => e.type === "remote" && !cache[e.uid],
);
const change = Object.fromEntries(items.map((e) => [e.uid, true]));
@@ -356,30 +367,29 @@ const ProfilePage = () => {
{t("New")}
</Button>
</Stack>
<Box
sx={{
pt: 1,
mb: 0.5,
pl: "10px",
mr: "10px",
height: "calc(100% - 68px)",
overflowY: "auto",
}}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={onDragEnd}
<Box
sx={{
pl: "10px",
pr: "10px",
height: "calc(100% - 48px)",
overflowY: "auto",
}}
>
<Box sx={{ mb: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<SortableContext
items={profileItems.map((x) => {
return x.uid;
})}
>
{profileItems.map((item) => (
<Grid item xs={12} sm={6} md={4} lg={3} key={item.file}>
<Grid2 size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={item.file}>
<ProfileItem
id={item.uid}
selected={profiles.current === item.uid}
@@ -390,47 +400,49 @@ const ProfilePage = () => {
onSave={async (prev, curr) => {
if (prev !== curr && profiles.current === item.uid) {
await onEnhance(false);
// await restartCore();
// Notice.success(t("Clash Core Restarted"), 1000);
}
}}
onDelete={() => onDelete(item.uid)}
/>
</Grid>
</Grid2>
))}
</SortableContext>
</Grid>
</Grid2>
</Box>
<Divider
variant="middle"
flexItem
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
<Box sx={{ mt: 1.5, mb: "10px" }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
<ProfileMore
id="Merge"
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance(false);
}
}}
/>
</Grid2>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
<ProfileMore
id="Script"
logInfo={chainLogs["Script"]}
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance(false);
}
}}
/>
</Grid2>
</Grid2>
</Box>
</DndContext>
<Divider
variant="middle"
flexItem
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
></Divider>
<Box sx={{ mt: 1.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid item xs={12} sm={6} md={6} lg={6}>
<ProfileMore
id="Merge"
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance(false);
}
}}
/>
</Grid>
<Grid item xs={12} sm={6} md={6} lg={6}>
<ProfileMore
id="Script"
logInfo={chainLogs["Script"]}
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance(false);
}
}}
/>
</Grid>
</Grid>
</Box>
</Box>
</DndContext>
<ProfileViewer
ref={viewerRef}
+4 -4
View File
@@ -4,7 +4,7 @@ import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { Box, Button, ButtonGroup } from "@mui/material";
import { closeAllConnections, getClashConfig } from "@/services/api";
import { patchClashConfig } from "@/services/cmds";
import { patchClashConfig, patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge";
import { BasePage } from "@/components/base";
import { ProxyGroups } from "@/components/proxy/proxy-groups";
@@ -15,7 +15,7 @@ const ProxyPage = () => {
const { data: clashConfig, mutate: mutateClash } = useSWR(
"getClashConfig",
getClashConfig
getClashConfig,
);
const { verge } = useVerge();
@@ -29,7 +29,7 @@ const ProxyPage = () => {
if (mode !== curMode && verge?.auto_close_connection) {
closeAllConnections();
}
await patchClashConfig({ mode });
await patchClashMode(mode);
mutateClash();
});
@@ -42,7 +42,7 @@ const ProxyPage = () => {
return (
<BasePage
full
contentStyle={{ height: "100%" }}
contentStyle={{ height: "101.5%" }}
title={t("Proxy Groups")}
header={
<Box display="flex" alignItems="center" gap={1}>
+27 -29
View File
@@ -39,7 +39,12 @@ const RulesPage = () => {
<BasePage
full
title={t("Rules")}
contentStyle={{ height: "100%" }}
contentStyle={{
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
}}
header={
<Box display="flex" alignItems="center" gap={1}>
<ProviderButton />
@@ -59,34 +64,27 @@ const RulesPage = () => {
<BaseSearchBox onSearch={(match) => setMatch(() => match)} />
</Box>
<Box
height="calc(100% - 65px)"
sx={{
margin: "10px",
borderRadius: "8px",
bgcolor: isDark ? "#282a36" : "#ffffff",
position: "relative",
}}
>
{rules.length > 0 ? (
<>
<Virtuoso
ref={virtuosoRef}
data={rules}
itemContent={(index, item) => (
<RuleItem index={index + 1} value={item} />
)}
followOutput={"smooth"}
scrollerRef={(ref) => {
if (ref) ref.addEventListener("scroll", handleScroll);
}}
/>
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
</>
) : (
<BaseEmpty />
)}
</Box>
{rules.length > 0 ? (
<>
<Virtuoso
ref={virtuosoRef}
data={rules}
style={{
flex: 1,
}}
itemContent={(index, item) => (
<RuleItem index={index + 1} value={item} />
)}
followOutput={"smooth"}
scrollerRef={(ref) => {
if (ref) ref.addEventListener("scroll", handleScroll);
}}
/>
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
</>
) : (
<BaseEmpty />
)}
</BasePage>
);
};
+20 -3
View File
@@ -1,10 +1,18 @@
import { Box, ButtonGroup, Grid, IconButton } from "@mui/material";
import {
Box,
ButtonGroup,
Grid,
IconButton,
Select,
MenuItem,
} from "@mui/material";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds";
import SettingVerge from "@/components/setting/setting-verge";
import SettingVergeBasic from "@/components/setting/setting-verge-basic";
import SettingVergeAdvanced from "@/components/setting/setting-verge-advanced";
import SettingClash from "@/components/setting/setting-clash";
import SettingSystem from "@/components/setting/setting-system";
import { useThemeMode } from "@/services/states";
@@ -88,10 +96,19 @@ const SettingPage = () => {
<Box
sx={{
borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? "#282a36" : "#ffffff",
}}
>
<SettingVerge onError={onError} />
<SettingVergeBasic onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? "#282a36" : "#ffffff",
}}
>
<SettingVergeAdvanced onError={onError} />
</Box>
</Grid>
</Grid>
+43 -8
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { useVerge } from "@/hooks/use-verge";
import { Box, Button, Grid } from "@mui/material";
import { Box, Button } from "@mui/material";
import Grid2 from "@mui/material/Grid2";
import {
DndContext,
closestCenter,
@@ -21,6 +22,7 @@ import { TestViewer, TestViewerRef } from "@/components/test/test-viewer";
import { TestItem } from "@/components/test/test-item";
import { emit } from "@tauri-apps/api/event";
import { nanoid } from "nanoid";
import { ScrollTopButton } from "@/components/layout/scroll-top-button";
// test icons
import apple from "@/assets/image/test/apple.svg?raw";
@@ -34,7 +36,7 @@ const TestPage = () => {
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
}),
);
const { verge, mutateVerge, patchVerge } = useVerge();
@@ -68,7 +70,7 @@ const TestPage = () => {
const onTestListItemChange = (
uid: string,
patch?: Partial<IVergeTestItem>
patch?: Partial<IVergeTestItem>,
) => {
if (patch) {
const newList = testList.map((x) => {
@@ -120,6 +122,19 @@ const TestPage = () => {
}, [verge]);
const viewerRef = useRef<TestViewerRef>(null);
const [showScrollTop, setShowScrollTop] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const scrollToTop = () => {
containerRef.current?.scrollTo({
top: 0,
behavior: "smooth",
});
};
const handleScroll = (e: any) => {
setShowScrollTop(e.target.scrollTop > 100);
};
return (
<BasePage
@@ -145,10 +160,15 @@ const TestPage = () => {
}
>
<Box
ref={containerRef}
onScroll={handleScroll}
sx={{
pt: 1.25,
mb: 0.5,
px: "10px",
height: "calc(100vh - 100px)",
overflow: "auto",
position: "relative",
}}
>
<DndContext
@@ -157,26 +177,41 @@ const TestPage = () => {
onDragEnd={onDragEnd}
>
<Box sx={{ mb: 4.5 }}>
<Grid container spacing={{ xs: 1, lg: 1 }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}>
<SortableContext
items={testList.map((x) => {
return x.uid;
})}
>
{testList.map((item) => (
<Grid item xs={6} sm={4} md={3} lg={2} key={item.uid}>
<Grid2
component={"div"}
size={{ xs: 6, lg: 2, sm: 4, md: 3 }}
key={item.uid}
>
<TestItem
id={item.uid}
itemData={item}
onEdit={() => viewerRef.current?.edit(item)}
onDelete={onDeleteTestListItem}
/>
</Grid>
</Grid2>
))}
</SortableContext>
</Grid>
</Grid2>
</Box>
</DndContext>
<ScrollTopButton
onClick={scrollToTop}
show={showScrollTop}
sx={{
position: "absolute",
bottom: "20px",
left: "20px",
zIndex: 1000,
}}
/>
</Box>
<TestViewer ref={viewerRef} onChange={onTestListItemChange} />
</BasePage>
+13 -1
View File
@@ -91,6 +91,10 @@ export async function patchClashConfig(payload: Partial<IConfigData>) {
return invoke<void>("patch_clash_config", { payload });
}
export async function patchClashMode(payload: String) {
return invoke<void>("patch_clash_mode", { payload });
}
export async function getVergeConfig() {
return invoke<IVergeConfig>("get_verge_config");
}
@@ -115,7 +119,7 @@ export async function getAutotemProxy() {
}
export async function changeClashCore(clashCore: string) {
return invoke<any>("change_clash_core", { clashCore });
return invoke<string | null>("change_clash_core", { clashCore });
}
export async function restartCore() {
@@ -237,3 +241,11 @@ export async function listWebDavBackup() {
});
return list;
}
export async function scriptValidateNotice(status: string, msg: string) {
return invoke<void>("script_validate_notice", { status, msg });
}
export async function validateScriptFile(filePath: string) {
return invoke<boolean>("validate_script_file", { filePath });
}
+13 -8
View File
@@ -4,18 +4,23 @@ import en from "@/locales/en.json";
import ru from "@/locales/ru.json";
import zh from "@/locales/zh.json";
import fa from "@/locales/fa.json";
import tt from "@/locales/tt.json";
import id from "@/locales/id.json";
import ar from "@/locales/ar.json";
const resources = {
en: { translation: en },
ru: { translation: ru },
zh: { translation: zh },
fa: { translation: fa },
};
export const languages = { en, ru, zh, fa, tt, id, ar };
const resources = Object.fromEntries(
Object.entries(languages).map(([key, value]) => [
key,
{ translation: value },
]),
);
i18n.use(initReactI18next).init({
resources,
lng: "en",
fallbackLng: "en",
lng: "zh",
fallbackLng: "zh",
interpolation: {
escapeValue: false,
},
+1 -1
View File
@@ -20,7 +20,7 @@ export const useConnectionSetting = () =>
{
serializer: JSON.stringify,
deserializer: JSON.parse,
}
},
);
// save the state of each profile item loading
+5
View File
@@ -120,6 +120,7 @@ interface IConnectionsItem {
sourcePort: string;
destinationPort: string;
destinationIP?: string;
remoteDestination?: string;
process?: string;
processPath?: string;
};
@@ -706,10 +707,13 @@ interface IVergeConfig {
common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean;
enable_tray_speed?: boolean;
enable_tun_mode?: boolean;
enable_lite_mode?: boolean;
enable_auto_launch?: boolean;
enable_silent_start?: boolean;
enable_system_proxy?: boolean;
enable_global_hotkey?: boolean;
proxy_auto_config?: boolean;
pac_file_content?: string;
enable_random_port?: boolean;
@@ -723,6 +727,7 @@ interface IVergeConfig {
verge_socks_enabled?: boolean;
verge_http_enabled?: boolean;
enable_proxy_guard?: boolean;
enable_bypass_check?: boolean;
use_default_bypass?: boolean;
proxy_guard_duration?: number;
system_proxy_bypass?: string;

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