Compare commits

...
148 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
82 changed files with 8577 additions and 4203 deletions
+29 -15
View File
@@ -2,9 +2,6 @@ name: Alpha Build
on: on:
workflow_dispatch: workflow_dispatch:
push:
branches: [main]
tags-ignore: [updater, alpha]
permissions: write-all permissions: write-all
env: env:
CARGO_INCREMENTAL: 0 CARGO_INCREMENTAL: 0
@@ -58,7 +55,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
@@ -70,6 +67,9 @@ jobs:
pnpm i pnpm i
pnpm check ${{ matrix.target }} pnpm check ${{ matrix.target }}
- name: Alpha Version update
run: pnpm run fix-alpha-version
- name: Tauri build - name: Tauri build
uses: tauri-apps/tauri-action@v0 uses: tauri-apps/tauri-action@v0
env: env:
@@ -129,7 +129,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v4 uses: pnpm/action-setup@v4
@@ -248,7 +248,7 @@ jobs:
- name: Install Node - name: Install Node
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: "20" node-version: "22"
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
name: Install pnpm name: Install pnpm
@@ -281,9 +281,23 @@ jobs:
- name: Rename - name: Rename
run: | 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' $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-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' foreach ($file in $files) {
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' $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 - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
@@ -313,12 +327,12 @@ jobs:
echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV
shell: bash shell: bash
- name: Update Tag # - name: Update Tag
uses: richardsimko/update-tag@v1 # uses: richardsimko/update-tag@v1
with: # with:
tag_name: alpha # tag_name: alpha
env: # env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: | - run: |
cat > release.txt << 'EOF' cat > release.txt << 'EOF'
+17 -3
View File
@@ -271,9 +271,23 @@ jobs:
- name: Rename - name: Rename
run: | 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' $files = Get-ChildItem ".\src-tauri\target\${{ matrix.target }}\release\bundle\nsis\*-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' foreach ($file in $files) {
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' $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 - name: Upload Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
-3
View File
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm pretty-quick --staged 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) 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 ## Development
See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
+150
View File
@@ -1,3 +1,153 @@
## 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的健壮性;而且更强大易用!
- 由于更改了服务安装逻辑,每次更新安装需要输入系统密码卸载老版本服务和安装新版本服务,以后可以丝滑使用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 ## v2.0.2
### Notice ### Notice
+48 -47
View File
@@ -1,6 +1,6 @@
{ {
"name": "clash-verge", "name": "clash-verge",
"version": "2.0.2", "version": "2.1.2",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "cross-env RUST_BACKTRACE=1 tauri dev", "dev": "cross-env RUST_BACKTRACE=1 tauri dev",
@@ -15,82 +15,83 @@
"updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs", "updater-fixed-webview2": "node scripts/updater-fixed-webview2.mjs",
"portable": "node scripts/portable.mjs", "portable": "node scripts/portable.mjs",
"portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs", "portable-fixed-webview2": "node scripts/portable-fixed-webview2.mjs",
"fix-alpha-version": "node scripts/alpha_version.mjs",
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.1.0", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^8.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.13.3", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.14.0",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"@mui/icons-material": "^6.1.6", "@mui/icons-material": "^6.4.2",
"@mui/lab": "5.0.0-alpha.149", "@mui/lab": "6.0.0-beta.25",
"@mui/material": "^6.1.6", "@mui/material": "^6.4.2",
"@mui/x-data-grid": "^7.22.2", "@mui/x-data-grid": "^7.25.0",
"@tauri-apps/api": "2.1.1", "@tauri-apps/api": "2.2.0",
"@tauri-apps/plugin-clipboard-manager": "2.0.0", "@tauri-apps/plugin-clipboard-manager": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.0.2", "@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.0.0", "@tauri-apps/plugin-global-shortcut": "^2.2.0",
"@tauri-apps/plugin-notification": "^2.0.0", "@tauri-apps/plugin-notification": "^2.2.1",
"@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.0.1", "@tauri-apps/plugin-shell": "2.2.0",
"@tauri-apps/plugin-updater": "^2.0.0", "@tauri-apps/plugin-updater": "2.3.0",
"@types/json-schema": "^7.0.15", "@types/json-schema": "^7.0.15",
"ahooks": "^3.8.1", "ahooks": "^3.8.4",
"axios": "^1.7.7", "axios": "^1.7.9",
"cli-color": "^2.0.4", "cli-color": "^2.0.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"foxact": "^0.2.41", "foxact": "^0.2.43",
"glob": "^11.0.0", "glob": "^11.0.1",
"i18next": "^23.16.5", "i18next": "^24.2.2",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"monaco-editor": "^0.52.0", "monaco-editor": "^0.52.2",
"monaco-yaml": "^5.2.3", "monaco-yaml": "^5.2.3",
"nanoid": "^5.0.8", "nanoid": "^5.0.9",
"peggy": "^4.1.1", "peggy": "^4.2.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2", "react-error-boundary": "^4.0.12",
"react-hook-form": "^7.53.2", "react-hook-form": "^7.54.2",
"react-i18next": "^15.1.1", "react-i18next": "^13.5.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-monaco-editor": "^0.56.2", "react-monaco-editor": "^0.56.0",
"react-router-dom": "^6.28.0", "react-router-dom": "^6.22.3",
"react-transition-group": "^4.4.5", "react-transition-group": "^4.4.5",
"react-virtuoso": "^4.12.0", "react-virtuoso": "^4.6.3",
"sockette": "^2.0.6", "sockette": "^2.0.6",
"swr": "^2.2.5", "swr": "^2.3.0",
"tar": "^7.4.3", "tar": "^7.4.3",
"types-pac": "^1.0.3", "types-pac": "^1.0.3",
"zustand": "^5.0.1" "zustand": "^5.0.3"
}, },
"devDependencies": { "devDependencies": {
"@actions/github": "^6.0.0", "@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-cookie": "^3.0.6",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.12", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.5",
"@types/react-transition-group": "^4.4.11", "@types/react-transition-group": "^4.4.12",
"@vitejs/plugin-legacy": "^5.4.3", "@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.2.1",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"https-proxy-agent": "^7.0.5", "https-proxy-agent": "^7.0.6",
"husky": "^9.1.7", "husky": "^9.1.7",
"meta-json-schema": "^1.18.10", "meta-json-schema": "^1.19.1",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"prettier": "^3.3.3", "prettier": "^3.4.2",
"pretty-quick": "^4.0.0", "pretty-quick": "^4.0.0",
"sass": "^1.81.0", "sass": "^1.83.4",
"terser": "^5.36.0", "terser": "^5.37.0",
"typescript": "^5.6.3", "typescript": "^5.7.3",
"vite": "^5.4.11", "vite": "^6.0.11",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vite-plugin-svgr": "^4.3.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);
+37 -4
View File
@@ -5,7 +5,7 @@ import { extract } from "tar";
import path from "path"; import path from "path";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import fetch from "node-fetch"; import fetch from "node-fetch";
import proxyAgent from "https-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent";
import { execSync } from "child_process"; import { execSync } from "child_process";
import { log_info, log_debug, log_error, log_success } from "./utils.mjs"; import { log_info, log_debug, log_error, log_success } from "./utils.mjs";
import { glob } from "glob"; import { glob } from "glob";
@@ -85,7 +85,7 @@ async function getLatestAlphaVersion() {
process.env.https_proxy; process.env.https_proxy;
if (httpProxy) { if (httpProxy) {
options.agent = proxyAgent(httpProxy); options.agent = new HttpsProxyAgent(httpProxy);
} }
try { try {
const response = await fetch(META_ALPHA_VERSION_URL, { const response = await fetch(META_ALPHA_VERSION_URL, {
@@ -132,7 +132,7 @@ async function getLatestReleaseVersion() {
process.env.https_proxy; process.env.https_proxy;
if (httpProxy) { if (httpProxy) {
options.agent = proxyAgent(httpProxy); options.agent = new HttpsProxyAgent(httpProxy);
} }
try { try {
const response = await fetch(META_VERSION_URL, { const response = await fetch(META_VERSION_URL, {
@@ -332,7 +332,7 @@ async function resolveResource(binInfo) {
process.env.https_proxy; process.env.https_proxy;
if (httpProxy) { if (httpProxy) {
options.agent = proxyAgent(httpProxy); options.agent = new HttpsProxyAgent(httpProxy);
} }
const response = await fetch(url, { const response = await fetch(url, {
@@ -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 * main
*/ */
@@ -509,6 +537,11 @@ const tasks = [
retry: 5, retry: 5,
macosOnly: true, macosOnly: true,
}, },
{
name: "locales",
func: resolveLocales,
retry: 2,
},
]; ];
async function runTask() { async function runTask() {
+6 -6
View File
@@ -60,35 +60,35 @@ async function resolveUpdater() {
const { name, browser_download_url } = asset; const { name, browser_download_url } = asset;
// win64 url // win64 url
if (name.endsWith("x64-setup.nsis.zip")) { if (name.endsWith("x64-setup.exe")) {
updateData.platforms.win64.url = browser_download_url; updateData.platforms.win64.url = browser_download_url;
updateData.platforms["windows-x86_64"].url = browser_download_url; updateData.platforms["windows-x86_64"].url = browser_download_url;
} }
// win64 signature // win64 signature
if (name.endsWith("x64-setup.nsis.zip.sig")) { if (name.endsWith("x64-setup.exe.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms.win64.signature = sig; updateData.platforms.win64.signature = sig;
updateData.platforms["windows-x86_64"].signature = sig; updateData.platforms["windows-x86_64"].signature = sig;
} }
// win32 url // 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-x86"].url = browser_download_url;
updateData.platforms["windows-i686"].url = browser_download_url; updateData.platforms["windows-i686"].url = browser_download_url;
} }
// win32 signature // win32 signature
if (name.endsWith("x86-setup.nsis.zip.sig")) { if (name.endsWith("x86-setup.exe.sig")) {
const sig = await getSignature(browser_download_url); const sig = await getSignature(browser_download_url);
updateData.platforms["windows-x86"].signature = sig; updateData.platforms["windows-x86"].signature = sig;
updateData.platforms["windows-i686"].signature = sig; updateData.platforms["windows-i686"].signature = sig;
} }
// win arm url // win arm url
if (name.endsWith("arm64-setup.nsis.zip")) { if (name.endsWith("arm64-setup.exe")) {
updateData.platforms["windows-aarch64"].url = browser_download_url; updateData.platforms["windows-aarch64"].url = browser_download_url;
} }
// win arm signature // 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); const sig = await getSignature(browser_download_url);
updateData.platforms["windows-aarch64"].signature = sig; updateData.platforms["windows-aarch64"].signature = sig;
} }
+1130 -1053
View File
File diff suppressed because it is too large Load Diff
+33 -25
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "clash-verge" name = "clash-verge"
version = "2.0.2" version = "2.1.2"
description = "clash verge" description = "clash verge"
authors = ["zzzgydi", "wonfen", "MystiPanda"] authors = ["zzzgydi", "wonfen", "MystiPanda"]
license = "GPL-3.0-only" license = "GPL-3.0-only"
@@ -9,35 +9,40 @@ default-run = "clash-verge"
edition = "2021" edition = "2021"
build = "build.rs" build = "build.rs"
[package.metadata.bundle]
identifier = "io.github.clash-verge-rev.clash-verge-rev"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.3", features = [] } tauri-build = { version = "2.0.5", features = [] }
[dependencies] [dependencies]
warp = "0.3" warp = "0.3"
anyhow = "1.0" anyhow = "1.0"
dirs = "5.0" dirs = "6.0"
open = "5.1" open = "5.1"
log = "0.4" log = "0.4"
dunce = "1.0" dunce = "1.0"
log4rs = "1" log4rs = "1"
nanoid = "0.4" nanoid = "0.4"
chrono = "0.4" chrono = "0.4"
sysinfo = "0.32.0" sysinfo = "0.33.1"
boa_engine = "0.19.1" boa_engine = "0.20.0"
serde_json = "1.0" serde_json = "1.0"
serde_yaml = "0.9" serde_yaml = "0.9"
once_cell = "1.19" once_cell = "1.19"
port_scanner = "0.1.5" port_scanner = "0.1.5"
delay_timer = "0.11" delay_timer = "0.11.6"
parking_lot = "0.12" parking_lot = "0.12"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
window-shadows = { version = "0.2.2" } window-shadows = { version = "0.2.2" }
tokio = { version = "1", features = ["full"] } tokio = { version = "1.43", features = ["full"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", features = ["json", "rustls-tls"] } reqwest = { version = "0.12", features = ["json", "rustls-tls"] }
sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", branch = "main" } sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs", rev = "3d748b5" }
image = "0.25.5" image = "0.24"
tauri = { version = "2.1.1", features = [ imageproc = "0.23"
rusttype = "0.9"
tauri = { version = "2.2.5", features = [
"protocol-asset", "protocol-asset",
"devtools", "devtools",
"tray-icon", "tray-icon",
@@ -45,36 +50,39 @@ tauri = { version = "2.1.1", features = [
"image-png", "image-png",
] } ] }
network-interface = { version = "2.0.0", features = ["serde"] } network-interface = { version = "2.0.0", features = ["serde"] }
tauri-plugin-shell = "2.0.2" tauri-plugin-shell = "2.2.0"
tauri-plugin-dialog = "2.0.2" tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.0.2" tauri-plugin-fs = "2.2.0"
tauri-plugin-notification = "2.0.1" tauri-plugin-notification = "2.2.1"
tauri-plugin-process = "2.0.1" tauri-plugin-process = "2.2.0"
tauri-plugin-clipboard-manager = "2.0.1" tauri-plugin-clipboard-manager = "2.2.1"
tauri-plugin-deep-link = "2.0.1" tauri-plugin-deep-link = "2.2.0"
tauri-plugin-devtools = "2.0.0-rc" tauri-plugin-devtools = "2.0.0-rc"
url = "2.5.2" url = "2.5.4"
zip = "2.2.0" zip = "2.2.2"
reqwest_dav = "0.1.14" reqwest_dav = "0.1.14"
aes-gcm = { version = "0.10.3", features = ["std"] } aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1" base64 = "0.22.1"
getrandom = "0.2" getrandom = "0.2"
tokio-tungstenite = "0.26.1"
futures = "0.3"
sys-locale = "0.3.1"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
runas = "=1.2.0" runas = "=1.2.0"
deelevate = "0.2.0" deelevate = "0.2.0"
winreg = "0.52.0" winreg = "0.55.0"
url = "2.5.2" url = "2.5.4"
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0" users = "0.11.0"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.0.0-rc" tauri-plugin-autostart = "2.2.0"
tauri-plugin-global-shortcut = "2.0.1" tauri-plugin-global-shortcut = "2.2.0"
tauri-plugin-updater = "2.0.2" tauri-plugin-updater = "2.3.0"
tauri-plugin-window-state = "2.0.2" tauri-plugin-window-state = "2.2.1"
#openssl #openssl
[features] [features]
BIN
View File
Binary file not shown.
+7
View File
@@ -6,6 +6,13 @@
"permissions": [ "permissions": [
"global-shortcut:default", "global-shortcut:default",
"updater: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", "deep-link:default",
"window-state:default", "window-state:default",
"window-state:default", "window-state:default",
@@ -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>
+294 -24
View File
@@ -4,7 +4,7 @@ use crate::{
feat, feat,
utils::{dirs, help}, utils::{dirs, help},
}; };
use crate::{ret_err, wrap_err}; use crate::{log_err, ret_err, wrap_err};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use network_interface::NetworkInterface; use network_interface::NetworkInterface;
use serde_yaml::Mapping; use serde_yaml::Mapping;
@@ -13,6 +13,7 @@ use sysproxy::{Autoproxy, Sysproxy};
type CmdResult<T = ()> = Result<T, String>; type CmdResult<T = ()> = Result<T, String>;
use reqwest_dav::list_cmd::ListFile; use reqwest_dav::list_cmd::ListFile;
use tauri::Manager; use tauri::Manager;
use std::fs;
#[tauri::command] #[tauri::command]
pub fn copy_clash_env() -> CmdResult { pub fn copy_clash_env() -> CmdResult {
@@ -22,14 +23,30 @@ pub fn copy_clash_env() -> CmdResult {
#[tauri::command] #[tauri::command]
pub fn get_profiles() -> CmdResult<IProfiles> { pub fn get_profiles() -> CmdResult<IProfiles> {
let _ = tray::Tray::global().update_menu();
Ok(Config::profiles().data().clone()) Ok(Config::profiles().data().clone())
} }
#[tauri::command] #[tauri::command]
pub async fn enhance_profiles() -> CmdResult { pub async fn enhance_profiles() -> CmdResult {
wrap_err!(CoreManager::global().update_config().await)?; match CoreManager::global().update_config().await {
handle::Handle::refresh_clash(); Ok((true, _)) => {
Ok(()) 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] #[tauri::command]
@@ -61,36 +78,80 @@ pub async fn delete_profile(index: String) -> CmdResult {
wrap_err!(CoreManager::global().update_config().await)?; wrap_err!(CoreManager::global().update_config().await)?;
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
Ok(()) Ok(())
} }
/// 修改profiles的 /// 修改profiles的配置
#[tauri::command] #[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) })?; wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
// 更新配置并进行验证
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok((true, _)) => {
println!("[cmd配置patch] 配置更新成功");
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
let _ = handle::Handle::update_systray_part(); let _ = tray::Tray::global().update_tooltip();
Config::profiles().apply(); Config::profiles().apply();
wrap_err!(Config::profiles().data().save_file())?; wrap_err!(Config::profiles().data().save_file())?;
Ok(()) Ok(true)
} }
Err(err) => { Ok((false, error_msg)) => {
println!("[cmd配置patch] 配置验证失败: {}", error_msg);
Config::profiles().discard(); 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的 /// 修改某个profile item的
#[tauri::command] #[tauri::command]
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
wrap_err!(Config::profiles().data().patch_item(index, profile))?; wrap_err!(Config::profiles().data().patch_item(index, profile))?;
wrap_err!(timer::Timer::global().refresh()) Ok(())
} }
#[tauri::command] #[tauri::command]
@@ -118,17 +179,107 @@ pub fn read_profile_file(index: String) -> CmdResult<String> {
let data = wrap_err!(item.read_file())?; let data = wrap_err!(item.read_file())?;
Ok(data) Ok(data)
} }
/// 保存profiles的配置
#[tauri::command] #[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() { if file_data.is_none() {
return Ok(()); return Ok(());
} }
let profiles = Config::profiles(); // 在异步操作前完成所有文件操作
let profiles = profiles.latest(); let (file_path, original_content, is_merge_file) = {
let item = wrap_err!(profiles.get_item(&index))?; let profiles = Config::profiles();
wrap_err!(item.save_file(file_data.unwrap())) 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] #[tauri::command]
@@ -168,6 +319,12 @@ pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
wrap_err!(feat::patch_clash(payload).await) 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] #[tauri::command]
pub fn get_verge_config() -> CmdResult<IVergeResponse> { pub fn get_verge_config() -> CmdResult<IVergeResponse> {
let verge = Config::verge(); let verge = Config::verge();
@@ -177,12 +334,27 @@ pub fn get_verge_config() -> CmdResult<IVergeResponse> {
#[tauri::command] #[tauri::command]
pub async fn patch_verge_config(payload: IVerge) -> CmdResult { 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] #[tauri::command]
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult { pub async fn change_clash_core(clash_core: String) -> CmdResult<Option<String>> {
wrap_err!(CoreManager::global().change_core(clash_core).await) 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 /// restart the sidecar
@@ -426,3 +598,101 @@ pub mod uwp {
Ok(()) 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);
}
}
+5 -2
View File
@@ -156,17 +156,20 @@ impl IClashTemp {
} }
pub fn guard_mixed_port(config: &Mapping) -> u16 { pub fn guard_mixed_port(config: &Mapping) -> u16 {
let mut port = config let raw_value = config.get("mixed-port");
.get("mixed-port")
let mut port = raw_value
.and_then(|value| match value { .and_then(|value| match value {
Value::String(val_str) => val_str.parse().ok(), Value::String(val_str) => val_str.parse().ok(),
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
_ => None, _ => None,
}) })
.unwrap_or(7897); .unwrap_or(7897);
if port == 0 { if port == 0 {
port = 7897; port = 7897;
} }
port port
} }
+50 -11
View File
@@ -3,10 +3,12 @@ use crate::{
config::PrfItem, config::PrfItem,
enhance, enhance,
utils::{dirs, help}, utils::{dirs, help},
core::{handle, CoreManager},
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::path::PathBuf; use std::path::PathBuf;
use tokio::time::{sleep, Duration};
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
pub const CHECK_CONFIG: &str = "clash-verge-check.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()))?; let script_item = PrfItem::from_script(Some("Script".to_string()))?;
Self::profiles().data().append_item(script_item.clone())?; 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文件拿过来 crate::log_err!(Self::generate().await);
if !runtime_path.exists() {
help::save_yaml( // 生成运行时配置文件并验证
&runtime_path, let config_result = Self::generate_file(ConfigType::Run);
&Config::clash().latest().0,
Some("# Clash Verge Runtime"), 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(()) Ok(())
} }
+19 -1
View File
@@ -71,7 +71,6 @@ impl IProfiles {
if let Some(current) = patch.current { if let Some(current) = patch.current {
let items = self.items.as_ref().unwrap(); let items = self.items.as_ref().unwrap();
let some_uid = Some(current); let some_uid = Some(current);
if items.iter().any(|e| e.uid == some_uid) { if items.iter().any(|e| e.uid == some_uid) {
self.current = some_uid; self.current = some_uid;
} }
@@ -465,4 +464,23 @@ impl IProfiles {
_ => None, _ => 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::DEFAULT_PAC;
use crate::config::{deserialize_encrypted, serialize_encrypted}; use crate::config::{deserialize_encrypted, serialize_encrypted};
use crate::utils::i18n;
use crate::utils::{dirs, help}; use crate::utils::{dirs, help};
use anyhow::Result; use anyhow::Result;
use log::LevelFilter; use log::LevelFilter;
@@ -97,6 +98,9 @@ pub struct IVerge {
/// hotkey map /// hotkey map
/// format: {func},{key} /// format: {func},{key}
pub hotkeys: Option<Vec<String>>, pub hotkeys: Option<Vec<String>>,
/// enable global hotkey
pub enable_global_hotkey: Option<bool>,
/// 切换代理时自动关闭连接 /// 切换代理时自动关闭连接
pub auto_close_connection: Option<bool>, pub auto_close_connection: Option<bool>,
@@ -175,6 +179,11 @@ pub struct IVerge {
default default
)] )]
pub webdav_password: Option<String>, pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
/// 轻量模式 - 只保留内核运行
pub enable_lite_mode: Option<bool>,
} }
#[derive(Default, Debug, Clone, Deserialize, Serialize)] #[derive(Default, Debug, Clone, Deserialize, Serialize)]
@@ -202,6 +211,21 @@ pub struct IVergeTheme {
} }
impl IVerge { 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 { pub fn new() -> Self {
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) { match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
Ok(config) => config, Ok(config) => config,
@@ -215,7 +239,7 @@ impl IVerge {
pub fn template() -> Self { pub fn template() -> Self {
Self { Self {
clash_core: Some("verge-mihomo".into()), clash_core: Some("verge-mihomo".into()),
language: Some("zh".into()), language: Some(Self::get_system_language()),
theme_mode: Some("system".into()), theme_mode: Some("system".into()),
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
env_type: Some("bash".into()), env_type: Some("bash".into()),
@@ -260,6 +284,9 @@ impl IVerge {
webdav_url: None, webdav_url: None,
webdav_username: None, webdav_username: None,
webdav_password: None, webdav_password: None,
enable_tray_speed: Some(true),
enable_global_hotkey: Some(true),
enable_lite_mode: Some(false),
..Self::default() ..Self::default()
} }
} }
@@ -326,6 +353,7 @@ impl IVerge {
patch!(web_ui_list); patch!(web_ui_list);
patch!(clash_core); patch!(clash_core);
patch!(hotkeys); patch!(hotkeys);
patch!(enable_global_hotkey);
patch!(auto_close_connection); patch!(auto_close_connection);
patch!(auto_check_update); patch!(auto_check_update);
@@ -339,6 +367,8 @@ impl IVerge {
patch!(webdav_url); patch!(webdav_url);
patch!(webdav_username); patch!(webdav_username);
patch!(webdav_password); 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_silent_start: Option<bool>,
pub enable_system_proxy: Option<bool>, pub enable_system_proxy: Option<bool>,
pub enable_proxy_guard: Option<bool>, pub enable_proxy_guard: Option<bool>,
pub enable_global_hotkey: Option<bool>,
pub use_default_bypass: Option<bool>, pub use_default_bypass: Option<bool>,
pub system_proxy_bypass: Option<String>, pub system_proxy_bypass: Option<String>,
pub proxy_guard_duration: Option<u64>, pub proxy_guard_duration: Option<u64>,
@@ -425,6 +456,8 @@ pub struct IVergeResponse {
pub webdav_url: Option<String>, pub webdav_url: Option<String>,
pub webdav_username: Option<String>, pub webdav_username: Option<String>,
pub webdav_password: Option<String>, pub webdav_password: Option<String>,
pub enable_tray_speed: Option<bool>,
pub enable_lite_mode: Option<bool>,
} }
impl From<IVerge> for IVergeResponse { impl From<IVerge> for IVergeResponse {
@@ -451,6 +484,7 @@ impl From<IVerge> for IVergeResponse {
enable_silent_start: verge.enable_silent_start, enable_silent_start: verge.enable_silent_start,
enable_system_proxy: verge.enable_system_proxy, enable_system_proxy: verge.enable_system_proxy,
enable_proxy_guard: verge.enable_proxy_guard, enable_proxy_guard: verge.enable_proxy_guard,
enable_global_hotkey: verge.enable_global_hotkey,
use_default_bypass: verge.use_default_bypass, use_default_bypass: verge.use_default_bypass,
system_proxy_bypass: verge.system_proxy_bypass, system_proxy_bypass: verge.system_proxy_bypass,
proxy_guard_duration: verge.proxy_guard_duration, proxy_guard_duration: verge.proxy_guard_duration,
@@ -485,6 +519,8 @@ impl From<IVerge> for IVergeResponse {
webdav_url: verge.webdav_url, webdav_url: verge.webdav_url,
webdav_username: verge.webdav_username, webdav_username: verge.webdav_username,
webdav_password: verge.webdav_password, webdav_password: verge.webdav_password,
enable_tray_speed: verge.enable_tray_speed,
enable_lite_mode: verge.enable_lite_mode,
} }
} }
} }
+1 -1
View File
@@ -173,7 +173,7 @@ impl WebDavClient {
Ok::<Vec<ListFile>, Error>(final_files) Ok::<Vec<ListFile>, Error>(final_files)
}; };
Ok(timeout(Duration::from_secs(TIMEOUT_LIST), fut).await??) timeout(Duration::from_secs(TIMEOUT_LIST), fut).await?
} }
pub async fn delete(&self, file_name: String) -> Result<(), Error> { pub async fn delete(&self, file_name: String) -> Result<(), Error> {
+16 -1
View File
@@ -5,6 +5,12 @@ use serde::{Deserialize, Serialize};
use serde_yaml::Mapping; use serde_yaml::Mapping;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Rate {
pub up: u64,
pub down: u64,
}
/// PUT /configs /// PUT /configs
/// path 是绝对路径 /// path 是绝对路径
pub async fn put_configs(path: &str) -> Result<()> { 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() { match response.status().as_u16() {
204 => Ok(()), 204 => Ok(()),
status => { 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 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] #[test]
fn test_parse_check_output() { fn test_parse_check_output() {
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#; let 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::config::*;
use crate::core::{clash_api, handle, service}; use crate::core::{clash_api, handle, service};
#[cfg(target_os = "macos")]
use crate::core::tray::Tray;
use crate::log_err; use crate::log_err;
use crate::utils::dirs; use crate::utils::{dirs, help};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde_yaml::Mapping; use serde_yaml::Mapping;
@@ -31,28 +33,6 @@ impl CoreManager {
Ok(()) 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<()> { pub async fn stop_core(&self) -> Result<()> {
let mut running = self.running.lock().await; let mut running = self.running.lock().await;
@@ -76,7 +56,6 @@ impl CoreManager {
service::stop_core_by_service().await?; service::stop_core_by_service().await?;
} }
*running = false; *running = false;
Ok(()) Ok(())
} }
@@ -94,8 +73,13 @@ impl CoreManager {
if service::check_service().await.is_ok() { if service::check_service().await.is_ok() {
log::info!(target: "app", "try to run core in service mode"); log::info!(target: "app", "try to run core in service mode");
service::run_core_by_service(&config_path).await?; service::run_core_by_service(&config_path).await?;
*running = true;
} }
// 流量订阅
#[cfg(target_os = "macos")]
log_err!(Tray::global().subscribe_traffic().await);
*running = true;
Ok(()) Ok(())
} }
@@ -107,6 +91,23 @@ impl CoreManager {
Ok(()) 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<()> { pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?; 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}`"); log::info!(target: "app", "change core to `{clash_core}`");
// 1. 先更新内核配置(但不应用)
Config::verge().draft().clash_core = Some(clash_core); Config::verge().draft().clash_core = Some(clash_core);
// 更新订阅 // 2. 使用新内核验证配置
Config::generate().await?; println!("[切换内核] 使用新内核验证配置");
match self.validate_config().await {
self.check_config().await?; Ok((true, _)) => {
println!("[切换内核] 配置验证通过,开始切换内核");
match self.restart_core().await { // 3. 验证通过后,应用内核配置并重启
Ok(_) => {
Config::verge().apply(); Config::verge().apply();
Config::runtime().apply();
log_err!(Config::verge().latest().save_file()); 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) => { Err(err) => {
println!("[切换内核] 验证过程发生错误: {}", err);
Config::verge().discard(); Config::verge().discard();
Config::runtime().discard();
Err(err) Err(err)
} }
} }
} }
/// 更新proxies那些 /// 内部验证配置文件的实现
/// 如果涉及端口和外部控制则需要重启 async fn validate_config_internal(&self, config_path: &str) -> Result<(bool, String)> {
pub async fn update_config(&self) -> Result<()> { println!("[core配置验证] 开始验证配置文件: {}", config_path);
log::debug!(target: "app", "try to update clash config");
// 更新订阅 let clash_core = { Config::verge().latest().clash_core.clone() };
Config::generate().await?; 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);
// 检查订阅是否正常 // 使用子进程运行clash验证配置
self.check_config().await?; println!("[core配置验证] 运行子进程验证配置");
let output = app_handle
.shell()
.sidecar(clash_core)?
.args(["-t", "-d", test_dir, "-f", config_path])
.output()
.await?;
// 更新运行时订阅 let stderr = String::from_utf8_lossy(&output.stderr);
let path = Config::generate_file(ConfigType::Run)?; let stdout = String::from_utf8_lossy(&output.stdout);
let path = dirs::path_to_str(&path)?;
// 检查进程退出状态和错误输出
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次 if has_error {
for i in 0..10 { println!("[core配置验证] 发现错误,开始处理错误信息");
match clash_api::put_configs(path).await { let error_msg = if !stdout.is_empty() {
Ok(_) => break, 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) => { Err(err) => {
if i < 9 { // 如果无法确定文件类型,尝试使用Clash内核验证
log::info!(target: "app", "{err}"); log::warn!(target: "app", "无法确定文件类型: {}, 错误: {}", config_path, err);
} else { return self.validate_config_internal(config_path).await;
bail!(err);
}
} }
} }
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 crate::log_err;
use anyhow::Result;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::RwLock; use parking_lot::RwLock;
use std::sync::Arc; 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) { pub fn set_is_exiting(&self) {
let mut is_exiting = self.is_exiting.write(); let mut is_exiting = self.is_exiting.write();
*is_exiting = true; *is_exiting = true;
+121 -10
View File
@@ -1,11 +1,14 @@
use crate::core::handle; use crate::core::handle;
use crate::{config::Config, feat, log_err}; use crate::{config::Config, feat, log_err};
use crate::utils::resolve;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use tauri::Manager; use tauri::Manager;
use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState};
use tauri::async_runtime;
pub struct Hotkey { pub struct Hotkey {
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置 current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
} }
@@ -21,8 +24,22 @@ impl Hotkey {
pub fn init(&self) -> Result<()> { pub fn init(&self) -> Result<()> {
let verge = Config::verge(); 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() { 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() { for hotkey in hotkeys.iter() {
let mut iter = hotkey.split(','); let mut iter = hotkey.split(',');
let func = iter.next(); let func = iter.next();
@@ -30,56 +47,151 @@ impl Hotkey {
match (key, func) { match (key, func) {
(Some(key), Some(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 key = key.unwrap_or("None");
let func = func.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); self.current.lock().clone_from(hotkeys);
} else {
println!("No hotkeys configured");
log::info!(target: "app", "No hotkeys configured");
} }
Ok(()) 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<()> { pub fn register(&self, hotkey: &str, func: &str) -> Result<()> {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut(); 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) { 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)?; manager.unregister(hotkey)?;
} }
let f = match func.trim() { 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_rule" => || feat::change_clash_mode("rule".into()),
"clash_mode_global" => || feat::change_clash_mode("global".into()), "clash_mode_global" => || feat::change_clash_mode("global".into()),
"clash_mode_direct" => || feat::change_clash_mode("direct".into()), "clash_mode_direct" => || feat::change_clash_mode("direct".into()),
"toggle_system_proxy" => feat::toggle_system_proxy, "toggle_system_proxy" => || feat::toggle_system_proxy(),
"toggle_tun_mode" => feat::toggle_tun_mode, "toggle_tun_mode" => || feat::toggle_tun_mode(None),
"quit" => || feat::quit(Some(0)), "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| { let _ = manager.on_shortcut(hotkey, move |app_handle, hotkey, event| {
if event.state == ShortcutState::Pressed { 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 let Some(window) = app_handle.get_webview_window("main") {
if window.is_focused().unwrap_or(false) { if window.is_focused().unwrap_or(false) {
println!("Executing quit function");
log::info!(target: "app", "Executing quit function");
f(); f();
} }
} }
} else { } 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(()) Ok(())
} }
@@ -87,7 +199,6 @@ impl Hotkey {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
let manager = app_handle.global_shortcut(); let manager = app_handle.global_shortcut();
manager.unregister(hotkey)?; manager.unregister(hotkey)?;
log::debug!(target: "app", "unregister hotkey {hotkey}"); log::debug!(target: "app", "unregister hotkey {hotkey}");
Ok(()) Ok(())
} }
+49
View File
@@ -10,6 +10,7 @@ use tokio::time::Duration;
// Windows only // Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211"; const SERVICE_URL: &str = "http://127.0.0.1:33211";
const REQUIRED_SERVICE_VERSION: &str = "1.0.1"; // 定义所需的服务版本号
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ResponseBody { pub struct ResponseBody {
@@ -19,6 +20,12 @@ pub struct ResponseBody {
pub log_file: String, pub log_file: String,
} }
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct VersionResponse {
pub service: String,
pub version: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct JsonResponse { pub struct JsonResponse {
pub code: u64, pub code: u64,
@@ -26,6 +33,13 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>, 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")] #[cfg(target_os = "windows")]
pub async fn reinstall_service() -> Result<()> { pub async fn reinstall_service() -> Result<()> {
log::info!(target:"app", "reinstall service"); log::info!(target:"app", "reinstall service");
@@ -177,8 +191,43 @@ pub async fn check_service() -> Result<JsonResponse> {
Ok(response) 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 /// start the clash by service
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { 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 = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into()); 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")] #[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>"; 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")] #[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")] #[cfg(target_os = "macos")]
static DEFAULT_BYPASS: &str = 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 { fn get_bypass() -> String {
let use_default = Config::verge().latest().use_default_bypass.unwrap_or(true); 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::config::Config;
use crate::feat; use crate::feat;
use crate::core::CoreManager;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@@ -172,7 +173,17 @@ impl Timer {
/// the task runner /// the task runner
async fn async_task(uid: String) { async fn async_task(uid: String) {
log::info!(target: "app", "running timer task `{uid}`"); 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);
}
} }
} }
-407
View File
@@ -1,407 +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,
)?));
#[allow(unused)]
let mut indication_icon = 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 is_template = crate::utils::help::is_monochrome_image_from_bytes(&indication_icon)
.unwrap_or(false);
let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&indication_icon)?));
let _ = tray.set_icon_as_template(is_template);
}
#[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)
}
}
+1 -7
View File
@@ -60,7 +60,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
global_merge, global_merge,
global_script, global_script,
profile_name, profile_name,
dns,
) = { ) = {
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles = profiles.latest(); let profiles = profiles.latest();
@@ -131,10 +130,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
.and_then(|item| item.name.clone()) .and_then(|item| item.name.clone())
.unwrap_or_default(); .unwrap_or_default();
let dns = current.get("dns").map_or(Mapping::new(), |val| {
val.as_mapping().cloned().unwrap_or(Mapping::new())
});
( (
current, current,
merge, merge,
@@ -145,7 +140,6 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
global_merge, global_merge,
global_script, global_script,
name, name,
dns,
) )
}; };
@@ -265,7 +259,7 @@ pub async fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
}); });
} }
config = use_tun(config, enable_tun, dns).await; config = use_tun(config, enable_tun).await;
config = use_sort(config); config = use_sort(config);
let mut exists_set = HashSet::new(); let mut exists_set = HashSet::new();
+125 -42
View File
@@ -1,55 +1,138 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_yaml::{Mapping, Sequence, Value}; use serde_yaml::{Mapping, Sequence, Value};
#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SeqMap { pub struct SeqMap {
prepend: Sequence, pub prepend: Sequence,
append: Sequence, pub append: Sequence,
delete: Sequence, pub delete: Vec<String>,
} }
pub fn use_seq(seq_map: SeqMap, config: Mapping, name: &str) -> Mapping { pub fn use_seq(seq: SeqMap, mut config: Mapping, field: &str) -> Mapping {
let mut prepend = seq_map.prepend; let SeqMap {
let append = seq_map.append; prepend,
let delete = seq_map.delete; append,
delete,
} = seq;
let origin_seq = config.get(name).map_or(Sequence::default(), |val| { let mut new_seq = Sequence::new();
val.as_sequence().unwrap_or(&Sequence::default()).clone() new_seq.extend(prepend);
});
let mut seq = origin_seq.clone();
let mut delete_names = Vec::new(); if let Some(Value::Sequence(origin)) = config.get(field) {
for item in delete { // Filter out deleted items
let item = item.clone(); let filtered: Sequence = origin
if let Some(name) = if item.is_string() { .iter()
Some(item) .filter(|item| {
} else { if let Value::String(s) = item {
item.get("name").cloned() !delete.contains(s)
} { } else if let Value::Mapping(m) = item {
delete_names.push(name.clone()); 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 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);
}
}
+28 -12
View File
@@ -18,7 +18,7 @@ macro_rules! append {
}; };
} }
pub async fn use_tun(mut config: Mapping, enable: bool, origin_dns_val: Mapping) -> Mapping { pub async fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
let tun_key = Value::from("tun"); let tun_key = Value::from("tun");
let tun_val = config.get(&tun_key); let tun_val = config.get(&tun_key);
let mut tun_val = tun_val.map_or(Mapping::new(), |val| { let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
@@ -36,20 +36,36 @@ pub async fn use_tun(mut config: Mapping, enable: bool, origin_dns_val: Mapping)
.unwrap_or(false); .unwrap_or(false);
if enable { if enable {
revise!(dns_val, "enable", true); // 检查现有的 enhanced-mode 设置
revise!(dns_val, "ipv6", ipv6_val); let current_mode = dns_val
revise!(dns_val, "enhanced-mode", "fake-ip"); .get(&Value::from("enhanced-mode"))
revise!(dns_val, "fake-ip-range", "198.18.0.1/16"); .and_then(|v| v.as_str())
#[cfg(target_os = "macos")] .unwrap_or("fake-ip");
{
crate::utils::resolve::restore_public_dns().await; // 只有当 enhanced-mode 是 fake-ip 或未设置时才修改 DNS 配置
crate::utils::resolve::set_public_dns("223.6.6.6".to_string()).await; 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 { } else {
revise!( revise!(
dns_val, dns_val,
"enable", "enable",
origin_dns_val dns_val
.get("enable") .get("enable")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(true) .unwrap_or(true)
@@ -60,7 +76,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool, origin_dns_val: Mapping)
revise!( revise!(
dns_val, dns_val,
"enhanced-mode", "enhanced-mode",
origin_dns_val dns_val
.get("enhanced-mode") .get("enhanced-mode")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("redir-host") .unwrap_or("redir-host")
@@ -69,7 +85,7 @@ pub async fn use_tun(mut config: Mapping, enable: bool, origin_dns_val: Mapping)
revise!( revise!(
dns_val, dns_val,
"fake-ip-range", "fake-ip-range",
origin_dns_val dns_val
.get("fake-ip-range") .get("fake-ip-range")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("198.18.0.1/16") .unwrap_or("198.18.0.1/16")
+189 -40
View File
@@ -4,6 +4,7 @@
//! - timer 定时器 //! - timer 定时器
//! - cmds 页面调用 //! - cmds 页面调用
//! //!
use crate::cmds;
use crate::config::*; use crate::config::*;
use crate::core::*; use crate::core::*;
use crate::log_err; use crate::log_err;
@@ -16,18 +17,46 @@ use std::fs;
use tauri::Manager; use tauri::Manager;
use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use std::env;
// 打开面板 // 打开面板
#[allow(dead_code)]
pub fn open_or_close_dashboard() { 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 Some(window) = handle::Handle::global().get_window() {
println!("Found existing window");
log::info!(target: "app", "Found existing window");
// 如果窗口存在,则切换其显示状态 // 如果窗口存在,则切换其显示状态
if window.is_visible().unwrap_or(false) { match window.is_visible() {
let _ = window.hide(); Ok(visible) => {
} else { println!("Window visibility status: {}", visible);
let _ = window.show(); log::info!(target: "app", "Window visibility status: {}", visible);
let _ = window.set_focus();
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 { } 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();
} }
} }
@@ -56,16 +85,44 @@ pub fn restart_app() {
resolve::resolve_reset(); resolve::resolve_reset();
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
std::thread::sleep(std::time::Duration::from_secs(1)); std::thread::sleep(std::time::Duration::from_secs(1));
let _ = app_handle.save_window_state(StateFlags::default());
tauri::process::restart(&app_handle.env()); 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 // 切换模式 rule/global/direct/script mode
pub fn change_clash_mode(mode: String) { pub fn change_clash_mode(mode: String) {
let mut mapping = Mapping::new(); let mut mapping = Mapping::new();
mapping.insert(Value::from("mode"), mode.clone().into()); mapping.insert(Value::from("mode"), mode.clone().into());
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
log::debug!(target: "app", "change clash mode to {mode}"); log::debug!(target: "app", "change clash mode to {mode}");
@@ -76,7 +133,8 @@ pub fn change_clash_mode(mode: String) {
if Config::clash().data().save_config().is_ok() { if Config::clash().data().save_config().is_ok() {
handle::Handle::refresh_clash(); 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}"), Err(err) => log::error!(target: "app", "{err}"),
@@ -93,7 +151,7 @@ pub fn toggle_system_proxy() {
match patch_verge(IVerge { match patch_verge(IVerge {
enable_system_proxy: Some(!enable), enable_system_proxy: Some(!enable),
..IVerge::default() ..IVerge::default()
}) }, false)
.await .await
{ {
Ok(_) => handle::Handle::refresh_verge(), Ok(_) => handle::Handle::refresh_verge(),
@@ -102,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模式 // 切换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 = Config::verge().data().enable_tun_mode;
let enable = enable.unwrap_or(false); let enable = enable.unwrap_or(false);
@@ -111,7 +184,7 @@ pub fn toggle_tun_mode() {
match patch_verge(IVerge { match patch_verge(IVerge {
enable_tun_mode: Some(!enable), enable_tun_mode: Some(!enable),
..IVerge::default() ..IVerge::default()
}) }, not_save_file.unwrap_or(false))
.await .await
{ {
Ok(_) => handle::Handle::refresh_verge(), Ok(_) => handle::Handle::refresh_verge(),
@@ -123,6 +196,7 @@ pub fn toggle_tun_mode() {
pub fn quit(code: Option<i32>) { pub fn quit(code: Option<i32>) {
let app_handle = handle::Handle::global().app_handle().unwrap(); let app_handle = handle::Handle::global().app_handle().unwrap();
handle::Handle::global().set_is_exiting(); handle::Handle::global().set_is_exiting();
toggle_tun_mode(Some(true));
resolve::resolve_reset(); resolve::resolve_reset();
log_err!(handle::Handle::global().get_window().unwrap().close()); log_err!(handle::Handle::global().get_window().unwrap().close());
app_handle.exit(code.unwrap_or(0)); app_handle.exit(code.unwrap_or(0));
@@ -139,7 +213,8 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
CoreManager::global().restart_core().await?; CoreManager::global().restart_core().await?;
} else { } else {
if patch.get("mode").is_some() { 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); Config::runtime().latest().patch_config(patch);
CoreManager::global().update_config().await?; CoreManager::global().update_config().await?;
@@ -162,7 +237,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> {
/// 修改verge的订阅 /// 修改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()); Config::verge().draft().patch_config(patch.clone());
let tun_mode = patch.enable_tun_mode; let tun_mode = patch.enable_tun_mode;
@@ -173,6 +248,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let proxy_bypass = patch.system_proxy_bypass; let proxy_bypass = patch.system_proxy_bypass;
let language = patch.language; let language = patch.language;
let mixed_port = patch.verge_mixed_port; let mixed_port = patch.verge_mixed_port;
let lite_mode = patch.enable_lite_mode;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let tray_icon = patch.tray_icon; let tray_icon = patch.tray_icon;
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
@@ -192,23 +268,33 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let socks_port = patch.verge_socks_port; let socks_port = patch.verge_socks_port;
let http_enabled = patch.verge_http_enabled; let http_enabled = patch.verge_http_enabled;
let http_port = patch.verge_port; 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 res: std::result::Result<(), anyhow::Error> = {
let mut should_restart_core = false; let mut should_restart_core = false;
let mut should_update_clash_config = 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_launch = false;
let mut should_update_sysproxy = 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() { if tun_mode.is_some() {
should_update_clash_config = true; 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"))] #[cfg(not(target_os = "windows"))]
if redir_enabled.is_some() || redir_port.is_some() { if redir_enabled.is_some() || redir_port.is_some() {
should_restart_core = true; should_restart_core = true;
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
if tproxy_enabled.is_some() || tproxy_port.is_some() { if tproxy_enabled.is_some() || tproxy_port.is_some() {
should_restart_core = true; should_restart_core = true;
@@ -224,25 +310,38 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
if auto_launch.is_some() { if auto_launch.is_some() {
should_update_launch = true; should_update_launch = true;
} }
if system_proxy.is_some()
|| proxy_bypass.is_some() if system_proxy.is_some() {
|| mixed_port.is_some() should_update_sysproxy = true;
|| pac.is_some() should_update_systray_menu = true;
|| pac_content.is_some() 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; should_update_sysproxy = true;
} }
if language.is_some() if language.is_some() {
|| system_proxy.is_some() should_update_systray_menu = true;
|| tun_mode.is_some() }
|| common_tray_icon.is_some() if common_tray_icon.is_some()
|| sysproxy_tray_icon.is_some() || sysproxy_tray_icon.is_some()
|| tun_tray_icon.is_some() || tun_tray_icon.is_some()
|| 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 { if should_restart_core {
CoreManager::global().restart_core().await?; CoreManager::global().restart_core().await?;
} }
@@ -250,6 +349,10 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
CoreManager::global().update_config().await?; CoreManager::global().update_config().await?;
handle::Handle::refresh_clash(); 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 { if should_update_launch {
sysopt::Sysopt::global().update_launch()?; sysopt::Sysopt::global().update_launch()?;
} }
@@ -258,12 +361,36 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
sysopt::Sysopt::global().update_sysproxy().await?; sysopt::Sysopt::global().update_sysproxy().await?;
} }
if let Some(hotkeys) = patch.hotkeys { if should_update_hotkey {
hotkey::Hotkey::global().update(hotkeys)?; hotkey::Hotkey::global().update(patch.hotkeys.unwrap())?;
} }
if should_update_systray_part { if should_update_systray_menu {
handle::Handle::update_systray_part()?; 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(()) <Result<()>>::Ok(())
@@ -271,7 +398,9 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
match res { match res {
Ok(()) => { Ok(()) => {
Config::verge().apply(); Config::verge().apply();
Config::verge().data().save_file()?; if !not_save_file {
Config::verge().data().save_file()?;
}
Ok(()) Ok(())
} }
@@ -285,6 +414,8 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
/// 更新某个profile /// 更新某个profile
/// 如果更新当前订阅就激活订阅 /// 如果更新当前订阅就激活订阅
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> { pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
println!("[订阅更新] 开始更新订阅 {}", uid);
let url_opt = { let url_opt = {
let profiles = Config::profiles(); let profiles = Config::profiles();
let profiles = profiles.latest(); let profiles = profiles.latest();
@@ -292,33 +423,44 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote"); let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
if !is_remote { if !is_remote {
None // 直接更新 println!("[订阅更新] {} 不是远程订阅,跳过更新", uid);
None // 非远程订阅直接更新
} else if item.url.is_none() { } else if item.url.is_none() {
println!("[订阅更新] {} 缺少URL,无法更新", uid);
bail!("failed to get the profile item url"); bail!("failed to get the profile item url");
} else { } else {
println!("[订阅更新] {} 是远程订阅,URL: {}", uid, item.url.clone().unwrap());
Some((item.url.clone().unwrap(), item.option.clone())) Some((item.url.clone().unwrap(), item.option.clone()))
} }
}; };
let should_update = match url_opt { let should_update = match url_opt {
Some((url, opt)) => { Some((url, opt)) => {
println!("[订阅更新] 开始下载新的订阅内容");
let merged_opt = PrfOption::merge(opt, option); let merged_opt = PrfOption::merge(opt, option);
let item = PrfItem::from_url(&url, None, None, merged_opt).await?; let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
println!("[订阅更新] 更新订阅配置");
let profiles = Config::profiles(); let profiles = Config::profiles();
let mut profiles = profiles.latest(); let mut profiles = profiles.latest();
profiles.update_item(uid.clone(), item)?; 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, None => true,
}; };
if should_update { if should_update {
println!("[订阅更新] 更新内核配置");
match CoreManager::global().update_config().await { match CoreManager::global().update_config().await {
Ok(_) => { Ok(_) => {
println!("[订阅更新] 更新成功");
handle::Handle::refresh_clash(); handle::Handle::refresh_clash();
} }
Err(err) => { Err(err) => {
println!("[订阅更新] 更新失败: {}", err);
handle::Handle::notice_message("set_config::error", format!("{err}")); handle::Handle::notice_message("set_config::error", format!("{err}"));
log::error!(target: "app", "{err}"); log::error!(target: "app", "{err}");
} }
@@ -330,15 +472,20 @@ pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()
/// copy env variable /// copy env variable
pub fn copy_clash_env() { 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 app_handle = handle::Handle::global().app_handle().unwrap();
let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) }; let port = { Config::verge().latest().verge_mixed_port.unwrap_or(7897) };
let http_proxy = format!("http://127.0.0.1:{}", port); let http_proxy = format!("http://{clash_verge_rev_ip}:{}", port);
let socks5_proxy = format!("socks5://127.0.0.1:{}", port); let socks5_proxy = format!("socks5://{clash_verge_rev_ip}:{}", port);
let sh = let sh =
format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}"); format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}"); let cmd: String = format!("set http_proxy={http_proxy}\r\nset https_proxy={http_proxy}");
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\""); let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
let nu: String =
format!("load-env {{ http_proxy: \"{http_proxy}\", https_proxy: \"{http_proxy}\" }}");
let cliboard = app_handle.clipboard(); let cliboard = app_handle.clipboard();
let env_type = { Config::verge().latest().env_type.clone() }; let env_type = { Config::verge().latest().env_type.clone() };
@@ -357,6 +504,7 @@ pub fn copy_clash_env() {
"bash" => cliboard.write_text(sh).unwrap_or_default(), "bash" => cliboard.write_text(sh).unwrap_or_default(),
"cmd" => cliboard.write_text(cmd).unwrap_or_default(), "cmd" => cliboard.write_text(cmd).unwrap_or_default(),
"powershell" => cliboard.write_text(ps).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}"), _ => log::error!(target: "app", "copy_clash_env: Invalid env type! {env_type}"),
}; };
} }
@@ -468,11 +616,12 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> {
log_err!( log_err!(
patch_verge(IVerge { patch_verge(IVerge {
webdav_url: webdav_url, webdav_url,
webdav_username: webdav_username, webdav_username,
webdav_password: webdav_password, webdav_password,
..IVerge::default() ..IVerge::default()
}) },
false)
.await .await
); );
// 最后删除临时文件 // 最后删除临时文件
+27 -22
View File
@@ -6,9 +6,9 @@ mod feat;
mod utils; mod utils;
use crate::core::hotkey; use crate::core::hotkey;
use crate::utils::{resolve, resolve::resolve_scheme, server}; use crate::utils::{resolve, resolve::resolve_scheme, server};
#[cfg(target_os = "macos")] use config::Config;
use tauri::Listener;
use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt;
pub fn run() { pub fn run() {
// 单例检测 // 单例检测
@@ -48,30 +48,22 @@ pub fn run() {
.plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_window_state::Builder::default().build()) .plugin(tauri_plugin_window_state::Builder::default().build())
.setup(|app| { .setup(|app| {
#[cfg(target_os = "linux")] #[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{ {
use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_deep_link::DeepLinkExt;
log_err!(app.deep_link().register_all()); log_err!(app.deep_link().register_all());
} }
#[cfg(target_os = "macos")]
{ app.deep_link().on_open_url(|event| {
app.listen("deep-link://new-url", |event| { tauri::async_runtime::spawn(async move {
tauri::async_runtime::spawn(async move { if let Some(url) = event.urls().first() {
let payload = event.payload(); log_err!(resolve_scheme(url.to_string()).await);
log_err!(resolve_scheme(payload.to_owned()).await); }
});
}); });
} });
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
resolve::resolve_setup(app).await; 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(()) Ok(())
@@ -86,12 +78,12 @@ pub fn run() {
cmds::open_core_dir, cmds::open_core_dir,
cmds::get_portable_flag, cmds::get_portable_flag,
cmds::get_network_interfaces, cmds::get_network_interfaces,
// cmds::kill_sidecar,
cmds::restart_core, cmds::restart_core,
cmds::restart_app, cmds::restart_app,
// clash // clash
cmds::get_clash_info, cmds::get_clash_info,
cmds::patch_clash_config, cmds::patch_clash_config,
cmds::patch_clash_mode,
cmds::change_clash_core, cmds::change_clash_core,
cmds::get_runtime_config, cmds::get_runtime_config,
cmds::get_runtime_yaml, cmds::get_runtime_yaml,
@@ -109,7 +101,6 @@ pub fn run() {
cmds::open_devtools, cmds::open_devtools,
cmds::exit_app, cmds::exit_app,
cmds::get_network_interfaces_info, cmds::get_network_interfaces_info,
// cmds::update_hotkeys,
// profile // profile
cmds::get_profiles, cmds::get_profiles,
cmds::enhance_profiles, cmds::enhance_profiles,
@@ -123,6 +114,9 @@ pub fn run() {
cmds::delete_profile, cmds::delete_profile,
cmds::read_profile_file, cmds::read_profile_file,
cmds::save_profile_file, cmds::save_profile_file,
// script validation
cmds::script_validate_notice,
cmds::validate_script_file,
// clash api // clash api
cmds::clash_api_get_proxy_delay, cmds::clash_api_get_proxy_delay,
// backup // backup
@@ -146,7 +140,6 @@ pub fn run() {
tauri::RunEvent::ExitRequested { api, code, .. } => { tauri::RunEvent::ExitRequested { api, code, .. } => {
if code.is_none() { if code.is_none() {
api.prevent_exit(); api.prevent_exit();
return;
} }
} }
tauri::RunEvent::WindowEvent { label, event, .. } => { tauri::RunEvent::WindowEvent { label, event, .. } => {
@@ -171,6 +164,12 @@ pub fn run() {
{ {
log_err!(hotkey::Hotkey::global().register("Control+Q", "quit")); 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) => { tauri::WindowEvent::Focused(false) => {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -181,6 +180,12 @@ pub fn run() {
{ {
log_err!(hotkey::Hotkey::global().unregister("Control+Q")); 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 => { tauri::WindowEvent::Destroyed => {
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
+32 -18
View File
@@ -99,7 +99,7 @@ pub fn get_last_part_and_decode(url: &str) -> Option<String> {
/// open file /// open file
pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> { pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> {
open::that_detached(&path.as_os_str())?; open::that_detached(path.as_os_str())?;
Ok(()) Ok(())
} }
@@ -201,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] #[test]
fn test_parse_value() { fn test_format_bytes_speed() {
let test_1 = "upload=111; download=2222; total=3333; expire=444"; assert_eq!(format_bytes_speed(0), "0B/s");
let test_2 = "attachment; filename=Clash.yaml"; assert_eq!(format_bytes_speed(1023), "1023B/s");
assert_eq!(format_bytes_speed(1024), "1.0KB/s");
assert_eq!(parse_str::<usize>(test_1, "upload").unwrap(), 111); assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s");
assert_eq!(parse_str::<usize>(test_1, "download").unwrap(), 2222); assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s");
assert_eq!(parse_str::<usize>(test_1, "total").unwrap(), 3333); assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s");
assert_eq!(parse_str::<usize>(test_1, "expire").unwrap(), 444); assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s");
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);
} }
+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()
}
+1
View File
@@ -5,3 +5,4 @@ pub mod init;
pub mod resolve; pub mod resolve;
pub mod server; pub mod server;
pub mod tmpl; pub mod tmpl;
pub mod i18n;
+56 -27
View File
@@ -1,7 +1,7 @@
use crate::config::IVerge; use crate::config::IVerge;
use crate::utils::error; use crate::utils::error;
use crate::{config::Config, config::PrfItem, core::*, utils::init, utils::server}; 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 anyhow::{bail, Result};
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use percent_encoding::percent_decode_str; use percent_encoding::percent_decode_str;
@@ -92,24 +92,31 @@ pub async fn resolve_setup(app: &mut App) {
server::embed_server(); server::embed_server();
log::trace!(target: "app", "init system tray"); 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 }; let silent_start = { Config::verge().data().enable_silent_start };
if !silent_start.unwrap_or(false) { if !silent_start.unwrap_or(false) {
create_window(); create_window();
} }
log_err!(sysopt::Sysopt::global().update_sysproxy().await); log_err!(tray::Tray::global().update_part());
log_err!(sysopt::Sysopt::global().init_guard_sysproxy());
log_err!(handle::Handle::update_systray_part());
log_err!(hotkey::Hotkey::global().init());
log_err!(timer::Timer::global().init()); log_err!(timer::Timer::global().init());
} }
/// reset system proxy /// reset system proxy
pub fn resolve_reset() { pub fn resolve_reset() {
tauri::async_runtime::block_on(async move { 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!(sysopt::Sysopt::global().reset_sysproxy().await);
log_err!(CoreManager::global().stop_core().await); log_err!(CoreManager::global().stop_core().await);
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -119,37 +126,46 @@ pub fn resolve_reset() {
/// create main window /// create main window
pub fn create_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(); let app_handle = handle::Handle::global().app_handle().unwrap();
if let Some(window) = handle::Handle::global().get_window() { if let Some(window) = handle::Handle::global().get_window() {
trace_err!(window.show(), "set win visible"); println!("Found existing window, trying to show it");
trace_err!(window.set_focus(), "set win focus"); 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; return;
} }
println!("Creating new window");
log::info!(target: "app", "Creating new window");
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let _ = { let window = tauri::WebviewWindowBuilder::new(
let app_handle = app_handle.clone();
std::thread::spawn(move || {
tauri::WebviewWindowBuilder::new(
&app_handle, &app_handle,
"main".to_string(), "main".to_string(),
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
) )
.title("Clash Verge") .title("Clash Verge")
.visible(false)
.inner_size(890.0, 700.0) .inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0) .min_inner_size(620.0, 550.0)
.decorations(false) .decorations(false)
.maximizable(true) .maximizable(true)
.additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling") .additional_browser_args("--enable-features=msWebView2EnableDraggableRegions --disable-features=OverscrollHistoryNavigation,msExperimentalScrolling")
.transparent(true) .transparent(true)
.build() .shadow(true)
}).join().unwrap() .build();
}.unwrap();
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let _ = tauri::WebviewWindowBuilder::new( let window = tauri::WebviewWindowBuilder::new(
&app_handle, &app_handle,
"main".to_string(), "main".to_string(),
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
@@ -159,11 +175,10 @@ pub fn create_window() {
.title_bar_style(tauri::TitleBarStyle::Overlay) .title_bar_style(tauri::TitleBarStyle::Overlay)
.inner_size(890.0, 700.0) .inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0) .min_inner_size(620.0, 550.0)
.build() .build();
.unwrap();
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
let _ = tauri::WebviewWindowBuilder::new( let window = tauri::WebviewWindowBuilder::new(
&app_handle, &app_handle,
"main".to_string(), "main".to_string(),
tauri::WebviewUrl::App("index.html".into()), tauri::WebviewUrl::App("index.html".into()),
@@ -173,8 +188,23 @@ pub fn create_window() {
.inner_size(890.0, 700.0) .inner_size(890.0, 700.0)
.min_inner_size(620.0, 550.0) .min_inner_size(620.0, 550.0)
.transparent(true) .transparent(true)
.build() .build();
.unwrap();
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) => {
println!("Failed to create window: {:?}", e);
log::error!(target: "app", "Failed to create window: {:?}", e);
}
}
} }
pub async fn resolve_scheme(param: String) -> Result<()> { pub async fn resolve_scheme(param: String) -> Result<()> {
@@ -220,6 +250,8 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
Ok(item) => { Ok(item) => {
let uid = item.uid.clone().unwrap(); let uid = item.uid.clone().unwrap();
let _ = wrap_err!(Config::profiles().data().append_item(item)); let _ = wrap_err!(Config::profiles().data().append_item(item));
handle::Handle::notice_message("import_sub_url::ok", uid);
app_handle app_handle
.notification() .notification()
.builder() .builder()
@@ -227,10 +259,9 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
.body("Import profile success") .body("Import profile success")
.show() .show()
.unwrap(); .unwrap();
handle::Handle::notice_message("import_sub_url::ok", uid);
} }
Err(e) => { Err(e) => {
handle::Handle::notice_message("import_sub_url::error", e.to_string());
app_handle app_handle
.notification() .notification()
.builder() .builder()
@@ -238,8 +269,6 @@ pub async fn resolve_scheme(param: String) -> Result<()> {
.body(format!("Import profile failed: {e}")) .body(format!("Import profile failed: {e}"))
.show() .show()
.unwrap(); .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.icns",
"icons/icon.ico" "icons/icon.ico"
], ],
"resources": ["resources"], "resources": ["resources", "resources/locales/*"],
"publisher": "Clash Verge Rev", "publisher": "Clash Verge Rev",
"externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"], "externalBin": ["sidecar/verge-mihomo", "sidecar/verge-mihomo-alpha"],
"copyright": "GNU General Public License v3.0", "copyright": "GNU General Public License v3.0",
"category": "DeveloperTool", "category": "DeveloperTool",
"shortDescription": "Clash Verge Rev", "shortDescription": "Clash Verge Rev",
"createUpdaterArtifacts": "v1Compatible" "createUpdaterArtifacts": true
}, },
"build": { "build": {
"beforeBuildCommand": "pnpm run web:build", "beforeBuildCommand": "pnpm run web:build",
@@ -25,7 +25,7 @@
"devUrl": "http://localhost:3000/" "devUrl": "http://localhost:3000/"
}, },
"productName": "Clash Verge", "productName": "Clash Verge",
"version": "2.0.2", "version": "2.1.2",
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.clash-verge-rev.clash-verge-rev",
"plugins": { "plugins": {
"updater": { "updater": {
@@ -33,7 +33,10 @@
"endpoints": [ "endpoints": [
"https://download.clashverge.dev/https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update-proxy.json", "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" "https://github.com/clash-verge-rev/clash-verge-rev/releases/download/updater/update.json"
] ],
"windows": {
"installMode": "basicUi"
}
}, },
"deep-link": { "deep-link": {
"desktop": { "desktop": {
+1 -2
View File
@@ -3,13 +3,12 @@
"identifier": "io.github.clash-verge-rev.clash-verge-rev", "identifier": "io.github.clash-verge-rev.clash-verge-rev",
"bundle": { "bundle": {
"targets": ["app", "dmg"], "targets": ["app", "dmg"],
"resources": ["resources"],
"macOS": { "macOS": {
"frameworks": [], "frameworks": [],
"minimumSystemVersion": "10.15", "minimumSystemVersion": "10.15",
"exceptionDomain": "", "exceptionDomain": "",
"signingIdentity": null, "signingIdentity": null,
"entitlements": null, "entitlements": "packages/macos/entitlements.plist",
"dmg": { "dmg": {
"background": "images/background.png", "background": "images/background.png",
"appPosition": { "appPosition": {
+1 -1
View File
@@ -24,7 +24,7 @@
"trayIcon": { "trayIcon": {
"iconPath": "icons/tray-icon.ico", "iconPath": "icons/tray-icon.ico",
"iconAsTemplate": true, "iconAsTemplate": true,
"menuOnLeftClick": false "showMenuOnLeftClick": false
}, },
"windows": [] "windows": []
} }
+1 -1
View File
@@ -111,7 +111,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
right: 2px; right: 1px;
bottom: 0px; bottom: 0px;
} }
} }
+37 -7
View File
@@ -18,7 +18,7 @@ interface InnerProps {
} }
const NoticeInner = (props: InnerProps) => { const NoticeInner = (props: InnerProps) => {
const { type, message, duration = 1500, onClose } = props; const { type, message, duration, onClose } = props;
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
const [isDark, setIsDark] = useState(false); const [isDark, setIsDark] = useState(false);
const { verge } = useVerge(); const { verge } = useVerge();
@@ -43,7 +43,7 @@ const NoticeInner = (props: InnerProps) => {
appWindow.theme().then((m) => m && setIsDark(m === "dark")); appWindow.theme().then((m) => m && setIsDark(m === "dark"));
const unlisten = appWindow.onThemeChanged((e) => const unlisten = appWindow.onThemeChanged((e) =>
setIsDark(e.payload === "dark") setIsDark(e.payload === "dark"),
); );
return () => { return () => {
@@ -72,7 +72,7 @@ const NoticeInner = (props: InnerProps) => {
<Snackbar <Snackbar
open={visible} open={visible}
anchorOrigin={{ vertical: "top", horizontal: "right" }} anchorOrigin={{ vertical: "top", horizontal: "right" }}
autoHideDuration={duration} autoHideDuration={duration === -1 ? null : duration || 1500}
onClose={onAutoClose} onClose={onAutoClose}
message={msgElement} message={msgElement}
sx={{ sx={{
@@ -105,25 +105,55 @@ let parent: HTMLDivElement = null!;
// @ts-ignore // @ts-ignore
export const Notice: NoticeInstance = (props) => { export const Notice: NoticeInstance = (props) => {
const { type, message, duration } = props;
// 验证必要的参数
if (!message) {
return;
}
if (!parent) { if (!parent) {
parent = document.createElement("div"); parent = document.createElement("div");
parent.setAttribute("id", "notice-container"); // 添加 id 便于调试
document.body.appendChild(parent); document.body.appendChild(parent);
} }
const container = document.createElement("div"); const container = document.createElement("div");
parent.appendChild(container); parent.appendChild(container);
const root = createRoot(container); const root = createRoot(container);
const onUnmount = () => { const onUnmount = () => {
root.unmount(); 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) => { (["info", "error", "success"] as const).forEach((type) => {
Notice[type] = (message, duration) => { Notice[type] = (message: ReactNode, duration?: number) => {
setTimeout(() => Notice({ type, message, duration }), 0); // 确保消息不为空
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 { Box, SvgIcon, TextField, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip"; 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 { useTranslation } from "react-i18next";
import matchCaseIcon from "@/assets/image/component/match_case.svg?react"; import matchCaseIcon from "@/assets/image/component/match_case.svg?react";
@@ -22,7 +22,20 @@ type SearchProps = {
onSearch: (match: (content: string) => boolean, state: SearchState) => void; 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 { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [matchCase, setMatchCase] = useState(props.matchCase ?? false); const [matchCase, setMatchCase] = useState(props.matchCase ?? false);
@@ -43,58 +56,58 @@ export const BaseSearchBox = styled((props: SearchProps) => {
inheritViewBox: true, inheritViewBox: true,
}; };
useEffect(() => { const createMatcher = useMemo(() => {
if (!inputRef.current) return; return (searchText: string) => {
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) => {
try { try {
let searchItemCopy = searchItem; return (content: string) => {
if (!matchCase) { if (!searchText) return true;
item = item.toLowerCase();
searchItemCopy = searchItemCopy.toLowerCase(); let item = !matchCase ? content.toLowerCase() : content;
} let searchItem = !matchCase ? searchText.toLowerCase() : searchText;
if (matchWholeWord) {
const regex = new RegExp(`\\b${searchItemCopy}\\b`);
if (useRegularExpression) { if (useRegularExpression) {
const regexWithOptions = new RegExp(searchItemCopy); return new RegExp(searchItem).test(item);
return regexWithOptions.test(item) && regex.test(item);
} else {
return regex.test(item);
} }
} else if (useRegularExpression) {
const regex = new RegExp(searchItemCopy); if (matchWholeWord) {
return regex.test(item); return new RegExp(`\\b${searchItem}\\b`).test(item);
} else { }
return item.includes(searchItemCopy);
} return item.includes(searchItem);
};
} catch (err) { } catch (err) {
setErrorMessage(`${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 ( return (
<Tooltip title={errorMessage} placement="bottom-start"> <Tooltip title={errorMessage} placement="bottom-start">
<TextField <StyledTextField
autoComplete="new-password" autoComplete="new-password"
inputRef={inputRef} inputRef={inputRef}
hiddenLabel hiddenLabel
@@ -115,9 +128,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
component={matchCaseIcon} component={matchCaseIcon}
{...iconStyle} {...iconStyle}
aria-label={matchCase ? "active" : "inactive"} aria-label={matchCase ? "active" : "inactive"}
onClick={() => { onClick={() => setMatchCase(!matchCase)}
setMatchCase(!matchCase);
}}
/> />
</div> </div>
</Tooltip> </Tooltip>
@@ -127,9 +138,7 @@ export const BaseSearchBox = styled((props: SearchProps) => {
component={matchWholeWordIcon} component={matchWholeWordIcon}
{...iconStyle} {...iconStyle}
aria-label={matchWholeWord ? "active" : "inactive"} aria-label={matchWholeWord ? "active" : "inactive"}
onClick={() => { onClick={() => setMatchWholeWord(!matchWholeWord)}
setMatchWholeWord(!matchWholeWord);
}}
/> />
</div> </div>
</Tooltip> </Tooltip>
@@ -139,28 +148,16 @@ export const BaseSearchBox = styled((props: SearchProps) => {
component={useRegularExpressionIcon} component={useRegularExpressionIcon}
aria-label={useRegularExpression ? "active" : "inactive"} aria-label={useRegularExpression ? "active" : "inactive"}
{...iconStyle} {...iconStyle}
onClick={() => { onClick={() =>
setUseRegularExpression(!useRegularExpression); setUseRegularExpression(!useRegularExpression)
}} }
/>{" "} />{" "}
</div> </div>
</Tooltip> </Tooltip>
</Box> </Box>
), ),
}} }}
{...props}
/> />
</Tooltip> </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",
},
}));
@@ -52,7 +52,10 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule; const rule = rulePayload ? `${data.rule}(${rulePayload})` : data.rule;
const host = metadata.host const host = metadata.host
? `${metadata.host}:${metadata.destinationPort}` ? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`; : `${metadata.remoteDestination}:${metadata.destinationPort}`;
const Destination = metadata.destinationIP
? metadata.destinationIP
: metadata.remoteDestination;
const information = [ const information = [
{ label: t("Host"), value: host }, { label: t("Host"), value: host },
@@ -79,7 +82,8 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => {
label: t("Source"), label: t("Source"),
value: `${metadata.sourceIP}:${metadata.sourcePort}`, 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})` }, { label: t("Type"), value: `${metadata.type}(${metadata.network})` },
]; ];
+79 -20
View File
@@ -1,6 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useMemo, useState } from "react"; import { useMemo, useState, useEffect } from "react";
import { DataGrid, GridColDef } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridColumnResizeParams } from "@mui/x-data-grid";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
import { truncateStr } from "@/utils/truncate-str"; import { truncateStr } from "@/utils/truncate-str";
import parseTraffic from "@/utils/parse-traffic"; import parseTraffic from "@/utils/parse-traffic";
@@ -21,12 +21,24 @@ export const ConnectionTable = (props: Props) => {
Partial<Record<keyof IConnectionsItem, boolean>> 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[]>([ 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", field: "download",
headerName: t("Downloaded"), headerName: t("Downloaded"),
width: 88, width: columnWidths["download"] || 88,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" "), valueFormatter: (value: number) => parseTraffic(value).join(" "),
@@ -34,7 +46,7 @@ export const ConnectionTable = (props: Props) => {
{ {
field: "upload", field: "upload",
headerName: t("Uploaded"), headerName: t("Uploaded"),
width: 88, width: columnWidths["upload"] || 88,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" "), valueFormatter: (value: number) => parseTraffic(value).join(" "),
@@ -42,7 +54,7 @@ export const ConnectionTable = (props: Props) => {
{ {
field: "dlSpeed", field: "dlSpeed",
headerName: t("DL Speed"), headerName: t("DL Speed"),
width: 88, width: columnWidths["dlSpeed"] || 88,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s", valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
@@ -50,18 +62,33 @@ export const ConnectionTable = (props: Props) => {
{ {
field: "ulSpeed", field: "ulSpeed",
headerName: t("UL Speed"), headerName: t("UL Speed"),
width: 88, width: columnWidths["ulSpeed"] || 88,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s", valueFormatter: (value: number) => parseTraffic(value).join(" ") + "/s",
}, },
{ field: "chains", headerName: t("Chains"), flex: 360, minWidth: 360 }, {
{ field: "rule", headerName: t("Rule"), flex: 300, minWidth: 250 }, field: "chains",
{ field: "process", headerName: t("Process"), flex: 240, minWidth: 120 }, 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", field: "time",
headerName: t("Time"), headerName: t("Time"),
flex: 120, width: columnWidths["time"] || 120,
minWidth: 100, minWidth: 100,
align: "right", align: "right",
headerAlign: "right", headerAlign: "right",
@@ -69,26 +96,56 @@ export const ConnectionTable = (props: Props) => {
new Date(v2).getTime() - new Date(v1).getTime(), new Date(v2).getTime() - new Date(v1).getTime(),
valueFormatter: (value: number) => dayjs(value).fromNow(), valueFormatter: (value: number) => dayjs(value).fromNow(),
}, },
{ field: "source", headerName: t("Source"), flex: 200, minWidth: 130 },
{ {
field: "destinationIP", field: "source",
headerName: t("Destination IP"), headerName: t("Source"),
flex: 200, width: columnWidths["source"] || 200,
minWidth: 130, 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(() => { const connRows = useMemo(() => {
return connections.map((each) => { return connections.map((each) => {
const { metadata, rulePayload } = each; const { metadata, rulePayload } = each;
const chains = [...each.chains].reverse().join(" / "); const chains = [...each.chains].reverse().join(" / ");
const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule; const rule = rulePayload ? `${each.rule}(${rulePayload})` : each.rule;
const Destination = metadata.destinationIP
? `${metadata.destinationIP}:${metadata.destinationPort}`
: `${metadata.remoteDestination}:${metadata.destinationPort}`;
return { return {
id: each.id, id: each.id,
host: metadata.host host: metadata.host
? `${metadata.host}:${metadata.destinationPort}` ? `${metadata.host}:${metadata.destinationPort}`
: `${metadata.destinationIP}:${metadata.destinationPort}`, : `${metadata.remoteDestination}:${metadata.destinationPort}`,
download: each.download, download: each.download,
upload: each.upload, upload: each.upload,
dlSpeed: each.curDownload, dlSpeed: each.curDownload,
@@ -98,7 +155,7 @@ export const ConnectionTable = (props: Props) => {
process: truncateStr(metadata.process || metadata.processPath), process: truncateStr(metadata.process || metadata.processPath),
time: each.start, time: each.start,
source: `${metadata.sourceIP}:${metadata.sourcePort}`, source: `${metadata.sourceIP}:${metadata.sourcePort}`,
destinationIP: metadata.destinationIP, remoteDestination: Destination,
type: `${metadata.type}(${metadata.network})`, type: `${metadata.type}(${metadata.network})`,
connectionData: each, connectionData: each,
}; };
@@ -115,12 +172,14 @@ export const ConnectionTable = (props: Props) => {
sx={{ sx={{
border: "none", border: "none",
"div:focus": { outline: "none !important" }, "div:focus": { outline: "none !important" },
"& div[aria-rowindex]": { "& .MuiDataGrid-columnHeader": {
backgroundColor: `${backgroundColor} !important`, userSelect: "none",
}, },
}} }}
columnVisibilityModel={columnVisible} columnVisibilityModel={columnVisible}
onColumnVisibilityModelChange={(e) => setColumnVisible(e)} onColumnVisibilityModelChange={(e) => setColumnVisible(e)}
onColumnResize={handleColumnResize}
disableColumnMenu={false}
/> />
); );
}; };
+2 -2
View File
@@ -48,7 +48,7 @@ export const LayoutTraffic = () => {
const { server = "", secret = "" } = clashInfo!; const { server = "", secret = "" } = clashInfo!;
const s = createSockette( const s = createSockette(
`ws://${server}/traffic?token=${encodeURIComponent(secret)}`, `ws://${server}${secret ? `/traffic?token=${encodeURIComponent(secret)}` : "/traffic"}`,
{ {
onmessage(event) { onmessage(event) {
const data = JSON.parse(event.data) as ITrafficItem; const data = JSON.parse(event.data) as ITrafficItem;
@@ -86,7 +86,7 @@ export const LayoutTraffic = () => {
const { server = "", secret = "" } = clashInfo!; const { server = "", secret = "" } = clashInfo!;
const s = createSockette( const s = createSockette(
`ws://${server}/memory?token=${encodeURIComponent(secret)}`, `ws://${server}${secret ? `/memory?token=${encodeURIComponent(secret)}` : "/memory"}`,
{ {
onmessage(event) { onmessage(event) {
const data = JSON.parse(event.data) as MemoryUsage; const data = JSON.parse(event.data) as MemoryUsage;
+2 -2
View File
@@ -85,9 +85,9 @@ const LogItem = ({ value, searchState }: Props) => {
return ( return (
<Item> <Item>
<div> <div>
<span className="time">{value.time}</span> <span className="time">{renderHighlightText(value.time || "")}</span>
<span className="type" data-type={value.type.toLowerCase()}> <span className="type" data-type={value.type.toLowerCase()}>
{value.type} {renderHighlightText(value.type)}
</span> </span>
</div> </div>
<div> <div>
+2 -2
View File
@@ -106,7 +106,7 @@ export const EditorViewer = <T extends Language>(props: Props<T>) => {
}; };
const editorDidMount = async ( const editorDidMount = async (
editor: monaco.editor.IStandaloneCodeEditor editor: monaco.editor.IStandaloneCodeEditor,
) => { ) => {
editorRef.current = editor; 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"${ fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"${
getSystem() === "windows" ? ", twemoji mozilla" : "" getSystem() === "windows" ? ", twemoji mozilla" : ""
}`, }`,
fontLigatures: true, // 连字符 fontLigatures: false, // 连字符
smoothScrolling: true, // 平滑滚动 smoothScrolling: true, // 平滑滚动
}} }}
editorWillMount={editorWillMount} editorWillMount={editorWillMount}
+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 { useLockFn } from "ahooks";
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso";
import { import {
@@ -16,6 +16,173 @@ import { ProxyRender } from "./proxy-render";
import delayManager from "@/services/delay"; import delayManager from "@/services/delay";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ScrollTopButton } from "../layout/scroll-top-button"; 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 { interface Props {
mode: string; mode: string;
@@ -32,22 +199,128 @@ export const ProxyGroups = (props: Props) => {
const timeout = verge?.default_latency_timeout || 10000; const timeout = verge?.default_latency_timeout || 10000;
const virtuosoRef = useRef<VirtuosoHandle>(null); const virtuosoRef = useRef<VirtuosoHandle>(null);
const scrollPositionRef = useRef<Record<string, number>>({});
const [showScrollTop, setShowScrollTop] = useState(false); 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");
// 添加滚动处理函数 // 使用useMemo缓存字母索引数据
const handleScroll = (e: any) => { const { groupFirstLetters, letterIndexMap } = useMemo(() => {
const scrollTop = e.target.scrollTop; const letters = new Set<string>();
setShowScrollTop(scrollTop > 100); 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?.({ virtuosoRef.current?.scrollTo?.({
top: 0, top: 0,
behavior: "smooth", 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( 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") { if (mode === "direct") {
return <BaseEmpty text={t("clash_mode_direct")} />; return <BaseEmpty text={t("clash_mode_direct")} />;
} }
@@ -142,30 +473,57 @@ export const ProxyGroups = (props: Props) => {
<div style={{ position: "relative", height: "100%" }}> <div style={{ position: "relative", height: "100%" }}>
<Virtuoso <Virtuoso
ref={virtuosoRef} ref={virtuosoRef}
style={{ height: "calc(100% - 16px)" }} style={{ height: "calc(100% - 14px)" }}
totalCount={renderList.length} totalCount={renderList.length}
increaseViewportBy={256} increaseViewportBy={{ top: 256, bottom: 256 }}
overscan={150}
defaultItemHeight={56}
scrollerRef={(ref) => { scrollerRef={(ref) => {
if (ref) { scrollerRef.current = ref as Element;
ref.addEventListener("scroll", handleScroll); }}
} components={{
Footer: () => <div style={{ height: "8px" }} />,
}} }}
itemContent={(index) => ( itemContent={(index) => (
<> <ProxyRender
<ProxyRender key={renderList[index].key}
key={renderList[index].key} item={renderList[index]}
item={renderList[index]} indent={mode === "rule" || mode === "script"}
indent={mode === "rule" || mode === "script"} onLocation={handleLocation}
onLocation={handleLocation} onCheckAll={handleCheckAll}
onCheckAll={handleCheckAll} onHeadState={onHeadState}
onHeadState={onHeadState} onChangeProxy={handleChangeProxy}
onChangeProxy={handleChangeProxy} />
/>
</>
)} )}
/> />
<ScrollTopButton show={showScrollTop} onClick={scrollToTop} /> <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> </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 <ListItemText
primary={<StyledPrimary>{group.name}</StyledPrimary>} primary={<StyledPrimary>{group.name}</StyledPrimary>}
secondary={ secondary={
<ListItemTextChild <Box
sx={{ sx={{
overflow: "hidden", overflow: "hidden",
display: "flex", display: "flex",
@@ -108,16 +108,19 @@ export const ProxyRender = (props: RenderProps) => {
pt: "2px", pt: "2px",
}} }}
> >
<Box sx={{ marginTop: "2px" }}> <Box component="span" sx={{ marginTop: "2px" }}>
<StyledTypeBox>{group.type}</StyledTypeBox> <StyledTypeBox>{group.type}</StyledTypeBox>
<StyledSubtitle sx={{ color: "text.secondary" }}> <StyledSubtitle sx={{ color: "text.secondary" }}>
{group.now} {group.now}
</StyledSubtitle> </StyledSubtitle>
</Box> </Box>
</ListItemTextChild> </Box>
} }
secondaryTypographyProps={{ slotProps={{
sx: { display: "flex", alignItems: "center", color: "#ccc" }, secondary: {
component: "div",
sx: { display: "flex", alignItems: "center", color: "#ccc" },
},
}} }}
/> />
{headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />} {headState?.open ? <ExpandLessRounded /> : <ExpandMoreRounded />}
@@ -204,7 +207,7 @@ export const ProxyRender = (props: RenderProps) => {
}; };
const StyledPrimary = styled("span")` const StyledPrimary = styled("span")`
font-size: 15px; font-size: 16px;
font-weight: 700; font-weight: 700;
line-height: 1.5; line-height: 1.5;
overflow: hidden; overflow: hidden;
@@ -219,11 +222,7 @@ const StyledSubtitle = styled("span")`
white-space: nowrap; white-space: nowrap;
`; `;
const ListItemTextChild = styled("span")` const StyledTypeBox = styled(Box)(({ theme }) => ({
display: block;
`;
const StyledTypeBox = styled(ListItemTextChild)(({ theme }) => ({
display: "inline-block", display: "inline-block",
border: "1px solid #ccc", border: "1px solid #ccc",
borderColor: alpha(theme.palette.primary.main, 0.5), borderColor: alpha(theme.palette.primary.main, 0.5),
+83 -54
View File
@@ -1,5 +1,5 @@
import useSWR from "swr"; import useSWR from "swr";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo, useCallback } from "react";
import { getProxies } from "@/services/api"; import { getProxies } from "@/services/api";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { filterSort } from "./use-filter-sort"; import { filterSort } from "./use-filter-sort";
@@ -11,7 +11,7 @@ import {
} from "./use-head-state"; } from "./use-head-state";
export interface IRenderItem { export interface IRenderItem {
// 组 | head | item | empty | item col // 组 | head | item | empty | item col
type: 0 | 1 | 2 | 3 | 4; type: 0 | 1 | 2 | 3 | 4;
key: string; key: string;
group: IProxyGroupItem; group: IProxyGroupItem;
@@ -19,32 +19,59 @@ export interface IRenderItem {
col?: number; col?: number;
proxyCol?: IProxyItem[]; proxyCol?: IProxyItem[];
headState?: HeadState; 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) => { export const useRenderList = (mode: string) => {
const { data: proxiesData, mutate: mutateProxies } = useSWR( const { data: proxiesData, mutate: mutateProxies } = useSWR(
"getProxies", "getProxies",
getProxies, getProxies,
{ refreshInterval: 45000 }, {
refreshInterval: 2000,
revalidateOnFocus: false,
revalidateOnReconnect: true,
},
); );
const { verge } = useVerge(); const { verge } = useVerge();
const { width } = useWindowWidth(); 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(); 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(() => { useEffect(() => {
if (!proxiesData) return; if (!proxiesData) return;
const { groups, proxies } = proxiesData; const { groups, proxies } = proxiesData;
@@ -55,22 +82,29 @@ export const useRenderList = (mode: string) => {
) { ) {
setTimeout(() => mutateProxies(), 500); setTimeout(() => mutateProxies(), 500);
} }
}, [proxiesData, mode]); }, [proxiesData, mode, mutateProxies]);
// 处理渲染列表
const renderList: IRenderItem[] = useMemo(() => { const renderList: IRenderItem[] = useMemo(() => {
if (!proxiesData) return []; if (!proxiesData) return [];
// global 和 direct 使用展开的样式
const useRule = mode === "rule" || mode === "script"; const useRule = mode === "rule" || mode === "script";
const renderGroups = const renderGroups =
(useRule && proxiesData.groups.length useRule && proxiesData.groups.length
? proxiesData.groups ? proxiesData.groups
: [proxiesData.global!]) || []; : [proxiesData.global!];
const retList = renderGroups.flatMap((group) => { const retList = renderGroups.flatMap((group) => {
const headState = headStates[group.name] || DEFAULT_STATE; const headState = headStates[group.name] || DEFAULT_STATE;
const ret: IRenderItem[] = [ 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) { if (headState?.open || !useRule) {
@@ -81,61 +115,56 @@ export const useRenderList = (mode: string) => {
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) { if (!proxies.length) {
ret.push({ type: 3, key: `empty-${group.name}`, group, headState }); ret.push({
} type: 3,
key: `empty-${group.name}`,
// 支持多列布局 group,
if (col > 1) { headState,
});
} else if (col > 1) {
return ret.concat( return ret.concat(
groupList(proxies, col).map((proxyCol) => ({ groupProxies(proxies, col).map((proxyCol) => ({
type: 4, type: 4,
key: `col-${group.name}-${proxyCol[0].name}`, key: `col-${group.name}-${proxyCol[0].name}`,
group, group,
headState, headState,
col, col,
proxyCol, 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; return ret;
}); });
if (!useRule) return retList.slice(1); 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]); }, [headStates, proxiesData, mode, col]);
return { return {
renderList, renderList,
onProxies: mutateProxies, onProxies: mutateProxies,
onHeadState: setHeadState, 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[][]);
}
@@ -51,7 +51,7 @@ export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => {
mutate("getClashConfig"); mutate("getClashConfig");
mutate("getVersion"); mutate("getVersion");
}, 100); }, 100);
Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000); // Notice.success(t("Switched to _clash Core", { core: `${core}` }), 1000);
} catch (err: any) { } catch (err: any) {
Notice.error(err?.message || err.toString()); Notice.error(err?.message || err.toString());
} }
+18 -3
View File
@@ -1,7 +1,7 @@
import { forwardRef, useImperativeHandle, useState } from "react"; import { forwardRef, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { styled, Typography } from "@mui/material"; import { styled, Typography, Switch } from "@mui/material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice } from "@/components/base"; import { BaseDialog, DialogRef, Notice } from "@/components/base";
import { HotkeyInput } from "./hotkey-input"; import { HotkeyInput } from "./hotkey-input";
@@ -29,6 +29,9 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
const { verge, patchVerge } = useVerge(); const { verge, patchVerge } = useVerge();
const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({}); const [hotkeyMap, setHotkeyMap] = useState<Record<string, string[]>>({});
const [enableGlobalHotkey, setEnableHotkey] = useState(
verge?.enable_global_hotkey ?? true,
);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
@@ -69,7 +72,10 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
.filter(Boolean); .filter(Boolean);
try { try {
await patchVerge({ hotkeys }); await patchVerge({
hotkeys,
enable_global_hotkey: enableGlobalHotkey,
});
setOpen(false); setOpen(false);
} catch (err: any) { } catch (err: any) {
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
@@ -80,13 +86,22 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
<BaseDialog <BaseDialog
open={open} open={open}
title={t("Hotkey Setting")} title={t("Hotkey Setting")}
contentSx={{ width: 450, maxHeight: 330 }} contentSx={{ width: 450, maxHeight: 380 }}
okBtn={t("Save")} okBtn={t("Save")}
cancelBtn={t("Cancel")} cancelBtn={t("Cancel")}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
onCancel={() => setOpen(false)} onCancel={() => setOpen(false)}
onOk={onSave} 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) => ( {HOTKEY_FUNC.map((func) => (
<ItemWrapper key={func}> <ItemWrapper key={func}>
<Typography>{t(func)}</Typography> <Typography>{t(func)}</Typography>
@@ -8,6 +8,7 @@ import {
styled, styled,
ListItem, ListItem,
ListItemText, ListItemText,
Box,
} from "@mui/material"; } from "@mui/material";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
@@ -163,6 +164,21 @@ export const LayoutViewer = forwardRef<DialogRef>((props, ref) => {
</GuardState> </GuardState>
</Item> </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> <Item>
<ListItemText primary={t("Common Tray Icon")} /> <ListItemText primary={t("Common Tray Icon")} />
+37 -21
View File
@@ -1,7 +1,13 @@
import { forwardRef, useImperativeHandle, useMemo, useState } from "react"; import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base";
import { useLockFn } from "ahooks"; import { BaseFieldset } from "@/components/base/base-fieldset";
import { useTranslation } from "react-i18next"; 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 { import {
Button,
InputAdornment, InputAdornment,
List, List,
ListItem, ListItem,
@@ -9,16 +15,10 @@ import {
styled, styled,
TextField, TextField,
Typography, Typography,
Button,
} from "@mui/material"; } from "@mui/material";
import { useVerge } from "@/hooks/use-verge"; import { useLockFn } from "ahooks";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds"; import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
import { BaseDialog, DialogRef, Notice, Switch } from "@/components/base"; import { useTranslation } from "react-i18next";
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";
const DEFAULT_PAC = `function FindProxyForURL(url, host) { const DEFAULT_PAC = `function FindProxyForURL(url, host) {
return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; 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, 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, () => ({ useImperativeHandle(ref, () => ({
open: () => { open: () => {
setOpen(true); 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")); Notice.error(t("Proxy Daemon Duration Cannot be Less than 1 Second"));
return; return;
} }
if (value.bypass && !validReg.test(value.bypass)) {
Notice.error(t("Invalid Bypass Format"));
return;
}
const patch: Partial<IVergeConfig> = {}; const patch: Partial<IVergeConfig> = {};
@@ -124,6 +138,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (value.bypass !== system_proxy_bypass) { if (value.bypass !== system_proxy_bypass) {
patch.system_proxy_bypass = value.bypass; patch.system_proxy_bypass = value.bypass;
} }
if (value.pac !== proxy_auto_config) { if (value.pac !== proxy_auto_config) {
patch.proxy_auto_config = value.pac; patch.proxy_auto_config = value.pac;
} }
@@ -133,10 +148,7 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
if (value.pac_content !== pac_file_content) { if (value.pac_content !== pac_file_content) {
patch.pac_file_content = value.pac_content; patch.pac_file_content = value.pac_content;
} }
if (value.bypass && !validReg.test(value.bypass)) {
Notice.error(t("Invalid Bypass Format"));
return;
}
try { try {
await patchVerge(patch); await patchVerge(patch);
setOpen(false); setOpen(false);
@@ -215,7 +227,6 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
<ListItem sx={{ padding: "5px 2px" }}> <ListItem sx={{ padding: "5px 2px" }}>
<ListItemText primary={t("Guard Duration")} /> <ListItemText primary={t("Guard Duration")} />
<TextField <TextField
autoComplete="new-password"
disabled={!enabled} disabled={!enabled}
size="small" size="small"
value={value.duration} value={value.duration}
@@ -242,11 +253,11 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
/> />
</ListItem> </ListItem>
)} )}
{!value.pac && (
{!value.pac && !value.use_default && (
<> <>
<ListItemText primary={t("Proxy Bypass")} /> <ListItemText primary={t("Proxy Bypass")} />
<TextField <TextField
autoComplete="new-password"
error={value.bypass ? !validReg.test(value.bypass) : false} error={value.bypass ? !validReg.test(value.bypass) : false}
disabled={!enabled} disabled={!enabled}
size="small" size="small"
@@ -258,20 +269,25 @@ export const SysproxyViewer = forwardRef<DialogRef>((props, ref) => {
setValue((v) => ({ ...v, bypass: e.target.value })); setValue((v) => ({ ...v, bypass: e.target.value }));
}} }}
/> />
</>
)}
{!value.pac && value.use_default && (
<>
<ListItemText primary={t("Bypass")} /> <ListItemText primary={t("Bypass")} />
<FlexBox> <FlexBox>
<TextField <TextField
autoComplete="new-password"
disabled={true} disabled={true}
size="small" size="small"
multiline multiline
rows={4} rows={4}
sx={{ width: "100%" }} sx={{ width: "100%" }}
value={sysproxy?.bypass || "-"} value={defaultBypass()}
/> />
</FlexBox> </FlexBox>
</> </>
)} )}
{value.pac && ( {value.pac && (
<> <>
<ListItem sx={{ padding: "5px 2px", alignItems: "start" }}> <ListItem sx={{ padding: "5px 2px", alignItems: "start" }}>
+3 -3
View File
@@ -35,7 +35,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
open: () => { open: () => {
setOpen(true); setOpen(true);
setValues({ setValues({
stack: clash?.tun.stack ?? "mixed", stack: clash?.tun.stack ?? "gvisor",
device: clash?.tun.device ?? "Mihomo", device: clash?.tun.device ?? "Mihomo",
autoRoute: clash?.tun["auto-route"] ?? true, autoRoute: clash?.tun["auto-route"] ?? true,
autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true, autoDetectInterface: clash?.tun["auto-detect-interface"] ?? true,
@@ -89,7 +89,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
size="small" size="small"
onClick={async () => { onClick={async () => {
let tun = { let tun = {
stack: "mixed", stack: "gvisor",
device: "Mihomo", device: "Mihomo",
"auto-route": true, "auto-route": true,
"auto-detect-interface": true, "auto-detect-interface": true,
@@ -98,7 +98,7 @@ export const TunViewer = forwardRef<DialogRef>((props, ref) => {
mtu: 1500, mtu: 1500,
}; };
setValues({ setValues({
stack: "mixed", stack: "gvisor",
device: "Mihomo", device: "Mihomo",
autoRoute: true, autoRoute: true,
autoDetectInterface: true, autoDetectInterface: true,
+31 -3
View File
@@ -1,7 +1,11 @@
import useSWR from "swr"; import useSWR, { mutate } from "swr";
import { useRef } from "react"; import { useRef } from "react";
import { useTranslation } from "react-i18next"; 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 { useVerge } from "@/hooks/use-verge";
import { DialogRef, Notice, Switch } from "@/components/base"; import { DialogRef, Notice, Switch } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
@@ -9,6 +13,7 @@ import { GuardState } from "./mods/guard-state";
import { SysproxyViewer } from "./mods/sysproxy-viewer"; import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer"; import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { getSystemProxy, getAutotemProxy } from "@/services/cmds";
interface Props { interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
@@ -19,6 +24,9 @@ const SettingSystem = ({ onError }: Props) => {
const { verge, mutateVerge, patchVerge } = useVerge(); const { verge, mutateVerge, patchVerge } = useVerge();
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
const sysproxyRef = useRef<DialogRef>(null); const sysproxyRef = useRef<DialogRef>(null);
const tunRef = useRef<DialogRef>(null); const tunRef = useRef<DialogRef>(null);
@@ -27,13 +35,25 @@ const SettingSystem = ({ onError }: Props) => {
enable_auto_launch, enable_auto_launch,
enable_silent_start, enable_silent_start,
enable_system_proxy, enable_system_proxy,
proxy_auto_config,
} = verge ?? {}; } = verge ?? {};
const isProxyEnabled = proxy_auto_config
? autoproxy?.enable
: sysproxy?.enable;
const onSwitchFormat = (_e: any, value: boolean) => value; const onSwitchFormat = (_e: any, value: boolean) => value;
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: Partial<IVergeConfig>) => {
mutateVerge({ ...verge, ...patch }, false); mutateVerge({ ...verge, ...patch }, false);
}; };
const updateProxyStatus = async () => {
// 等待一小段时间让系统代理状态变化
await new Promise((resolve) => setTimeout(resolve, 100));
await mutate("getSystemProxy");
await mutate("getAutotemProxy");
};
return ( return (
<SettingList title={t("System Setting")}> <SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} /> <SysproxyViewer ref={sysproxyRef} />
@@ -73,6 +93,11 @@ const SettingSystem = ({ onError }: Props) => {
icon={SettingsRounded} icon={SettingsRounded}
onClick={() => sysproxyRef.current?.open()} 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} onCatch={onError}
onFormat={onSwitchFormat} onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_system_proxy: e })} 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" /> <Switch edge="end" />
</GuardState> </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 { useCallback, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { Button, MenuItem, Select, Input, Typography } from "@mui/material"; import { Button, MenuItem, Select, Input } from "@mui/material";
import { import { copyClashEnv } from "@/services/cmds";
exitApp,
openAppDir,
openCoreDir,
openLogsDir,
openDevTools,
copyClashEnv,
} from "@/services/cmds";
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { version } from "@root/package.json";
import { DialogRef, Notice } from "@/components/base"; import { DialogRef, Notice } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp"; import { SettingList, SettingItem } from "./mods/setting-comp";
import { ThemeModeSwitch } from "./mods/theme-mode-switch"; import { ThemeModeSwitch } from "./mods/theme-mode-switch";
@@ -28,6 +19,7 @@ import getSystem from "@/utils/get-system";
import { routers } from "@/pages/_routers"; import { routers } from "@/pages/_routers";
import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { TooltipIcon } from "@/components/base/base-tooltip-icon";
import { ContentCopyRounded } from "@mui/icons-material"; import { ContentCopyRounded } from "@mui/icons-material";
import { languages } from "@/services/i18n";
interface Props { interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
@@ -35,7 +27,20 @@ interface Props {
const OS = getSystem(); 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 { t } = useTranslation();
const { verge, patchVerge, mutateVerge } = useVerge(); const { verge, patchVerge, mutateVerge } = useVerge();
@@ -55,30 +60,17 @@ const SettingVerge = ({ onError }: Props) => {
const updateRef = useRef<DialogRef>(null); const updateRef = useRef<DialogRef>(null);
const backupRef = useRef<DialogRef>(null); const backupRef = useRef<DialogRef>(null);
const onChangeData = (patch: Partial<IVergeConfig>) => { const onChangeData = (patch: any) => {
mutateVerge({ ...verge, ...patch }, false); 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 () => { const onCopyClashEnv = useCallback(async () => {
await copyClashEnv(); await copyClashEnv();
Notice.success(t("Copy Success"), 1000); Notice.success(t("Copy Success"), 1000);
}, []); }, []);
return ( return (
<SettingList title={t("Verge Setting")}> <SettingList title={t("Verge Basic Setting")}>
<ThemeViewer ref={themeRef} /> <ThemeViewer ref={themeRef} />
<ConfigViewer ref={configRef} /> <ConfigViewer ref={configRef} />
<HotkeyViewer ref={hotkeyRef} /> <HotkeyViewer ref={hotkeyRef} />
@@ -96,10 +88,11 @@ const SettingVerge = ({ onError }: Props) => {
onGuard={(e) => patchVerge({ language: e })} onGuard={(e) => patchVerge({ language: e })}
> >
<Select size="small" sx={{ width: 110, "> div": { py: "7.5px" } }}> <Select size="small" sx={{ width: 110, "> div": { py: "7.5px" } }}>
<MenuItem value="zh">中文</MenuItem> {languageOptions.map(({ code, label }) => (
<MenuItem value="en">English</MenuItem> <MenuItem key={code} value={code}>
<MenuItem value="ru">Русский</MenuItem> {label}
<MenuItem value="fa">فارسی</MenuItem> </MenuItem>
))}
</Select> </Select>
</GuardState> </GuardState>
</SettingItem> </SettingItem>
@@ -150,6 +143,7 @@ const SettingVerge = ({ onError }: Props) => {
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}> <Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
<MenuItem value="bash">Bash</MenuItem> <MenuItem value="bash">Bash</MenuItem>
<MenuItem value="cmd">CMD</MenuItem> <MenuItem value="cmd">CMD</MenuItem>
<MenuItem value="nushell">Nushell</MenuItem>
<MenuItem value="powershell">PowerShell</MenuItem> <MenuItem value="powershell">PowerShell</MenuItem>
</Select> </Select>
</GuardState> </GuardState>
@@ -165,7 +159,11 @@ const SettingVerge = ({ onError }: Props) => {
> >
<Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}> <Select size="small" sx={{ width: 140, "> div": { py: "7.5px" } }}>
{routers.map((page: { label: string; path: string }) => { {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> </Select>
</GuardState> </GuardState>
@@ -240,54 +238,8 @@ const SettingVerge = ({ onError }: Props) => {
onClick={() => hotkeyRef.current?.open()} onClick={() => hotkeyRef.current?.open()}
label={t("Hotkey Setting")} 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")}
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
onClick={() => {
exitApp();
}}
label={t("Exit")}
/>
<SettingItem label={t("Verge Version")}>
<Typography sx={{ py: "7px", pr: 1 }}>v{version}</Typography>
</SettingItem>
</SettingList> </SettingList>
); );
}; };
export default SettingVerge; export default SettingVergeBasic;
+22 -26
View File
@@ -4,6 +4,7 @@ import { createSockette } from "../utils/websocket";
import { useClashInfo } from "./use-clash"; import { useClashInfo } from "./use-clash";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { create } from "zustand"; import { create } from "zustand";
import { useVisibility } from "./use-visibility";
const MAX_LOG_NUM = 1000; const MAX_LOG_NUM = 1000;
@@ -33,34 +34,25 @@ const buildWSUrl = (server: string, secret: string, logLevel: LogLevel) => {
}; };
interface LogStore { interface LogStore {
logs: Record<LogLevel, ILogItem[]>; logs: ILogItem[];
clearLogs: (level?: LogLevel) => void; clearLogs: () => void;
appendLog: (level: LogLevel, log: ILogItem) => void; appendLog: (log: ILogItem) => void;
} }
const useLogStore = create<LogStore>( const useLogStore = create<LogStore>(
(set: (fn: (state: LogStore) => Partial<LogStore>) => void) => ({ (set: (fn: (state: LogStore) => Partial<LogStore>) => void) => ({
logs: { logs: [],
warning: [], clearLogs: () =>
info: [], set(() => ({
debug: [], logs: [],
error: [],
all: [],
},
clearLogs: (level?: LogLevel) =>
set((state: LogStore) => ({
logs: level
? { ...state.logs, [level]: [] }
: { warning: [], info: [], debug: [], error: [], all: [] },
})), })),
appendLog: (level: LogLevel, log: ILogItem) => appendLog: (log: ILogItem) =>
set((state: LogStore) => { set((state: LogStore) => {
const currentLogs = state.logs[level];
const newLogs = const newLogs =
currentLogs.length >= MAX_LOG_NUM state.logs.length >= MAX_LOG_NUM
? [...currentLogs.slice(1), log] ? [...state.logs.slice(1), log]
: [...currentLogs, log]; : [...state.logs, log];
return { logs: { ...state.logs, [level]: newLogs } }; return { logs: newLogs };
}), }),
}), }),
); );
@@ -69,9 +61,10 @@ export const useLogData = (logLevel: LogLevel) => {
const { clashInfo } = useClashInfo(); const { clashInfo } = useClashInfo();
const [enableLog] = useEnableLog(); const [enableLog] = useEnableLog();
const { logs, appendLog } = useLogStore(); const { logs, appendLog } = useLogStore();
const pageVisible = useVisibility();
useEffect(() => { useEffect(() => {
if (!enableLog || !clashInfo) return; if (!enableLog || !clashInfo || !pageVisible) return;
const { server = "", secret = "" } = clashInfo; const { server = "", secret = "" } = clashInfo;
const wsUrl = buildWSUrl(server, secret, logLevel); const wsUrl = buildWSUrl(server, secret, logLevel);
@@ -82,7 +75,7 @@ export const useLogData = (logLevel: LogLevel) => {
if (!isActive) return; if (!isActive) return;
const data = JSON.parse(event.data) as ILogItem; const data = JSON.parse(event.data) as ILogItem;
const time = dayjs().format("MM-DD HH:mm:ss"); const time = dayjs().format("MM-DD HH:mm:ss");
appendLog(logLevel, { ...data, time }); appendLog({ ...data, time });
}, },
onerror() { onerror() {
if (!isActive) return; if (!isActive) return;
@@ -96,10 +89,13 @@ export const useLogData = (logLevel: LogLevel) => {
}; };
}, [clashInfo, enableLog, logLevel]); }, [clashInfo, enableLog, logLevel]);
return logs[logLevel]; // 根据当前选择的日志等级过滤日志
return logLevel === "all"
? logs
: logs.filter((log) => log.type.toLowerCase() === logLevel);
}; };
// 导出清空日志的方法 // 导出清空日志的方法
export const clearLogs = (level?: LogLevel) => { export const clearLogs = () => {
useLogStore.getState().clearLogs(level); 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"
}
+73 -3
View File
@@ -178,7 +178,8 @@
"Process": "Process", "Process": "Process",
"Time": "Time", "Time": "Time",
"Source": "Source", "Source": "Source",
"Destination IP": "Destination IP", "Destination": "Destination",
"DestinationPort": "Destination Port",
"Close Connection": "Close Connection", "Close Connection": "Close Connection",
"Rules": "Rules", "Rules": "Rules",
"Rule Provider": "Rule Provider", "Rule Provider": "Rule Provider",
@@ -229,6 +230,7 @@
"Proxy Guard Info": "Enable to prevent other software from modifying the operating system's proxy settings", "Proxy Guard Info": "Enable to prevent other software from modifying the operating system's proxy settings",
"Guard Duration": "Guard Duration", "Guard Duration": "Guard Duration",
"Always use Default Bypass": "Always use Default Bypass", "Always use Default Bypass": "Always use Default Bypass",
"Use Bypass Check": "Use Bypass Check",
"Proxy Bypass": "Proxy Bypass Settings: ", "Proxy Bypass": "Proxy Bypass Settings: ",
"Bypass": "Bypass: ", "Bypass": "Bypass: ",
"Use PAC Mode": "Use PAC Mode", "Use PAC Mode": "Use PAC Mode",
@@ -249,6 +251,7 @@
"Unified Delay": "Unified Delay", "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", "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": "Log Level",
"Log Level Info": "This parameter is valid only for kernel log files in the log directory Service folder",
"Port Config": "Port Config", "Port Config": "Port Config",
"Random Port": "Random Port", "Random Port": "Random Port",
"Mixed Port": "Mixed Port", "Mixed Port": "Mixed Port",
@@ -274,7 +277,8 @@
"Open UWP tool": "Open UWP tool", "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", "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", "Update GeoData": "Update GeoData",
"Verge Setting": "Verge Setting", "Verge Basic Setting": "Verge Basic Setting",
"Verge Advanced Setting": "Verge Advanced Setting",
"Language": "Language", "Language": "Language",
"Theme Mode": "Theme Mode", "Theme Mode": "Theme Mode",
"theme.light": "Light", "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 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", "Default Latency Timeout": "Default Latency Timeout",
"Hotkey Setting": "Hotkey Setting", "Hotkey Setting": "Hotkey Setting",
"Enable Global Hotkey": "Enable Global Hotkey",
"open_or_close_dashboard": "Open/Close Dashboard", "open_or_close_dashboard": "Open/Close Dashboard",
"clash_mode_rule": "Rule Mode", "clash_mode_rule": "Rule Mode",
"clash_mode_global": "Global Mode", "clash_mode_global": "Global Mode",
@@ -333,6 +338,7 @@
"toggle_system_proxy": "Enable/Disable System Proxy", "toggle_system_proxy": "Enable/Disable System Proxy",
"toggle_tun_mode": "Enable/Disable Tun Mode", "toggle_tun_mode": "Enable/Disable Tun Mode",
"Backup Setting": "Backup Setting", "Backup Setting": "Backup Setting",
"Backup Setting Info": "Support WebDAV backup configuration files",
"Runtime Config": "Runtime Config", "Runtime Config": "Runtime Config",
"Open Conf Dir": "Open Conf Dir", "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 Conf Dir Info": "If the software runs abnormally, BACKUP and delete all files in this folder than restart the software",
@@ -395,5 +401,69 @@
"Confirm to delete this backup file?": "Confirm to delete this backup file?", "Confirm to delete this backup file?": "Confirm to delete this backup file?",
"Confirm to restore this backup file?": "Confirm to restore 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", "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"
} }
+53 -4
View File
@@ -113,7 +113,7 @@
"select": "انتخاب پروکسی به صورت دستی", "select": "انتخاب پروکسی به صورت دستی",
"url-test": "انتخاب پروکسی بر اساس تأخیر آزمایش URL", "url-test": "انتخاب پروکسی بر اساس تأخیر آزمایش URL",
"fallback": "تعویض به پروکسی دیگر در صورت بروز خطا", "fallback": "تعویض به پروکسی دیگر در صورت بروز خطا",
"load-balance": "توزیع ��روکسی بر اساس توازن بار", "load-balance": "توزیع پراکسی بر اساس توازن بار",
"relay": "عبور از زنجیره پروکسی تعریف شده", "relay": "عبور از زنجیره پروکسی تعریف شده",
"Group Name": "نام گروه", "Group Name": "نام گروه",
"Use Proxies": "استفاده از پروکسی‌ها", "Use Proxies": "استفاده از پروکسی‌ها",
@@ -178,7 +178,8 @@
"Process": "فرآیند", "Process": "فرآیند",
"Time": "زمان", "Time": "زمان",
"Source": "منبع", "Source": "منبع",
"Destination IP": "آدرس IP مقصد", "Destination": "آدرس IP مقصد",
"DestinationPort": "بندر هدف",
"Close Connection": "بستن اتصال", "Close Connection": "بستن اتصال",
"Rules": "قوانین", "Rules": "قوانین",
"Rule Provider": "تأمین‌کننده قانون", "Rule Provider": "تأمین‌کننده قانون",
@@ -229,6 +230,7 @@
"Proxy Guard Info": "امکان جلوگیری از نرم‌افزارهای دیگر از تغییر تنظیمات پروکسی سیستم عامل را فعال کنید", "Proxy Guard Info": "امکان جلوگیری از نرم‌افزارهای دیگر از تغییر تنظیمات پروکسی سیستم عامل را فعال کنید",
"Guard Duration": "مدت محافظت", "Guard Duration": "مدت محافظت",
"Always use Default Bypass": "همیشه از دور زدن پیش‌فرض استفاده کنید", "Always use Default Bypass": "همیشه از دور زدن پیش‌فرض استفاده کنید",
"Use Bypass Check": "استخدم التحقق من التحايل",
"Proxy Bypass": "دور زدن پراکسی: ", "Proxy Bypass": "دور زدن پراکسی: ",
"Bypass": "دور زدن: ", "Bypass": "دور زدن: ",
"Use PAC Mode": "استفاده از حالت PAC", "Use PAC Mode": "استفاده از حالت PAC",
@@ -327,6 +329,7 @@
"Default Latency Test Info": "فقط برای تست درخواست‌های کلاینت HTTP استفاده می‌شود و بر فایل پیکربندی تأثیری نخواهد داشت", "Default Latency Test Info": "فقط برای تست درخواست‌های کلاینت HTTP استفاده می‌شود و بر فایل پیکربندی تأثیری نخواهد داشت",
"Default Latency Timeout": "زمان انتظار تأخیر پیش‌فرض", "Default Latency Timeout": "زمان انتظار تأخیر پیش‌فرض",
"Hotkey Setting": "تنظیمات کلیدهای میانبر", "Hotkey Setting": "تنظیمات کلیدهای میانبر",
"Enable Global Hotkey": "فعال کردن کلید میانبر سراسری",
"open_or_close_dashboard": "باز/بستن داشبورد", "open_or_close_dashboard": "باز/بستن داشبورد",
"clash_mode_rule": "حالت قانون", "clash_mode_rule": "حالت قانون",
"clash_mode_global": "حالت جهانی", "clash_mode_global": "حالت جهانی",
@@ -370,7 +373,6 @@
"Permissions Granted Successfully for _clash Core": "مجوزها با موفقیت برای هسته {{core}} اعطا شد", "Permissions Granted Successfully for _clash Core": "مجوزها با موفقیت برای هسته {{core}} اعطا شد",
"Core Version Updated": "نسخه هسته به‌روزرسانی شد", "Core Version Updated": "نسخه هسته به‌روزرسانی شد",
"Clash Core Restarted": "هسته Clash مجدداً راه‌اندازی شد", "Clash Core Restarted": "هسته Clash مجدداً راه‌اندازی شد",
"Switched to _clash Core": "تغییر به هسته {{core}}",
"GeoData Updated": "GeoData به‌روزرسانی شد", "GeoData Updated": "GeoData به‌روزرسانی شد",
"Currently on the Latest Version": "در حال حاضر در آخرین نسخه", "Currently on the Latest Version": "در حال حاضر در آخرین نسخه",
"Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد", "Import Subscription Successful": "وارد کردن اشتراک با موفقیت انجام شد",
@@ -397,5 +399,52 @@
"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": "بازیابی با موفقیت انجام شد، برنامه در 1 ثانیه راه‌اندازی مجدد می‌شود", "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"
}
+52 -3
View File
@@ -178,7 +178,8 @@
"Process": "Процесс", "Process": "Процесс",
"Time": "Время подключения", "Time": "Время подключения",
"Source": "Исходный адрес", "Source": "Исходный адрес",
"Destination IP": "IP-адрес назначения", "Destination": "IP-адрес назначения",
"DestinationPort": "Целевой порт",
"Close Connection": "Закрыть соединение", "Close Connection": "Закрыть соединение",
"Rules": "Правила", "Rules": "Правила",
"Rule Provider": "Провайдер правило", "Rule Provider": "Провайдер правило",
@@ -229,6 +230,7 @@
"Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим программным обеспечением", "Proxy Guard Info": "Включите эту функцию чтобы предотвратить изменение настроек прокси-сервера операционной системы другим программным обеспечением",
"Guard Duration": "Период защиты", "Guard Duration": "Период защиты",
"Always use Default Bypass": "Всегда использовать стандартное обходное решение", "Always use Default Bypass": "Всегда использовать стандартное обходное решение",
"Use Bypass Check": "Используйте проверку обхода",
"Proxy Bypass": "Игнорирование прокси: ", "Proxy Bypass": "Игнорирование прокси: ",
"Bypass": "Игнорирование: ", "Bypass": "Игнорирование: ",
"Use PAC Mode": "Используйте режим PAC", "Use PAC Mode": "Используйте режим PAC",
@@ -327,6 +329,7 @@
"Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации", "Default Latency Test Info": "Используется только для тестирования HTTP-запросов клиента и не влияет на файл конфигурации",
"Default Latency Timeout": "Таймаут задержки по умолчанию", "Default Latency Timeout": "Таймаут задержки по умолчанию",
"Hotkey Setting": "Настройки клавиатурных сокращений", "Hotkey Setting": "Настройки клавиатурных сокращений",
"Enable Global Hotkey": "Включить глобальную горячую клавишу",
"open_or_close_dashboard": "Открыть/Закрыть панель управления", "open_or_close_dashboard": "Открыть/Закрыть панель управления",
"clash_mode_rule": "Режим правил", "clash_mode_rule": "Режим правил",
"clash_mode_global": "Глобальный режим", "clash_mode_global": "Глобальный режим",
@@ -370,7 +373,6 @@
"Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}", "Permissions Granted Successfully for _clash Core": "Разрешения успешно предоставлены для ядра {{core}}",
"Core Version Updated": "Обновлена версия ядра", "Core Version Updated": "Обновлена версия ядра",
"Clash Core Restarted": "Clash ядра перезапущено", "Clash Core Restarted": "Clash ядра перезапущено",
"Switched to _clash Core": "Переключено на ядра {{core}}",
"GeoData Updated": "GeoData Обновлена", "GeoData Updated": "GeoData Обновлена",
"Currently on the Latest Version": "В настоящее время используется последняя версия", "Currently on the Latest Version": "В настоящее время используется последняя версия",
"Import subscription successful": "Импорт подписки успешно", "Import subscription successful": "Импорт подписки успешно",
@@ -397,5 +399,52 @@
"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": "Восстановление успешно выполнено, приложение перезапустится через 1 секунду", "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": "毫秒", "millis": "毫秒",
"mins": "分钟",
"seconds": "秒", "seconds": "秒",
"mins": "分钟",
"Back": "返回", "Back": "返回",
"Close": "关闭", "Close": "关闭",
"Cancel": "取消", "Cancel": "取消",
@@ -55,12 +55,12 @@
"Create Profile": "新建配置", "Create Profile": "新建配置",
"Edit Profile": "编辑配置", "Edit Profile": "编辑配置",
"Edit Proxies": "编辑节点", "Edit Proxies": "编辑节点",
"Use newlines for multiple uri": "多条URI请使用换行分隔(支持Base64编码)", "Use newlines for multiple uri": "多条 URI 请使用换行分隔(支持 Base64 编码)",
"Edit Rules": "编辑规则", "Edit Rules": "编辑规则",
"Rule Type": "规则类型", "Rule Type": "规则类型",
"Rule Content": "规则内容", "Rule Content": "规则内容",
"Proxy Policy": "代理策略", "Proxy Policy": "代理策略",
"No Resolve": "跳过DNS解析", "No Resolve": "跳过 DNS 解析",
"Prepend Rule": "添加前置规则", "Prepend Rule": "添加前置规则",
"Append Rule": "添加后置规则", "Append Rule": "添加后置规则",
"Prepend Group": "添加前置代理组", "Prepend Group": "添加前置代理组",
@@ -75,26 +75,26 @@
"DOMAIN-SUFFIX": "匹配域名后缀", "DOMAIN-SUFFIX": "匹配域名后缀",
"DOMAIN-KEYWORD": "匹配域名关键字", "DOMAIN-KEYWORD": "匹配域名关键字",
"DOMAIN-REGEX": "匹配域名正则表达式", "DOMAIN-REGEX": "匹配域名正则表达式",
"GEOSITE": "匹配Geosite内的域名", "GEOSITE": "匹配 Geosite 内的域名",
"GEOIP": "匹配IP所属国家代码", "GEOIP": "匹配 IP 所属国家代码",
"SRC-GEOIP": "匹配来源IP所属国家代码", "SRC-GEOIP": "匹配来源 IP 所属国家代码",
"IP-ASN": "匹配IP所属ASN", "IP-ASN": "匹配 IP 所属 ASN",
"SRC-IP-ASN": "匹配来源IP所属ASN", "SRC-IP-ASN": "匹配来源 IP 所属 ASN",
"IP-CIDR": "匹配IP地址范围", "IP-CIDR": "匹配 IP 地址范围",
"IP-CIDR6": "匹配IP地址范围", "IP-CIDR6": "匹配 IP 地址范围",
"SRC-IP-CIDR": "匹配来源IP地址范围", "SRC-IP-CIDR": "匹配来源 IP 地址范围",
"IP-SUFFIX": "匹配IP后缀范围", "IP-SUFFIX": "匹配 IP 后缀范围",
"SRC-IP-SUFFIX": "匹配来源IP后缀范围", "SRC-IP-SUFFIX": "匹配来源 IP 后缀范围",
"SRC-PORT": "匹配请求来源端口范围", "SRC-PORT": "匹配请求来源端口范围",
"DST-PORT": "匹配请求目标端口范围", "DST-PORT": "匹配请求目标端口范围",
"IN-PORT": "匹配入站端口", "IN-PORT": "匹配入站端口",
"DSCP": "DSCP标记(仅限tproxy udp入站)", "DSCP": "DSCP标记(仅限 TPROXY UDP 入站)",
"PROCESS-NAME": "匹配进程名称(Android包名)", "PROCESS-NAME": "匹配进程名称(Android 包名)",
"PROCESS-PATH": "匹配完整进程路径", "PROCESS-PATH": "匹配完整进程路径",
"PROCESS-NAME-REGEX": "正则匹配完整进程名称(Android包名)", "PROCESS-NAME-REGEX": "正则匹配完整进程名称(Android 包名)",
"PROCESS-PATH-REGEX": "正则匹配完整进程路径", "PROCESS-PATH-REGEX": "正则匹配完整进程路径",
"NETWORK": "匹配传输协议(tcp/udp)", "NETWORK": "匹配传输协议 (TCP/UDP)",
"UID": "匹配Linux USER ID", "UID": "匹配 Linux USER ID",
"IN-TYPE": "匹配入站类型", "IN-TYPE": "匹配入站类型",
"IN-USER": "匹配入站用户名", "IN-USER": "匹配入站用户名",
"IN-NAME": "匹配入站名称", "IN-NAME": "匹配入站名称",
@@ -131,13 +131,13 @@
"Include All Proxies": "引入所有出站代理", "Include All Proxies": "引入所有出站代理",
"Exclude Filter": "排除节点", "Exclude Filter": "排除节点",
"Exclude Type": "排除节点类型", "Exclude Type": "排除节点类型",
"Disable UDP": "禁用UDP", "Disable UDP": "禁用 UDP",
"Hidden": "隐藏代理组", "Hidden": "隐藏代理组",
"Group Name Required": "代理组名称不能为空", "Group Name Required": "代理组名称不能为空",
"Group Name Already Exists": "代理组名称已存在", "Group Name Already Exists": "代理组名称已存在",
"Extend Config": "扩展配置", "Extend Config": "扩展覆写配置",
"Extend Script": "扩展脚本", "Extend Script": "扩展脚本",
"Global Merge": "全局扩展配置", "Global Merge": "全局扩展覆写配置",
"Global Script": "全局扩展脚本", "Global Script": "全局扩展脚本",
"Type": "类型", "Type": "类型",
"Name": "名称", "Name": "名称",
@@ -147,7 +147,7 @@
"Choose File": "选择文件", "Choose File": "选择文件",
"Use System Proxy": "使用系统代理更新", "Use System Proxy": "使用系统代理更新",
"Use Clash Proxy": "使用内核代理更新", "Use Clash Proxy": "使用内核代理更新",
"Accept Invalid Certs (Danger)": "允许无效证书 (危险)", "Accept Invalid Certs (Danger)": "允许无效证书(危险)",
"Refresh": "刷新", "Refresh": "刷新",
"Home": "首页", "Home": "首页",
"Select": "使用", "Select": "使用",
@@ -155,7 +155,7 @@
"Edit File": "编辑文件", "Edit File": "编辑文件",
"Open File": "打开文件", "Open File": "打开文件",
"Update": "更新", "Update": "更新",
"Update(Proxy)": "更新(代理)", "Update(Proxy)": "更新(代理)",
"Confirm deletion": "确认删除", "Confirm deletion": "确认删除",
"This operation is not reversible": "此操作不可逆", "This operation is not reversible": "此操作不可逆",
"Script Console": "脚本控制台输出", "Script Console": "脚本控制台输出",
@@ -178,7 +178,8 @@
"Process": "进程", "Process": "进程",
"Time": "连接时间", "Time": "连接时间",
"Source": "源地址", "Source": "源地址",
"Destination IP": "目标地址", "Destination": "目标地址",
"DestinationPort": "目标端口",
"Close Connection": "关闭连接", "Close Connection": "关闭连接",
"Rules": "规则", "Rules": "规则",
"Rule Provider": "规则集合", "Rule Provider": "规则集合",
@@ -193,19 +194,19 @@
"Test URL": "测试地址", "Test URL": "测试地址",
"Settings": "设置", "Settings": "设置",
"System Setting": "系统设置", "System Setting": "系统设置",
"Tun Mode": "Tun(虚拟网卡)模式", "Tun Mode": "TUN(虚拟网卡)模式",
"Reset to Default": "重置为默认值", "Reset to Default": "重置为默认值",
"Tun Mode Info": "Tun(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理", "Tun Mode Info": "TUN(虚拟网卡)模式接管系统所有流量,启用时无须打开系统代理",
"Stack": "Tun 模式堆栈", "Stack": "TUN 模式堆栈",
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用", "System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用",
"Device": "Tun 网卡名称", "Device": "TUN 网卡名称",
"Auto Route": "自动设置全局路由", "Auto Route": "自动设置全局路由",
"Strict Route": "严格路由", "Strict Route": "严格路由",
"Auto Detect Interface": "自动选择流量出口接口", "Auto Detect Interface": "自动选择流量出口接口",
"DNS Hijack": "DNS 劫持", "DNS Hijack": "DNS 劫持",
"MTU": "最大传输单元", "MTU": "最大传输单元",
"Service Mode": "服务模式", "Service Mode": "服务模式",
"Service Mode Info": "启用TUN模式前请安装服务模式,该服务启动的内核进程可获得安装虚拟网卡(TUN模式)的权限", "Service Mode Info": "启用 TUN 模式前请安装服务模式,该服务启动的内核进程可获得安装虚拟网卡(TUN 模式)的权限",
"Current State": "当前状态", "Current State": "当前状态",
"pending": "等待中", "pending": "等待中",
"installed": "已安装", "installed": "已安装",
@@ -229,11 +230,12 @@
"Proxy Guard Info": "开启以防止其他软件修改操作系统的代理设置", "Proxy Guard Info": "开启以防止其他软件修改操作系统的代理设置",
"Guard Duration": "代理守卫间隔", "Guard Duration": "代理守卫间隔",
"Always use Default Bypass": "始终使用默认绕过", "Always use Default Bypass": "始终使用默认绕过",
"Use Bypass Check": "启用代理绕过检查",
"Proxy Bypass": "代理绕过设置:", "Proxy Bypass": "代理绕过设置:",
"Bypass": "当前绕过:", "Bypass": "当前绕过:",
"Use PAC Mode": "使用PAC模式", "Use PAC Mode": "使用 PAC 模式",
"PAC Script Content": "PAC脚本内容", "PAC Script Content": "PAC 脚本内容",
"PAC URL": "PAC地址:", "PAC URL": "PAC 地址:",
"Auto Launch": "开机自启", "Auto Launch": "开机自启",
"Silent Start": "静默启动", "Silent Start": "静默启动",
"Silent Start Info": "程序启动时以后台模式运行,不显示程序面板", "Silent Start Info": "程序启动时以后台模式运行,不显示程序面板",
@@ -249,14 +251,14 @@
"Unified Delay": "统一延迟", "Unified Delay": "统一延迟",
"Unified Delay Info": "开启统一延迟时,会进行两次延迟测试,以消除连接握手等带来的不同类型节点的延迟差异", "Unified Delay Info": "开启统一延迟时,会进行两次延迟测试,以消除连接握手等带来的不同类型节点的延迟差异",
"Log Level": "日志等级", "Log Level": "日志等级",
"Log Level Info": "仅对日志目录Service文件夹下的内核日志文件生效", "Log Level Info": "仅对日志目录 Service 文件夹下的内核日志文件生效",
"Port Config": "端口设置", "Port Config": "端口设置",
"Random Port": "随机端口", "Random Port": "随机端口",
"Mixed Port": "混合代理端口", "Mixed Port": "混合代理端口",
"Socks Port": "Socks代理端口", "Socks Port": "SOCKS 代理端口",
"Http Port": "Http(s)代理端口", "Http Port": "HTTP(S) 代理端口",
"Redir Port": "Redir透明代理端口", "Redir Port": "Redir 透明代理端口",
"Tproxy Port": "Tproxy透明代理端口", "TPROXY Port": "TPROXY 透明代理端口",
"External": "外部控制", "External": "外部控制",
"External Controller": "外部控制器监听地址", "External Controller": "外部控制器监听地址",
"Core Secret": "API 访问密钥", "Core Secret": "API 访问密钥",
@@ -273,7 +275,7 @@
"Please enter your root password": "请输入您的 root 密码", "Please enter your root password": "请输入您的 root 密码",
"Grant": "授权", "Grant": "授权",
"Open UWP tool": "UWP 工具", "Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制", "Open UWP tool Info": "Windows 8 开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",
"Update GeoData": "更新 GeoData", "Update GeoData": "更新 GeoData",
"Verge Setting": "Verge 设置", "Verge Setting": "Verge 设置",
"Language": "语言设置", "Language": "语言设置",
@@ -310,9 +312,9 @@
"Tray Icon": "托盘图标", "Tray Icon": "托盘图标",
"Common Tray Icon": "常规托盘图标", "Common Tray Icon": "常规托盘图标",
"System Proxy Tray Icon": "系统代理托盘图标", "System Proxy Tray Icon": "系统代理托盘图标",
"Tun Tray Icon": "Tun 模式托盘图标", "Tun Tray Icon": "TUN 模式托盘图标",
"Miscellaneous": "杂项设置", "Miscellaneous": "杂项设置",
"App Log Level": "App日志等级", "App Log Level": "应用日志等级",
"Auto Close Connections": "自动关闭连接", "Auto Close Connections": "自动关闭连接",
"Auto Close Connections Info": "当代理组选中节点或代理模式变动时,关闭已建立的连接", "Auto Close Connections Info": "当代理组选中节点或代理模式变动时,关闭已建立的连接",
"Auto Check Update": "自动检查更新", "Auto Check Update": "自动检查更新",
@@ -322,22 +324,23 @@
"Auto Columns": "自动列数", "Auto Columns": "自动列数",
"Auto Log Clean": "自动清理日志", "Auto Log Clean": "自动清理日志",
"Never Clean": "不清理", "Never Clean": "不清理",
"Retain _n Days": "保留{{n}}天", "Retain _n Days": "保留 {{n}} 天",
"Default Latency Test": "默认测试链接", "Default Latency Test": "默认测试链接",
"Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响", "Default Latency Test Info": "仅用于 HTTP 客户端请求测试,不会对配置文件产生影响",
"Default Latency Timeout": "测试超时时间", "Default Latency Timeout": "测试超时时间",
"Hotkey Setting": "热键设置", "Hotkey Setting": "热键设置",
"Enable Global Hotkey": "启用全局热键",
"open_or_close_dashboard": "打开/关闭面板", "open_or_close_dashboard": "打开/关闭面板",
"clash_mode_rule": "规则模式", "clash_mode_rule": "规则模式",
"clash_mode_global": "全局模式", "clash_mode_global": "全局模式",
"clash_mode_direct": "直连模式", "clash_mode_direct": "直连模式",
"toggle_system_proxy": "打开/关闭系统代理", "toggle_system_proxy": "打开/关闭系统代理",
"toggle_tun_mode": "打开/关闭 Tun 模式", "toggle_tun_mode": "打开/关闭 TUN 模式",
"Backup Setting": "备份设置", "Backup Setting": "备份设置",
"Backup Setting Info": "支持WebDAV备份配置文件", "Backup Setting Info": "支持 WebDAV 备份配置文件",
"Runtime Config": "当前配置", "Runtime Config": "当前配置",
"Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
"Open Conf Dir": "配置目录", "Open Conf Dir": "配置目录",
"Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
"Open Core Dir": "内核目录", "Open Core Dir": "内核目录",
"Open Logs Dir": "日志目录", "Open Logs Dir": "日志目录",
"Check for Updates": "检查更新", "Check for Updates": "检查更新",
@@ -361,20 +364,19 @@
"Settings Applied": "设置已应用", "Settings Applied": "设置已应用",
"Service Installed Successfully": "已成功安装服务", "Service Installed Successfully": "已成功安装服务",
"Service Uninstalled 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": "无效的代理绕过格式", "Invalid Bypass Format": "无效的代理绕过格式",
"Clash Port Modified": "Clash 端口已修改", "Clash Port Modified": "Clash 端口已修改",
"Port Conflict": "端口冲突", "Port Conflict": "端口冲突",
"Restart Application to Apply Modifications": "重启Verge以应用修改", "Restart Application to Apply Modifications": "重启 Verge 以应用修改",
"External Controller Address Modified": "外部控制器监听地址已修改", "External Controller Address Modified": "外部控制器监听地址已修改",
"Permissions Granted Successfully for _clash Core": "{{core}} 内核授权成功", "Permissions Granted Successfully for _clash Core": "{{core}} 内核授权成功",
"Core Version Updated": "内核版本已更新", "Core Version Updated": "内核版本已更新",
"Clash Core Restarted": "已重启 Clash 内核", "Clash Core Restarted": "已重启 Clash 内核",
"Switched to _clash Core": "已切换至 {{core}} 内核",
"GeoData Updated": "已更新 GeoData", "GeoData Updated": "已更新 GeoData",
"Currently on the Latest Version": "当前已是最新版本", "Currently on the Latest Version": "当前已是最新版本",
"Import Subscription Successful": "导入订阅成功", "Import Subscription Successful": "导入订阅成功",
"WebDAV Server URL": "WebDAV服务器地址 http(s)://", "WebDAV Server URL": "WebDAV 服务器地址 http(s)://",
"Username": "用户名", "Username": "用户名",
"Password": "密码", "Password": "密码",
"Backup": "备份", "Backup": "备份",
@@ -395,7 +397,67 @@
"Restore Backup": "恢复备份", "Restore Backup": "恢复备份",
"Backup Time": "备份时间", "Backup Time": "备份时间",
"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": "恢复成功,应用将在1秒后重启", "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 透明代理端口",
"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 -67
View File
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
import i18next from "i18next"; import i18next from "i18next";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { SWRConfig, mutate } from "swr"; import { SWRConfig, mutate } from "swr";
import { useEffect } from "react"; import { useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useRoutes, useNavigate } from "react-router-dom"; import { useLocation, useRoutes, useNavigate } from "react-router-dom";
import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material"; import { List, Paper, ThemeProvider, SvgIcon } from "@mui/material";
@@ -27,6 +27,7 @@ import { getPortableFlag } from "@/services/cmds";
import React from "react"; import React from "react";
import { TransitionGroup, CSSTransition } from "react-transition-group"; import { TransitionGroup, CSSTransition } from "react-transition-group";
import { useListen } from "@/hooks/use-listen"; import { useListen } from "@/hooks/use-listen";
import { listen } from "@tauri-apps/api/event";
const appWindow = getCurrentWebviewWindow(); const appWindow = getCurrentWebviewWindow();
export let portableFlag = false; export let portableFlag = false;
@@ -35,73 +36,178 @@ dayjs.extend(relativeTime);
const OS = getSystem(); 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 Layout = () => {
const mode = useThemeMode(); const mode = useThemeMode();
const isDark = mode === "light" ? false : true; const isDark = mode === "light" ? false : true;
const { t } = useTranslation(); const { t } = useTranslation();
const { theme } = useCustomTheme(); const { theme } = useCustomTheme();
const { verge } = useVerge(); const { verge } = useVerge();
const { language, start_page } = verge || {}; const { language, start_page } = verge ?? {};
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const routersEles = useRoutes(routers); const routersEles = useRoutes(routers);
const { addListener, setupCloseListener } = useListen(); 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(() => { useEffect(() => {
addListener("verge://refresh-clash-config", async () => { const listeners = [
// the clash info may be updated // 配置更新监听
await getAxios(true); addListener("verge://refresh-clash-config", async () => {
mutate("getProxies"); await getAxios(true);
mutate("getVersion"); mutate("getProxies");
mutate("getClashConfig"); mutate("getVersion");
mutate("getProxyProviders"); mutate("getClashConfig");
}); mutate("getProxyProviders");
}),
// update the verge config // verge 配置更新监听
addListener("verge://refresh-verge-config", () => mutate("getVergeConfig")); addListener("verge://refresh-verge-config", () =>
mutate("getVergeConfig"),
),
// 设置提示监听 // 通知消息监听
addListener("verge://notice-message", ({ payload }) => { addListener("verge://notice-message", ({ payload }) =>
const [status, msg] = payload as [string, string]; handleNotice(payload as [string, string]),
switch (status) { ),
case "import_sub_url::ok": ];
navigate("/profile", { state: { current: msg } });
Notice.success(t("Import Subscription Successful")); // 设置窗口显示/隐藏监听
break; const setupWindowListeners = async () => {
case "import_sub_url::error": const [hideUnlisten, showUnlisten] = await Promise.all([
navigate("/profile"); listen("verge://hide-window", () => appWindow.hide()),
Notice.error(msg); listen("verge://show-window", () => appWindow.show()),
break; ]);
case "set_config::error":
Notice.error(msg);
break;
default:
break;
}
});
setTimeout(async () => { return () => {
portableFlag = await getPortableFlag(); hideUnlisten();
await appWindow.unminimize(); showUnlisten();
await appWindow.show(); };
await appWindow.setFocus(); };
}, 50);
}, []);
// 初始化
setupCloseListener();
const cleanupWindow = setupWindowListeners();
// 清理函数
return () => {
// 清理主要监听器
listeners.forEach((listener) => {
if (typeof listener.then === "function") {
listener.then((unlisten) => unlisten());
}
});
// 清理窗口监听器
cleanupWindow.then((cleanup) => cleanup());
};
}, [handleNotice]);
// 语言和起始页设置
useEffect(() => { useEffect(() => {
if (language) { if (language) {
dayjs.locale(language === "zh" ? "zh-cn" : language); dayjs.locale(language === "zh" ? "zh-cn" : language);
i18next.changeLanguage(language); i18next.changeLanguage(language);
} }
}, [language]);
useEffect(() => {
if (start_page) { if (start_page) {
navigate(start_page); navigate(start_page, { replace: true });
} }
}, [language, start_page]); }, [start_page]);
if (!routersEles) return null;
return ( return (
<SWRConfig value={{ errorRetryCount: 3 }}> <SWRConfig value={{ errorRetryCount: 3 }}>
@@ -111,29 +217,24 @@ const Layout = () => {
elevation={0} elevation={0}
className={`${OS} layout`} className={`${OS} layout`}
onContextMenu={(e) => { onContextMenu={(e) => {
// only prevent it on Windows
const validList = ["input", "textarea"];
const target = e.currentTarget;
if ( if (
OS === "windows" && OS === "windows" &&
!( !["input", "textarea"].includes(
validList.includes(target.tagName.toLowerCase()) || e.currentTarget.tagName.toLowerCase(),
target.isContentEditable ) &&
) !e.currentTarget.isContentEditable
) { ) {
e.preventDefault(); e.preventDefault();
} }
}} }}
sx={[ sx={[
({ palette }) => ({ ({ palette }) => ({ bgcolor: palette.background.paper }),
bgcolor: palette.background.paper,
}),
OS === "linux" OS === "linux"
? { ? {
borderRadius: "8px", borderRadius: "8px",
border: "2px solid var(--divider-color)", border: "1px solid var(--divider-color)",
width: "calc(100vw - 4px)", width: "calc(100vw - 0px)",
height: "calc(100vh - 4px)", height: "calc(100vh - 0px)",
} }
: {}, : {},
]} ]}
@@ -160,7 +261,7 @@ const Layout = () => {
/> />
<LogoSvg fill={isDark ? "white" : "black"} /> <LogoSvg fill={isDark ? "white" : "black"} />
</div> </div>
{<UpdateButton className="the-newbtn" />} <UpdateButton className="the-newbtn" />
</div> </div>
<List className="the-menu"> <List className="the-menu">
@@ -181,16 +282,14 @@ const Layout = () => {
</div> </div>
<div className="layout__right"> <div className="layout__right">
{ <div className="the-bar">
<div className="the-bar"> <div
<div className="the-dragbar"
className="the-dragbar" data-tauri-drag-region="true"
data-tauri-drag-region="true" style={{ width: "100%" }}
style={{ width: "100%" }} />
></div> {OS !== "macos" && <LayoutControl />}
{OS !== "macos" && <LayoutControl />} </div>
</div>
}
<TransitionGroup className="the-content"> <TransitionGroup className="the-content">
<CSSTransition <CSSTransition
+136 -98
View File
@@ -1,9 +1,14 @@
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState, useCallback } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, Button, IconButton, MenuItem } from "@mui/material"; import { Box, Button, IconButton, MenuItem } from "@mui/material";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
import { useTranslation } from "react-i18next"; 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 { closeAllConnections } from "@/services/api";
import { useConnectionSetting } from "@/services/states"; import { useConnectionSetting } from "@/services/states";
import { useClashInfo } from "@/hooks/use-clash"; import { useClashInfo } from "@/hooks/use-clash";
@@ -15,11 +20,15 @@ import {
ConnectionDetailRef, ConnectionDetailRef,
} from "@/components/connection/connection-detail"; } from "@/components/connection/connection-detail";
import parseTraffic from "@/utils/parse-traffic"; 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 { BaseStyledSelect } from "@/components/base/base-styled-select";
import useSWRSubscription from "swr/subscription"; import useSWRSubscription from "swr/subscription";
import { createSockette } from "@/utils/websocket"; import { createSockette } from "@/utils/websocket";
import { useTheme } from "@mui/material/styles"; import { useTheme } from "@mui/material/styles";
import { useVisibility } from "@/hooks/use-visibility";
const initConn: IConnections = { const initConn: IConnections = {
uploadTotal: 0, uploadTotal: 0,
@@ -32,7 +41,7 @@ type OrderFunc = (list: IConnectionsItem[]) => IConnectionsItem[];
const ConnectionsPage = () => { const ConnectionsPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { clashInfo } = useClashInfo(); const { clashInfo } = useClashInfo();
const pageVisible = useVisibility();
const theme = useTheme(); const theme = useTheme();
const isDark = theme.palette.mode === "dark"; const isDark = theme.palette.mode === "dark";
const [match, setMatch] = useState(() => (_: string) => true); const [match, setMatch] = useState(() => (_: string) => true);
@@ -54,96 +63,121 @@ const ConnectionsPage = () => {
list.sort((a, b) => b.curDownload! - a.curDownload!), 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< const { data: connData = initConn } = useSWRSubscription<
IConnections, IConnections,
any, any,
"getClashConnections" | null "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( const connections: IConnectionsItem[] = [];
`ws://${server}/connections?token=${encodeURIComponent(secret)}`,
{
onmessage(event) {
// meta v1.15.0 出现 data.connections 为 null 的情况
const data = JSON.parse(event.data) as IConnections;
// 尽量与前一次 connections 的展示顺序保持一致
next(null, (old = initConn) => {
const oldConn = old.connections;
const maxLen = data.connections?.length;
const connections: IConnectionsItem[] = []; const rest = (data.connections || []).filter((each) => {
const index = oldConn.findIndex((o) => o.id === each.id);
const rest = (data.connections || []).filter((each) => { if (index >= 0 && index < maxLen) {
const index = oldConn.findIndex((o) => o.id === each.id); const old = oldConn[index];
each.curUpload = each.upload - old.upload;
each.curDownload = each.download - old.download;
if (index >= 0 && index < maxLen) { connections[index] = each;
const old = oldConn[index]; return false;
each.curUpload = each.upload - old.upload; }
each.curDownload = each.download - old.download; return true;
});
connections[index] = each; for (let i = 0; i < maxLen; ++i) {
return false; 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) { onerror(event) {
if (!connections[i] && rest.length > 0) { next(event);
connections[i] = rest.shift()!; },
connections[i].curUpload = 0;
connections[i].curDownload = 0;
}
}
return { ...data, connections };
});
}, },
onerror(event) { 3,
next(event); );
},
},
3,
);
return () => { return () => {
s.close(); 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]; const orderFunc = orderOpts[curOrderOpt];
let connections = connData.connections.filter((conn) => let connections = displayData.connections.filter((conn) => {
match(conn.metadata.host || conn.metadata.destinationIP || ""), const { host, destinationIP, process } = conn.metadata;
); return (
match(host || "") || match(destinationIP || "") || match(process || "")
);
});
if (orderFunc) connections = orderFunc(connections); if (orderFunc) connections = orderFunc(connections);
let download = 0; return [connections];
let upload = 0; }, [displayData, match, curOrderOpt]);
connections.forEach((x) => {
download += x.download;
upload += x.upload;
});
return [connections, download, upload];
}, [connData, match, curOrderOpt]);
const onCloseAll = useLockFn(closeAllConnections); const onCloseAll = useLockFn(closeAllConnections);
const detailRef = useRef<ConnectionDetailRef>(null!); 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 ( return (
<BasePage <BasePage
full full
title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>} title={<span style={{ whiteSpace: "nowrap" }}>{t("Connections")}</span>}
contentStyle={{ height: "100%" }} contentStyle={{
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
borderRadius: "8px",
}}
header={ header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<Box sx={{ mx: 1 }}> <Box sx={{ mx: 1 }}>
{t("Downloaded")}: {parseTraffic(download)} {t("Downloaded")}: {parseTraffic(displayData.downloadTotal)}
</Box> </Box>
<Box sx={{ mx: 1 }}> <Box sx={{ mx: 1 }}>
{t("Uploaded")}: {parseTraffic(upload)} {t("Uploaded")}: {parseTraffic(displayData.uploadTotal)}
</Box> </Box>
<IconButton <IconButton
color="inherit" color="inherit"
@@ -157,16 +191,23 @@ const ConnectionsPage = () => {
} }
> >
{isTableLayout ? ( {isTableLayout ? (
<span title={t("List View")}> <TableRowsRounded titleAccess={t("List View")} />
<TableRowsRounded fontSize="inherit" />
</span>
) : ( ) : (
<span title={t("Table View")}> <TableChartRounded titleAccess={t("Table View")} />
<TableChartRounded fontSize="inherit" /> )}
</span> </IconButton>
<IconButton
color="inherit"
size="small"
onClick={handlePauseToggle}
title={isPaused ? t("Resume") : t("Pause")}
>
{isPaused ? (
<PlayCircleOutlineRounded />
) : (
<PauseCircleOutlineRounded />
)} )}
</IconButton> </IconButton>
<Button size="small" variant="contained" onClick={onCloseAll}> <Button size="small" variant="contained" onClick={onCloseAll}>
<span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span> <span style={{ whiteSpace: "nowrap" }}>{t("Close All")}</span>
</Button> </Button>
@@ -182,6 +223,9 @@ const ConnectionsPage = () => {
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
userSelect: "text", userSelect: "text",
position: "sticky",
top: 0,
zIndex: 2,
}} }}
> >
{!isTableLayout && ( {!isTableLayout && (
@@ -196,37 +240,31 @@ const ConnectionsPage = () => {
))} ))}
</BaseStyledSelect> </BaseStyledSelect>
)} )}
<BaseSearchBox onSearch={(match) => setMatch(() => match)} /> <BaseSearchBox onSearch={handleSearch} />
</Box> </Box>
<Box {filterConn.length === 0 ? (
height="calc(100% - 65px)" <BaseEmpty />
sx={{ ) : isTableLayout ? (
userSelect: "text", <ConnectionTable
margin: "10px", connections={filterConn}
borderRadius: "8px", onShowDetail={(detail) => detailRef.current?.open(detail)}
bgcolor: isDark ? "#282a36" : "#ffffff", />
}} ) : (
> <Virtuoso
{filterConn.length === 0 ? ( style={{
<BaseEmpty /> flex: 1,
) : isTableLayout ? ( borderRadius: "8px",
<ConnectionTable }}
connections={filterConn} data={filterConn}
onShowDetail={(detail) => detailRef.current?.open(detail)} itemContent={(_, item) => (
/> <ConnectionItem
) : ( value={item}
<Virtuoso onShowDetail={() => detailRef.current?.open(item)}
data={filterConn} />
itemContent={(_, item) => ( )}
<ConnectionItem />
value={item} )}
onShowDetail={() => detailRef.current?.open(item)}
/>
)}
/>
)}
</Box>
<ConnectionDetail ref={detailRef} /> <ConnectionDetail ref={detailRef} />
</BasePage> </BasePage>
); );
+31 -28
View File
@@ -32,11 +32,15 @@ const LogPage = () => {
const filterLogs = useMemo(() => { const filterLogs = useMemo(() => {
return logData return logData
? logData.filter((data) => ? logData.filter((data) => {
logLevel === "all" // 构建完整的搜索文本,包含时间、类型和内容
? match(data.payload) const searchText =
: data.type.includes(logLevel) && match(data.payload), `${data.time || ""} ${data.type} ${data.payload}`.toLowerCase();
)
return logLevel === "all"
? match(searchText)
: data.type.toLowerCase() === logLevel && match(searchText);
})
: []; : [];
}, [logData, logLevel, match]); }, [logData, logLevel, match]);
@@ -44,7 +48,12 @@ const LogPage = () => {
<BasePage <BasePage
full full
title={t("Logs")} title={t("Logs")}
contentStyle={{ height: "100%" }} contentStyle={{
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
}}
header={ header={
<Box sx={{ display: "flex", alignItems: "center", gap: 2 }}> <Box sx={{ display: "flex", alignItems: "center", gap: 2 }}>
<IconButton <IconButton
@@ -65,7 +74,7 @@ const LogPage = () => {
size="small" size="small"
variant="contained" variant="contained"
onClick={() => { onClick={() => {
clearLogs(logLevel); clearLogs();
}} }}
> >
{t("Clear")} {t("Clear")}
@@ -102,27 +111,21 @@ const LogPage = () => {
/> />
</Box> </Box>
<Box {filterLogs.length > 0 ? (
height="calc(100% - 65px)" <Virtuoso
sx={{ initialTopMostItemIndex={999}
margin: "10px", data={filterLogs}
borderRadius: "8px", style={{
bgcolor: isDark ? "#282a36" : "#ffffff", flex: 1,
}} }}
> itemContent={(index, item) => (
{filterLogs.length > 0 ? ( <LogItem value={item} searchState={searchState} />
<Virtuoso )}
initialTopMostItemIndex={999} followOutput={"smooth"}
data={filterLogs} />
itemContent={(index, item) => ( ) : (
<LogItem value={item} searchState={searchState} /> <BaseEmpty />
)} )}
followOutput={"smooth"}
/>
) : (
<BaseEmpty />
)}
</Box>
</BasePage> </BasePage>
); );
}; };
+47 -45
View File
@@ -27,6 +27,7 @@ import { useTranslation } from "react-i18next";
import { import {
importProfile, importProfile,
enhanceProfiles, enhanceProfiles,
//restartCore,
getRuntimeLogs, getRuntimeLogs,
deleteProfile, deleteProfile,
updateProfile, updateProfile,
@@ -173,11 +174,11 @@ const ProfilePage = () => {
}, 100); }, 100);
try { try {
await patchProfiles({ current: profile }); const success = await patchProfiles({ current: profile });
await mutateLogs(); await mutateLogs();
closeAllConnections(); closeAllConnections();
await activateSelected(); await activateSelected();
if (notifySuccess) { if (notifySuccess && success) {
Notice.success(t("Profile Switched"), 1000); Notice.success(t("Profile Switched"), 1000);
} }
} catch (err: any) { } catch (err: any) {
@@ -366,20 +367,19 @@ const ProfilePage = () => {
{t("New")} {t("New")}
</Button> </Button>
</Stack> </Stack>
<Box
sx={{ <DndContext
pt: 1, sensors={sensors}
mb: 0.5, collisionDetection={closestCenter}
pl: "10px", onDragEnd={onDragEnd}
mr: "10px",
height: "calc(100% - 68px)",
overflowY: "auto",
}}
> >
<DndContext <Box
sensors={sensors} sx={{
collisionDetection={closestCenter} pl: "10px",
onDragEnd={onDragEnd} pr: "10px",
height: "calc(100% - 48px)",
overflowY: "auto",
}}
> >
<Box sx={{ mb: 1.5 }}> <Box sx={{ mb: 1.5 }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}> <Grid2 container spacing={{ xs: 1, lg: 1 }}>
@@ -400,6 +400,8 @@ const ProfilePage = () => {
onSave={async (prev, curr) => { onSave={async (prev, curr) => {
if (prev !== curr && profiles.current === item.uid) { if (prev !== curr && profiles.current === item.uid) {
await onEnhance(false); await onEnhance(false);
// await restartCore();
// Notice.success(t("Clash Core Restarted"), 1000);
} }
}} }}
onDelete={() => onDelete(item.uid)} onDelete={() => onDelete(item.uid)}
@@ -409,38 +411,38 @@ const ProfilePage = () => {
</SortableContext> </SortableContext>
</Grid2> </Grid2>
</Box> </Box>
</DndContext> <Divider
<Divider variant="middle"
variant="middle" flexItem
flexItem sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }}
sx={{ width: `calc(100% - 32px)`, borderColor: dividercolor }} ></Divider>
></Divider> <Box sx={{ mt: 1.5, mb: "10px" }}>
<Box sx={{ mt: 1.5 }}> <Grid2 container spacing={{ xs: 1, lg: 1 }}>
<Grid2 container spacing={{ xs: 1, lg: 1 }}> <Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}> <ProfileMore
<ProfileMore id="Merge"
id="Merge" onSave={async (prev, curr) => {
onSave={async (prev, curr) => { if (prev !== curr) {
if (prev !== curr) { await onEnhance(false);
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> </Grid2>
<Grid2 size={{ xs: 12, sm: 6, md: 6, lg: 6 }}> </Box>
<ProfileMore
id="Script"
logInfo={chainLogs["Script"]}
onSave={async (prev, curr) => {
if (prev !== curr) {
await onEnhance(false);
}
}}
/>
</Grid2>
</Grid2>
</Box> </Box>
</Box> </DndContext>
<ProfileViewer <ProfileViewer
ref={viewerRef} ref={viewerRef}
+3 -3
View File
@@ -4,7 +4,7 @@ import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Box, Button, ButtonGroup } from "@mui/material"; import { Box, Button, ButtonGroup } from "@mui/material";
import { closeAllConnections, getClashConfig } from "@/services/api"; import { closeAllConnections, getClashConfig } from "@/services/api";
import { patchClashConfig } from "@/services/cmds"; import { patchClashConfig, patchClashMode } from "@/services/cmds";
import { useVerge } from "@/hooks/use-verge"; import { useVerge } from "@/hooks/use-verge";
import { BasePage } from "@/components/base"; import { BasePage } from "@/components/base";
import { ProxyGroups } from "@/components/proxy/proxy-groups"; import { ProxyGroups } from "@/components/proxy/proxy-groups";
@@ -29,7 +29,7 @@ const ProxyPage = () => {
if (mode !== curMode && verge?.auto_close_connection) { if (mode !== curMode && verge?.auto_close_connection) {
closeAllConnections(); closeAllConnections();
} }
await patchClashConfig({ mode }); await patchClashMode(mode);
mutateClash(); mutateClash();
}); });
@@ -42,7 +42,7 @@ const ProxyPage = () => {
return ( return (
<BasePage <BasePage
full full
contentStyle={{ height: "100%" }} contentStyle={{ height: "101.5%" }}
title={t("Proxy Groups")} title={t("Proxy Groups")}
header={ header={
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
+27 -29
View File
@@ -39,7 +39,12 @@ const RulesPage = () => {
<BasePage <BasePage
full full
title={t("Rules")} title={t("Rules")}
contentStyle={{ height: "100%" }} contentStyle={{
height: "100%",
display: "flex",
flexDirection: "column",
overflow: "auto",
}}
header={ header={
<Box display="flex" alignItems="center" gap={1}> <Box display="flex" alignItems="center" gap={1}>
<ProviderButton /> <ProviderButton />
@@ -59,34 +64,27 @@ const RulesPage = () => {
<BaseSearchBox onSearch={(match) => setMatch(() => match)} /> <BaseSearchBox onSearch={(match) => setMatch(() => match)} />
</Box> </Box>
<Box {rules.length > 0 ? (
height="calc(100% - 65px)" <>
sx={{ <Virtuoso
margin: "10px", ref={virtuosoRef}
borderRadius: "8px", data={rules}
bgcolor: isDark ? "#282a36" : "#ffffff", style={{
position: "relative", flex: 1,
}} }}
> itemContent={(index, item) => (
{rules.length > 0 ? ( <RuleItem index={index + 1} value={item} />
<> )}
<Virtuoso followOutput={"smooth"}
ref={virtuosoRef} scrollerRef={(ref) => {
data={rules} if (ref) ref.addEventListener("scroll", handleScroll);
itemContent={(index, item) => ( }}
<RuleItem index={index + 1} value={item} /> />
)} <ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
followOutput={"smooth"} </>
scrollerRef={(ref) => { ) : (
if (ref) ref.addEventListener("scroll", handleScroll); <BaseEmpty />
}} )}
/>
<ScrollTopButton onClick={scrollToTop} show={showScrollTop} />
</>
) : (
<BaseEmpty />
)}
</Box>
</BasePage> </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 { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { BasePage, Notice } from "@/components/base"; import { BasePage, Notice } from "@/components/base";
import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material"; import { GitHub, HelpOutlineRounded, Telegram } from "@mui/icons-material";
import { openWebUrl } from "@/services/cmds"; 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 SettingClash from "@/components/setting/setting-clash";
import SettingSystem from "@/components/setting/setting-system"; import SettingSystem from "@/components/setting/setting-system";
import { useThemeMode } from "@/services/states"; import { useThemeMode } from "@/services/states";
@@ -88,10 +96,19 @@ const SettingPage = () => {
<Box <Box
sx={{ sx={{
borderRadius: 2, borderRadius: 2,
marginBottom: 1.5,
backgroundColor: isDark ? "#282a36" : "#ffffff", backgroundColor: isDark ? "#282a36" : "#ffffff",
}} }}
> >
<SettingVerge onError={onError} /> <SettingVergeBasic onError={onError} />
</Box>
<Box
sx={{
borderRadius: 2,
backgroundColor: isDark ? "#282a36" : "#ffffff",
}}
>
<SettingVergeAdvanced onError={onError} />
</Box> </Box>
</Grid> </Grid>
</Grid> </Grid>
+13 -1
View File
@@ -91,6 +91,10 @@ export async function patchClashConfig(payload: Partial<IConfigData>) {
return invoke<void>("patch_clash_config", { payload }); return invoke<void>("patch_clash_config", { payload });
} }
export async function patchClashMode(payload: String) {
return invoke<void>("patch_clash_mode", { payload });
}
export async function getVergeConfig() { export async function getVergeConfig() {
return invoke<IVergeConfig>("get_verge_config"); return invoke<IVergeConfig>("get_verge_config");
} }
@@ -115,7 +119,7 @@ export async function getAutotemProxy() {
} }
export async function changeClashCore(clashCore: string) { 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() { export async function restartCore() {
@@ -237,3 +241,11 @@ export async function listWebDavBackup() {
}); });
return list; 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 ru from "@/locales/ru.json";
import zh from "@/locales/zh.json"; import zh from "@/locales/zh.json";
import fa from "@/locales/fa.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 = { export const languages = { en, ru, zh, fa, tt, id, ar };
en: { translation: en },
ru: { translation: ru }, const resources = Object.fromEntries(
zh: { translation: zh }, Object.entries(languages).map(([key, value]) => [
fa: { translation: fa }, key,
}; { translation: value },
]),
);
i18n.use(initReactI18next).init({ i18n.use(initReactI18next).init({
resources, resources,
lng: "en", lng: "zh",
fallbackLng: "en", fallbackLng: "zh",
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
+5
View File
@@ -120,6 +120,7 @@ interface IConnectionsItem {
sourcePort: string; sourcePort: string;
destinationPort: string; destinationPort: string;
destinationIP?: string; destinationIP?: string;
remoteDestination?: string;
process?: string; process?: string;
processPath?: string; processPath?: string;
}; };
@@ -706,10 +707,13 @@ interface IVergeConfig {
common_tray_icon?: boolean; common_tray_icon?: boolean;
sysproxy_tray_icon?: boolean; sysproxy_tray_icon?: boolean;
tun_tray_icon?: boolean; tun_tray_icon?: boolean;
enable_tray_speed?: boolean;
enable_tun_mode?: boolean; enable_tun_mode?: boolean;
enable_lite_mode?: boolean;
enable_auto_launch?: boolean; enable_auto_launch?: boolean;
enable_silent_start?: boolean; enable_silent_start?: boolean;
enable_system_proxy?: boolean; enable_system_proxy?: boolean;
enable_global_hotkey?: boolean;
proxy_auto_config?: boolean; proxy_auto_config?: boolean;
pac_file_content?: string; pac_file_content?: string;
enable_random_port?: boolean; enable_random_port?: boolean;
@@ -723,6 +727,7 @@ interface IVergeConfig {
verge_socks_enabled?: boolean; verge_socks_enabled?: boolean;
verge_http_enabled?: boolean; verge_http_enabled?: boolean;
enable_proxy_guard?: boolean; enable_proxy_guard?: boolean;
enable_bypass_check?: boolean;
use_default_bypass?: boolean; use_default_bypass?: boolean;
proxy_guard_duration?: number; proxy_guard_duration?: number;
system_proxy_bypass?: string; system_proxy_bypass?: string;
+57 -1
View File
@@ -10,6 +10,17 @@ const KEY_MAP: Record<string, string> = {
",": "Comma", ",": "Comma",
".": "Period", ".": "Period",
"/": "Slash", "/": "Slash",
// 数字键映射
"1": "Digit1",
"2": "Digit2",
"3": "Digit3",
"4": "Digit4",
"5": "Digit5",
"6": "Digit6",
"7": "Digit7",
"8": "Digit8",
"9": "Digit9",
"0": "Digit0",
// Option + 特殊字符映射 // Option + 特殊字符映射
"–": "Minus", // Option + - "–": "Minus", // Option + -
"≠": "Equal", // Option + = "≠": "Equal", // Option + =
@@ -57,7 +68,52 @@ const mapKeyCombination = (key: string): string => {
}; };
export const parseHotkey = (key: string) => { export const parseHotkey = (key: string) => {
let temp = key.toUpperCase(); let temp = key.toUpperCase();
console.log(temp);
// 处理特殊符号到键位的映射
switch (temp) {
// 数字键符号
case "!":
return "DIGIT1"; // shift + 1
case "@":
return "DIGIT2"; // shift + 2
case "#":
return "DIGIT3"; // shift + 3
case "$":
return "DIGIT4"; // shift + 4
case "%":
return "DIGIT5"; // shift + 5
case "^":
return "DIGIT6"; // shift + 6
case "&":
return "DIGIT7"; // shift + 7
case "*":
return "DIGIT8"; // shift + 8
case "(":
return "DIGIT9"; // shift + 9
case ")":
return "DIGIT0"; // shift + 0
// 其他特殊符号
case "?":
return "SLASH"; // shift + /
case ":":
return "SEMICOLON"; // shift + ;
case "+":
return "EQUAL"; // shift + =
case "_":
return "MINUS"; // shift + -
case '"':
return "QUOTE"; // shift + '
case "<":
return "COMMA"; // shift + ,
case ">":
return "PERIOD"; // shift + .
case "{":
return "BRACKETLEFT"; // shift + [
case "}":
return "BRACKETRIGHT"; // shift + ]
case "|":
return "BACKSLASH"; // shift + \
}
if (temp.startsWith("ARROW")) { if (temp.startsWith("ARROW")) {
temp = temp.slice(5); temp = temp.slice(5);
+15 -11
View File
@@ -36,7 +36,7 @@ export default function parseUri(uri: string): IProxyConfig {
function getIfNotBlank( function getIfNotBlank(
value: string | undefined, value: string | undefined,
dft?: string dft?: string,
): string | undefined { ): string | undefined {
return value && value.trim() !== "" ? value : dft; return value && value.trim() !== "" ? value : dft;
} }
@@ -180,7 +180,7 @@ function URI_SS(line: string): IProxyShadowsocksConfig {
if (v2rayPlugin) { if (v2rayPlugin) {
proxy.plugin = "v2ray-plugin"; proxy.plugin = "v2ray-plugin";
proxy["plugin-opts"] = JSON.parse( proxy["plugin-opts"] = JSON.parse(
decodeBase64OrOriginal(v2rayPlugin) decodeBase64OrOriginal(v2rayPlugin),
); );
} }
} }
@@ -193,7 +193,7 @@ function URI_SS(line: string): IProxyShadowsocksConfig {
const portIdx = serverAndPort?.lastIndexOf(":") ?? 0; const portIdx = serverAndPort?.lastIndexOf(":") ?? 0;
proxy.server = serverAndPort?.substring(0, portIdx) ?? ""; proxy.server = serverAndPort?.substring(0, portIdx) ?? "";
proxy.port = parseInt( proxy.port = parseInt(
`${serverAndPort?.substring(portIdx + 1)}`.match(/\d+/)?.[0] ?? "" `${serverAndPort?.substring(portIdx + 1)}`.match(/\d+/)?.[0] ?? "",
); );
const userInfo = userInfoStr.match(/(^.*?):(.*$)/); const userInfo = userInfoStr.match(/(^.*?):(.*$)/);
proxy.cipher = getCipher(userInfo?.[1]); proxy.cipher = getCipher(userInfo?.[1]);
@@ -252,7 +252,7 @@ function URI_SSR(line: string): IProxyshadowsocksRConfig {
const serverAndPort = line.substring(0, splitIdx); const serverAndPort = line.substring(0, splitIdx);
const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":")); const server = serverAndPort.substring(0, serverAndPort.lastIndexOf(":"));
const port = parseInt( const port = parseInt(
serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1) serverAndPort.substring(serverAndPort.lastIndexOf(":") + 1),
); );
let params = line let params = line
@@ -284,12 +284,12 @@ function URI_SSR(line: string): IProxyshadowsocksRConfig {
...proxy, ...proxy,
name: other_params.remarks name: other_params.remarks
? decodeBase64OrOriginal(other_params.remarks).trim() ? decodeBase64OrOriginal(other_params.remarks).trim()
: proxy.server ?? "", : (proxy.server ?? ""),
"protocol-param": getIfNotBlank( "protocol-param": getIfNotBlank(
decodeBase64OrOriginal(other_params.protoparam || "").replace(/\s/g, "") decodeBase64OrOriginal(other_params.protoparam || "").replace(/\s/g, ""),
), ),
"obfs-param": getIfNotBlank( "obfs-param": getIfNotBlank(
decodeBase64OrOriginal(other_params.obfsparam || "").replace(/\s/g, "") decodeBase64OrOriginal(other_params.obfsparam || "").replace(/\s/g, ""),
), ),
}; };
return proxy; return proxy;
@@ -330,7 +330,7 @@ function URI_VMESS(line: string): IProxyVmessConfig {
proxy["ws-opts"] = { proxy["ws-opts"] = {
path: path:
(getIfNotBlank(params["obfs-path"]) || '"/"').match( (getIfNotBlank(params["obfs-path"]) || '"/"').match(
/^"(.*)"$/ /^"(.*)"$/,
)?.[1] || "/", )?.[1] || "/",
headers: { headers: {
Host: Host:
@@ -492,6 +492,9 @@ function URI_VMESS(line: string): IProxyVmessConfig {
} }
} }
/**
* VLess URL Decode.
*/
function URI_VLESS(line: string): IProxyVlessConfig { function URI_VLESS(line: string): IProxyVlessConfig {
line = line.split("vless://")[1]; line = line.split("vless://")[1];
let isShadowrocket; let isShadowrocket;
@@ -571,9 +574,11 @@ function URI_VLESS(line: string): IProxyVlessConfig {
if (params.headerType === "http") { if (params.headerType === "http") {
proxy.network = "http"; proxy.network = "http";
} else { } else if (params.type === "ws") {
proxy.network = "ws"; proxy.network = "ws";
httpupgrade = true; httpupgrade = true;
} else {
proxy.network = "tcp";
} }
if (!proxy.network && isShadowrocket && params.obfs) { if (!proxy.network && isShadowrocket && params.obfs) {
switch (params.type) { switch (params.type) {
@@ -619,7 +624,7 @@ function URI_VLESS(line: string): IProxyVlessConfig {
opts["v2ray-http-upgrade-fast-open"] = true; opts["v2ray-http-upgrade-fast-open"] = true;
} }
if (Object.keys(opts).length > 0) { if (Object.keys(opts).length > 0) {
proxy[`${proxy.network}-opts`] = opts; proxy[`ws-opts`] = opts;
} }
} }
@@ -631,7 +636,6 @@ function URI_VLESS(line: string): IProxyVlessConfig {
proxy.servername = Array.isArray(httpHost) ? httpHost[0] : httpHost; proxy.servername = Array.isArray(httpHost) ? httpHost[0] : httpHost;
} }
} }
return proxy; return proxy;
} }