Compare commits

...

114 commits

Author SHA1 Message Date
2dust
899b3fc97b up 7.15.3
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-14 19:52:42 +08:00
DHR60
a1490d0ac1
Update singbox_fakeip_filter (#8117)
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-10-12 09:59:30 +08:00
DHR60
b23f49ffce
Remove unnecessary settings (#8107)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-11 19:22:26 +08:00
2dust
9a9e28e494 Update GlobalHotKeys
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-10 19:25:54 +08:00
2dust
65ee5eb510 Fix,remove NaiveproxyFmt HysteriaFmt ,adjust ClashFmt
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8102
2025-10-10 17:12:45 +08:00
DHR60
1f42d32e1a
Fix Freedom Resolver (#8100) 2025-10-10 16:58:18 +08:00
2dust
f2ed8c1d6b up 7.15.2
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-09 20:29:45 +08:00
2dust
308b216d1b Adjust ActionPrecheckManager 2025-10-09 20:29:25 +08:00
2dust
c713f5c8f5 Update Directory.Packages.props 2025-10-09 20:22:41 +08:00
2dust
6771eb25d1 Adjust ActionPrecheckManager 2025-10-09 20:22:35 +08:00
2dust
91af50f99a Optimize code ,add IsGroupType extension. Adjust EConfigType
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-10-08 17:13:54 +08:00
2dust
a559586e71 Code clean 2025-10-08 15:48:51 +08:00
2dust
929520775d Bug fix 2025-10-08 15:48:45 +08:00
2dust
4eaf31bbf8 Fix
https://github.com/2dust/v2rayN/issues/8092
2025-10-08 14:36:47 +08:00
2dust
1607525539 Optimize the ruletype UI 2025-10-08 14:12:16 +08:00
DHR60
31b5b4ca0c
Add rule type selection to routing rules (#8007)
* Add rule type selection to routing rules

* Use enum

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2025-10-08 10:40:26 +08:00
2dust
64c7fea2bc Update Directory.Packages.props
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-07 19:17:35 +08:00
2dust
f76fd364a2 Rename ProfileGroupItem.ParentIndexId to IndexId
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
Because ProfileGroupItem is an extension of ProfileItem, it is better to name the fields the same way.
2025-10-07 14:01:36 +08:00
2dust
0a1d6db9d1 Add cycle check for AddGroupServerViewModel 2025-10-07 13:54:31 +08:00
2dust
7a750a127e Rename ActionPrecheckService 2025-10-07 13:53:33 +08:00
2dust
fce4a7b74c Optimization and improvement, tray, etc.
https://github.com/2dust/v2rayN/pull/8083
2025-10-07 11:16:20 +08:00
DHR60
fec7353703
PreCheck (#7902)
* PreCheck

* Fix
2025-10-07 10:03:20 +08:00
Weheal
40c90d5b3b
Fix: AutoHideStartup's bug of displaying window before hiding it. (#8083)
* Fix: AutoHideStartup's bug of displaying window before hiding it.

* Disable AutoHideStartup for Linux

* Revert "Disable AutoHideStartup for Linux"

This reverts commit 09f27e3455.

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2025-10-07 10:02:53 +08:00
2dust
9c58fec8d4 Bug fix
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-10-05 19:55:52 +08:00
DHR60
11343a30fd
Multi profile (#7929)
* Multi Profile

* VM and wpf

* avalonia

* Fix right click not working

* Exclude specific profile types from selection

* Rename

* Add Policy Group support

* Add generate policy group

* Adjust UI

* Add Proxy Chain support

* Fix

* Add fallback support

* Add PolicyGroup include other Group support

* Add group in traffic splitting support

* Avoid duplicate tags

* Refactor

* Adjust chained proxy, actual outbound is at the top

Based on actual network flow instead of data packets

* Add helper function

* Refactor

* Add chain selection control to group outbounds

* Avoid self-reference

* Fix

* Improves Tun2Socks address handling

* Avoids circular dependency in profile groups

Adds cycle detection to prevent infinite loops when evaluating profile groups.

This ensures that profile group configurations don't result in stack overflow errors when groups reference each other, directly or indirectly.

* Fix

* Fix

* Update ProfileGroupItem.cs

* Refactor

* Remove unnecessary checks

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2025-10-05 16:27:34 +08:00
2dust
3693a7fee6 up 7.15.1
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-05 09:22:47 +08:00
2dust
a452bbe140 Fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8061
2025-10-04 19:54:15 +08:00
DHR60
185c5e4bfb
Fix (#8057) 2025-10-04 16:17:39 +08:00
2dust
bbe64aa970 Remove AutoCompleteBox
https://github.com/2dust/v2rayN/pull/8067
2025-10-04 16:16:32 +08:00
DHR60
513662d89a
Use editable ComboBox instead of AutoCompleteBox (#8067)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
* Update Avalonia

* Use editable ComboBox instead of AutoCompleteBox
2025-10-04 15:18:37 +08:00
2dust
22f0d04f01 Fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/8060
2025-10-03 14:13:03 +08:00
2dust
d7c5161431 Optimize and improve
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-02 19:55:49 +08:00
2dust
12cc09d0c9 Bug fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-10-01 20:17:26 +08:00
2dust
5b12c36da5 Optimize and improve, encapsulate ProcessService 2025-10-01 19:49:28 +08:00
DHR60
e970372a9f
Fix some minor UI bugs (#8053) 2025-10-01 16:47:22 +08:00
2dust
5d6c5da9d9 up 7.15.0
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-28 19:12:58 +08:00
2dust
ade2db3903 Code clean 2025-09-28 19:12:17 +08:00
Wydy
7f07279a4c
Update pac (#7991) 2025-09-28 19:08:29 +08:00
2dust
b25d4d57bd Fix ProfilesSelectWindow
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-27 19:46:31 +08:00
2dust
46edd8f9a4 Bug fix 2025-09-27 18:07:20 +08:00
JieXu
ebb95b5ee8
Update MsgView.axaml.cs (#8042) 2025-09-27 17:02:49 +08:00
2dust
dc4611a258 Adjust qrcode width
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-26 20:36:27 +08:00
2dust
03d5b7a05b Bug fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-26 17:11:48 +08:00
2dust
a652fd879b Added simple highlight function to the message view 2025-09-26 15:29:46 +08:00
2dust
326bf334e7 Optimize and improve MsgView 2025-09-26 15:07:33 +08:00
JieXu
21a773f400
Update MsgView.axaml.cs Plan C (#8035)
* Add avaloniaEdit for test

* Adjust avaloniaEdit

* Optimize and improve message function

* Update build-linux.yml

* Update MsgView.axaml

* Update MsgView.axaml.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2025-09-26 13:55:35 +08:00
2dust
d86003df55 Optimize and improve the Subject
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-25 10:56:10 +08:00
2dust
faff8e4ea2 Remove secret data from mihomo configuration
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-24 18:41:00 +08:00
2dust
6b85aa0b03 Remove Splat.NLog package 2025-09-24 10:57:23 +08:00
2dust
671678724b Optimization and improvement, using event subscribers 2025-09-24 10:57:06 +08:00
2dust
e96a4818c4 Optimization and improvement
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-23 15:31:19 +08:00
2dust
0377e7ce19 Optimization and improvement, using event subscribers 2025-09-23 14:27:42 +08:00
2dust
6929886b3e Optimization and improvement, using event subscribers 2025-09-23 12:08:43 +08:00
2dust
721d70c8c7 Update Directory.Packages.props 2025-09-23 11:39:57 +08:00
2dust
27b45aee83 Optimization and improvement, using event subscribers 2025-09-23 11:39:55 +08:00
2dust
18ac76e683 up 7.14.12
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-21 14:50:01 +08:00
2dust
3e1e23a524 Update Directory.Packages.props 2025-09-21 14:48:54 +08:00
2dust
534c7ab444 Optimize and improve QR code display 2025-09-21 14:35:49 +08:00
2dust
c2c13ad318 Create v2rayN.slnx
https://github.com/2dust/v2rayN/pull/7969
2025-09-21 12:12:24 +08:00
2dust
3a21596d95 Fix node domain resolving in TUN mode
https://github.com/2dust/v2rayN/pull/7989
2025-09-21 12:05:06 +08:00
2dust
ef30d389dc up 7.14.11
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-20 14:06:55 +08:00
2dust
bf8783fed7 Update CheckUpdateViewModel.cs 2025-09-20 14:06:41 +08:00
DHR60
4e042295d2
Add global fakeip and fakeip filter (#7919)
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-13 14:55:30 +08:00
2dust
33d9c5db6c up GlobalUsings 2025-09-13 14:46:35 +08:00
DHR60
cb182125f6
Fix (#7946)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/pull/7937
2025-09-13 11:13:09 +08:00
2dust
ec627bdb82 up 7.14.10 2025-09-13 09:53:03 +08:00
2dust
4606e78570 Update Directory.Packages.props 2025-09-13 09:46:28 +08:00
2dust
f00e968b8f Bug fix
https://github.com/2dust/v2rayN/issues/7944
2025-09-13 09:41:34 +08:00
DHR60
a87a015c03
Fix some minor UI bugs (#7941)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-12 20:28:24 +08:00
2dust
c559914ff7 Fix
https://github.com/2dust/v2rayN/issues/7938
2025-09-12 17:01:53 +08:00
2dust
436d95576e Optimization and improvement JsonUtils 2025-09-12 16:45:55 +08:00
DHR60
54e83391d0
Pre-resolve to apply hosts (#7937) 2025-09-12 16:28:31 +08:00
JieXu
3e0578f775
Update CheckUpdateViewModel.cs (#7932)
* Update CheckUpdateViewModel.cs

* Update Utils.cs

* Update Utils.cs

* Update Utils.cs

* Update CheckUpdateViewModel.cs

* Update CheckUpdateViewModel.cs

* Update Utils.cs
2025-09-12 16:24:59 +08:00
2dust
29a5abf4d6 Optimization and improvement
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-10 19:43:11 +08:00
2dust
b54c67d6f1 up 7.14.9
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-09 20:18:55 +08:00
2dust
b49486cc23 Update ProfilesSelectWindow.axaml 2025-09-09 20:00:00 +08:00
JieXu
b95830b3d5
Update package-rhel.sh package-debian.sh MainWindowViewModel.cs (#7910)
* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update package-rhel.sh

* Update MainWindowViewModel.cs

* Update package-rhel.sh

* Update package-debian.sh
2025-09-09 19:51:10 +08:00
2dust
8e0c5cb9aa Bug fix
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/7914
2025-09-09 17:55:15 +08:00
2dust
6ffb3bd30c up 7.14.8
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-08 18:48:56 +08:00
2dust
2826444ffc Code clean 2025-09-08 18:45:21 +08:00
JieXu
56c3e9c46d
Fix package-appimage.sh bugs. (#7904)
* Update package-appimage.sh

* Delete pkg2appimage.yml
2025-09-08 18:02:54 +08:00
th1nker
0770e30034
fix: 修正获取系统hosts (#7903)
- 修复当host的记录存在行尾注释时,无法将其添加到dns.host中

示例hosts
```
127.0.0.1 test1.com
127.0.0.1 test2.com # test
```
在之前仅仅会添加`127.0.0.1 test1.com`这条记录,而忽略另一条
2025-09-08 18:02:44 +08:00
DHR60
04195c2957
Profiles Select Window (#7891)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
* Profiles Select Window

* Sort

* wpf

* avalonia

* Allow single select

* Fix

* Add Config Type Filter

* Remove unnecessary
2025-09-07 18:58:59 +08:00
JieXu
d18d74ac1c
Update package-debian.sh (#7899)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-07 16:39:54 +08:00
2dust
6391667c15 up 7.14.7
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-06 16:59:54 +08:00
2dust
7f26445327 Update Directory.Packages.props 2025-09-06 16:59:38 +08:00
2dust
291d4bd8e5 Update Directory.Packages.props 2025-09-06 16:52:57 +08:00
dependabot[bot]
f2f3a7eb5f
Bump actions/setup-dotnet from 4.3.1 to 5.0.0 (#7883)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4.3.1 to 5.0.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4.3.1...v5.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-06 15:46:34 +08:00
JieXu
e7609619d4
Update package-debian.sh (#7888) 2025-09-06 15:46:20 +08:00
DHR60
84bf9ecfaf
Fix DNS Regional Presets (#7885)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-05 18:11:16 +08:00
2dust
a2917b3ce8 Update Directory.Packages.props
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-09-04 20:50:25 +08:00
2dust
d094370209 up 7.14.6
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-03 19:05:45 +08:00
2dust
1a6fbf782d Using RxApp replace ViewAction
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-09-02 17:12:38 +08:00
2dust
3f67a23f8b up 7.14.5
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-08-31 19:55:17 +08:00
2dust
b8eb7e7b29 Optimization and Improvement. 2025-08-31 15:41:25 +08:00
2dust
1d69916410 Update GlobalHotKeys
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-08-31 14:21:22 +08:00
2dust
49fa103077 Optimize UI 2025-08-31 14:08:05 +08:00
2dust
e3a63db966 Using RxApp replace ViewAction
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-08-30 20:36:16 +08:00
DHR60
ef4a1903ec
Update mihomo download url (#7852) 2025-08-30 19:44:54 +08:00
2dust
5a3286dad1 Using RxApp replace ViewAction 2025-08-30 19:32:07 +08:00
2dust
058c6e4a85 Use Rx event subscription instead of MessageBus to send information
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-08-29 15:46:09 +08:00
2dust
ea1d438e40 Use Rx event subscription to replace MessageBus refresh configuration file function 2025-08-29 14:46:08 +08:00
2dust
a108eaf34b Optimization and Improvement.
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 11:53:30 +08:00
2dust
da28c639b3 Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 11:40:08 +08:00
2dust
8ef68127d4 Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 10:53:57 +08:00
2dust
f39d966a33 Optimization and Improvement.
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 10:31:09 +08:00
2dust
f83e83de13 Optimization and Improvement
Changed callback from synchronous Action<bool, string> to asynchronous Func<bool, string, Task>
2025-08-29 09:49:30 +08:00
2dust
abdafc9b3b up 7.14.4
Some checks failed
release Linux / build (Release) (push) Has been cancelled
release macOS / build (Release) (push) Has been cancelled
release Windows desktop (Avalonia UI) / build (Release) (push) Has been cancelled
release Windows / build (Release) (push) Has been cancelled
2025-08-27 20:29:16 +08:00
2dust
8f93c50151 Bug fix 2025-08-27 17:22:13 +08:00
2dust
fe7c505cc9 Update subscription using Task.Run 2025-08-27 17:14:24 +08:00
2dust
0d5afa4ff5 Optimizing SQLite performance
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
https://github.com/2dust/v2rayN/issues/7835
2025-08-26 20:56:28 +08:00
2dust
2ad716a4ad Remove Cursor="Hand" 2025-08-26 17:46:43 +08:00
DHR60
cddf88730f
Fix dns (#7834) 2025-08-26 17:34:12 +08:00
DHR60
3eb49aa24c
Add mieru support (#7828)
Some checks are pending
release Linux / build (Release) (push) Waiting to run
release macOS / build (Release) (push) Waiting to run
release Windows desktop (Avalonia UI) / build (Release) (push) Waiting to run
release Windows / build (Release) (push) Waiting to run
2025-08-25 17:43:53 +08:00
195 changed files with 7818 additions and 3364 deletions

View file

@ -22,7 +22,7 @@ jobs:
matrix:
configuration: [Release]
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -32,7 +32,7 @@ jobs:
fetch-depth: '0'
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@v5.0.0
- name: Setup
uses: actions/setup-dotnet@v4.3.1
uses: actions/setup-dotnet@v5.0.0
with:
dotnet-version: '8.0.x'

View file

@ -1,14 +1,67 @@
#!/bin/bash
set -euo pipefail
# Install deps
sudo apt update -y
sudo apt install -y libfuse2
wget -O pkg2appimage https://github.com/AppImageCommunity/pkg2appimage/releases/download/continuous/pkg2appimage-1eceb30-x86_64.AppImage
chmod a+x pkg2appimage
export AppImageOutputArch=$OutputArch
export OutputPath=$OutputPath64
./pkg2appimage ./pkg2appimage.yml
mv out/*.AppImage v2rayN-${AppImageOutputArch}.AppImage
export AppImageOutputArch=$OutputArchArm
export OutputPath=$OutputPathArm64
./pkg2appimage ./pkg2appimage.yml
mv out/*.AppImage v2rayN-${AppImageOutputArch}.AppImage
sudo apt install -y libfuse2 wget file
# Get tools
wget -qO appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool
# x86_64 AppDir
APPDIR_X64="AppDir-x86_64"
rm -rf "$APPDIR_X64"
mkdir -p "$APPDIR_X64/usr/lib/v2rayN" "$APPDIR_X64/usr/bin" "$APPDIR_X64/usr/share/applications" "$APPDIR_X64/usr/share/pixmaps"
cp -rf "$OutputPath64"/* "$APPDIR_X64/usr/lib/v2rayN" || true
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/usr/share/pixmaps/v2rayN.png" || true
[ -f "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_X64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_X64/v2rayN.png" || true
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_X64/AppRun"
chmod +x "$APPDIR_X64/AppRun"
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_X64/usr/bin/v2rayN"
cat > "$APPDIR_X64/v2rayN.desktop" <<EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
Exec=v2rayN
Icon=v2rayN
Terminal=false
Type=Application
Categories=Network;
EOF
install -Dm644 "$APPDIR_X64/v2rayN.desktop" "$APPDIR_X64/usr/share/applications/v2rayN.desktop"
ARCH=x86_64 ./appimagetool "$APPDIR_X64" "v2rayN-${OutputArch}.AppImage"
file "v2rayN-${OutputArch}.AppImage" | grep -q 'x86-64'
# aarch64 AppDir
APPDIR_ARM64="AppDir-aarch64"
rm -rf "$APPDIR_ARM64"
mkdir -p "$APPDIR_ARM64/usr/lib/v2rayN" "$APPDIR_ARM64/usr/bin" "$APPDIR_ARM64/usr/share/applications" "$APPDIR_ARM64/usr/share/pixmaps"
cp -rf "$OutputPathArm64"/* "$APPDIR_ARM64/usr/lib/v2rayN" || true
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/usr/share/pixmaps/v2rayN.png" || true
[ -f "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" ] && cp "$APPDIR_ARM64/usr/lib/v2rayN/v2rayN.png" "$APPDIR_ARM64/v2rayN.png" || true
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'cd "$HERE/usr/lib/v2rayN"' 'exec "$HERE/usr/lib/v2rayN/v2rayN" "$@"' > "$APPDIR_ARM64/AppRun"
chmod +x "$APPDIR_ARM64/AppRun"
ln -sf usr/lib/v2rayN/v2rayN "$APPDIR_ARM64/usr/bin/v2rayN"
cat > "$APPDIR_ARM64/v2rayN.desktop" <<EOF
[Desktop Entry]
Name=v2rayN
Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
Exec=v2rayN
Icon=v2rayN
Terminal=false
Type=Application
Categories=Network;
EOF
install -Dm644 "$APPDIR_ARM64/v2rayN.desktop" "$APPDIR_ARM64/usr/share/applications/v2rayN.desktop"
# aarch64 runtime
wget -qO runtime-aarch64 https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-aarch64
chmod +x runtime-aarch64
# build aarch64 AppImage
ARCH=aarch64 ./appimagetool --runtime-file ./runtime-aarch64 "$APPDIR_ARM64" "v2rayN-${OutputArchArm}.AppImage"
file "v2rayN-${OutputArchArm}.AppImage" | grep -q 'ARM aarch64'

View file

@ -28,6 +28,7 @@ Package: v2rayN
Version: $Version
Architecture: $Arch2
Maintainer: https://github.com/2dust/v2rayN
Depends: desktop-file-utils, xdg-utils
Description: A GUI client for Windows and Linux, support Xray core and sing-box-core and others
EOF
@ -52,7 +53,17 @@ sudo chmod 0755 "${PackagePath}/DEBIAN/postinst"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/v2rayN"
sudo chmod 0755 "${PackagePath}/opt/v2rayN/AmazTool"
# desktop && PATH
# Patch
# set owner to root:root
sudo chown -R root:root "${PackagePath}"
# set all directories to 755 (readable & traversable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type d -exec chmod 755 {} +
# set all regular files to 644 (readable by all users)
sudo find "${PackagePath}/opt/v2rayN" -type f -exec chmod 644 {} +
# ensure main binaries are 755 (executable by all users)
sudo chmod 755 "${PackagePath}/opt/v2rayN/v2rayN" 2>/dev/null || true
sudo chmod 755 "${PackagePath}/opt/v2rayN/AmazTool" 2>/dev/null || true
# build deb package
sudo dpkg-deb -Zxz --build $PackagePath
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"
sudo mv "${PackagePath}.deb" "v2rayN-${Arch}.deb"

View file

@ -1,11 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
# ===== Require Red Hat Enterprise Linux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ====
# == Require Red Hat Enterprise Linux/FedoraLinux/RockyLinux/AlmaLinux/CentOS OR Ubuntu/Debian ==
if [[ -r /etc/os-release ]]; then
. /etc/os-release
case "$ID" in
rhel|rocky|almalinux|centos|ubuntu|debian)
rhel|rocky|almalinux|fedora|centos|ubuntu|debian)
echo "[OK] Detected supported system: $NAME $VERSION_ID"
;;
*)
@ -390,25 +390,30 @@ download_mihomo() {
chmod +x "$outroot/bin/mihomo/mihomo" || true
}
# Move geo files to a unified path: outroot/bin/xray/
# Move geo files to a unified path: outroot/bin
unify_geo_layout() {
local outroot="$1"
mkdir -p "$outroot/bin/xray"
local srcs=( \
"$outroot/bin/geosite.dat" \
"$outroot/bin/geoip.dat" \
"$outroot/bin/geoip-only-cn-private.dat" \
"$outroot/bin/Country.mmdb" \
"$outroot/bin/geoip.metadb" \
mkdir -p "$outroot/bin"
local names=( \
"geosite.dat" \
"geoip.dat" \
"geoip-only-cn-private.dat" \
"Country.mmdb" \
"geoip.metadb" \
)
for s in "${srcs[@]}"; do
if [[ -f "$s" ]]; then
mv -f "$s" "$outroot/bin/xray/$(basename "$s")"
for n in "${names[@]}"; do
# If file exists under bin/xray/, move it up to bin/
if [[ -f "$outroot/bin/xray/$n" ]]; then
mv -f "$outroot/bin/xray/$n" "$outroot/bin/$n"
fi
# If file already in bin/, leave it as-is
if [[ -f "$outroot/bin/$n" ]]; then
:
fi
done
}
# Download geo/rule assets; then unify to bin/xray/
# Download geo/rule assets; then unify to bin/
download_geo_assets() {
local outroot="$1"
local bin_dir="$outroot/bin"
@ -442,7 +447,7 @@ download_geo_assets() {
"https://raw.githubusercontent.com/2dust/sing-box-rules/rule-set-geosite/$f" || true
done
# Unify to bin/xray/
# Unify to bin/
unify_geo_layout "$outroot"
}
@ -480,7 +485,7 @@ download_v2rayn_bundle() {
rm -rf "$nested_dir"
fi
# Unify to bin/xray/
# Unify to bin/
unify_geo_layout "$outroot"
echo "[+] Bundle extracted to $outroot"
@ -610,7 +615,7 @@ Source0: __PKGROOT__.tar.gz
# Runtime dependencies (Avalonia / X11 / Fonts / GL)
Requires: libX11, libXrandr, libXcursor, libXi, libXext, libxcb, libXrender, libXfixes, libXinerama, libxkbcommon
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL
Requires: fontconfig, freetype, cairo, pango, mesa-libEGL, mesa-libGL, xdg-utils
%description
v2rayN Linux for Red Hat Enterprise Linux
@ -629,25 +634,13 @@ https://github.com/2dust/v2rayN
install -dm0755 %{buildroot}/opt/v2rayN
cp -a * %{buildroot}/opt/v2rayN/
# Launcher (prefer native ELF first, then DLL fallback; also create Geo symlinks for the user)
# Launcher (prefer native ELF first, then DLL fallback)
install -dm0755 %{buildroot}%{_bindir}
cat > %{buildroot}%{_bindir}/v2rayn << 'EOF'
#!/usr/bin/bash
set -euo pipefail
DIR="/opt/v2rayN"
# --- Symlink GEO files into user's XDG dir (first-run convenience) ---
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
USR_GEO_DIR="$XDG_DATA_HOME/v2rayN/bin"
SYS_XRAY_DIR="$DIR/bin/xray"
mkdir -p "$USR_GEO_DIR"
for f in geosite.dat geoip.dat geoip-only-cn-private.dat Country.mmdb; do
if [[ -f "$SYS_XRAY_DIR/$f" && ! -e "$USR_GEO_DIR/$f" ]]; then
ln -s "$SYS_XRAY_DIR/$f" "$USR_GEO_DIR/$f" || true
fi
done
# --- end GEO ---
# Prefer native apphost
if [[ -x "$DIR/v2rayN" ]]; then exec "$DIR/v2rayN" "$@"; fi

View file

@ -1,37 +0,0 @@
app: v2rayN
binpatch: true
ingredients:
script:
- export FileName="v2rayN-${AppImageOutputArch}.zip"
- wget -nv -O $FileName "https://github.com/2dust/v2rayN-core-bin/raw/refs/heads/master/${FileName}"
- 7z x $FileName -aoa
- cp -rf v2rayN-${AppImageOutputArch}/* $OutputPath
script:
- mkdir -p usr/bin usr/lib
- cp -rf $OutputPath usr/lib/v2rayN
- echo "When this file exists, app will not store configs under this folder" > usr/lib/v2rayN/NotStoreConfigHere.txt
- ln -sf usr/lib/v2rayN/v2rayN usr/bin/v2rayN
- chmod a+x usr/lib/v2rayN/v2rayN
- find usr -type f -exec sh -c 'file "{}" | grep -qi "executable" && chmod +x "{}"' \;
- install -Dm644 usr/lib/v2rayN/v2rayN.png v2rayN.png
- install -Dm644 usr/lib/v2rayN/v2rayN.png usr/share/pixmaps/v2rayN.png
- cat > v2rayN.desktop <<EOF
- [Desktop Entry]
- Name=v2rayN
- Comment=A GUI client for Windows and Linux, support Xray core and sing-box-core and others
- Exec=v2rayN
- Icon=v2rayN
- Terminal=false
- Type=Application
- Categories=Network;
- EOF
- install -Dm644 v2rayN.desktop usr/share/applications/v2rayN.desktop
- cat > AppRun <<\EOF
- #!/bin/sh
- HERE="$(dirname "$(readlink -f "${0}")")"
- cd ${HERE}/usr/lib/v2rayN
- exec ${HERE}/usr/lib/v2rayN/v2rayN $@
- EOF
- chmod a+x AppRun

View file

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.14.3</Version>
<Version>7.15.3</Version>
</PropertyGroup>
<PropertyGroup>

View file

@ -5,22 +5,24 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.4" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.4" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.4" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.4" />
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.7" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.7" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.7" />
<PackageVersion Include="Avalonia.ReactiveUI" Version="11.3.7" />
<PackageVersion Include="CliWrap" Version="3.9.0" />
<PackageVersion Include="Downloader" Version="4.0.3" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
<PackageVersion Include="MaterialDesignThemes" Version="5.2.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.0" />
<PackageVersion Include="MessageBox.Avalonia" Version="3.2.0" />
<PackageVersion Include="QRCoder" Version="1.6.0" />
<PackageVersion Include="QRCoder" Version="1.7.0" />
<PackageVersion Include="ReactiveUI" Version="20.4.1" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.9" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.9" />
<PackageVersion Include="Splat.NLog" Version="15.5.3" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.7" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7" />
<PackageVersion Include="NLog" Version="6.0.5" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />

@ -1 +1 @@
Subproject commit ef73fa22c46cfc7d1ec192ffe8497f6e61b4f0db
Subproject commit ffb2850df0991495d0918e13cc5701737f26175a

View file

@ -84,4 +84,14 @@ public static class Extension
{
return source.Concat(new[] { string.Empty }).ToList();
}
public static bool IsGroupType(this EConfigType configType)
{
return configType is EConfigType.PolicyGroup or EConfigType.ProxyChain;
}
public static bool IsComplexType(this EConfigType configType)
{
return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain;
}
}

View file

@ -9,6 +9,31 @@ public class JsonUtils
{
private static readonly string _tag = "JsonUtils";
private static readonly JsonSerializerOptions _defaultDeserializeOptions = new()
{
PropertyNameCaseInsensitive = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
private static readonly JsonSerializerOptions _defaultSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonSerializerOptions _nullValueSerializeOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static readonly JsonDocumentOptions _defaultDocumentOptions = new()
{
CommentHandling = JsonCommentHandling.Skip
};
/// <summary>
/// DeepCopy
/// </summary>
@ -34,11 +59,7 @@ public class JsonUtils
{
return default;
}
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<T>(strJson, options);
return JsonSerializer.Deserialize<T>(strJson, _defaultDeserializeOptions);
}
catch
{
@ -59,7 +80,7 @@ public class JsonUtils
{
return null;
}
return JsonNode.Parse(strJson);
return JsonNode.Parse(strJson, nodeOptions: null, _defaultDocumentOptions);
}
catch
{
@ -84,12 +105,7 @@ public class JsonUtils
{
return result;
}
var options = new JsonSerializerOptions
{
WriteIndented = indented,
DefaultIgnoreCondition = nullValue ? JsonIgnoreCondition.Never : JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var options = nullValue ? _nullValueSerializeOptions : _defaultSerializeOptions;
result = JsonSerializer.Serialize(obj, options);
}
catch (Exception ex)

View file

@ -67,116 +67,4 @@ public static class ProcUtils
Logging.SaveLog(_tag, ex);
}
}
public static async Task ProcessKill(int pid)
{
try
{
await ProcessKill(Process.GetProcessById(pid), false);
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public static async Task ProcessKill(Process? proc, bool review)
{
if (proc is null)
{
return;
}
GetProcessKeyInfo(proc, review, out var procId, out var fileName, out var processName);
try
{
if (Utils.IsNonWindows())
{
proc?.Kill(true);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Kill();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Close();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
try
{
proc?.Dispose();
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
await Task.Delay(300);
await ProcessKillByKeyInfo(review, procId, fileName, processName);
}
private static void GetProcessKeyInfo(Process? proc, bool review, out int? procId, out string? fileName, out string? processName)
{
procId = null;
fileName = null;
processName = null;
if (!review)
{
return;
}
try
{
procId = proc?.Id;
fileName = proc?.MainModule?.FileName;
processName = proc?.ProcessName;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
private static async Task ProcessKillByKeyInfo(bool review, int? procId, string? fileName, string? processName)
{
if (review && procId != null && fileName != null)
{
try
{
var lstProc = Process.GetProcessesByName(processName);
foreach (var proc2 in lstProc)
{
if (proc2.Id == procId)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job, procId");
await ProcessKill(proc2, false);
}
if (proc2.MainModule != null && proc2.MainModule?.FileName == fileName)
{
Logging.SaveLog($"{_tag}, KillProcess not completing the job, fileName");
}
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
}
}

View file

@ -1,4 +1,5 @@
using QRCoder;
using QRCoder.Exceptions;
using SkiaSharp;
using ZXing.SkiaSharp;
@ -8,10 +9,45 @@ public class QRCodeUtils
{
public static byte[]? GenQRCode(string? url)
{
if (url.IsNullOrEmpty())
{
return null;
}
using QRCodeGenerator qrGenerator = new();
using var qrCodeData = qrGenerator.CreateQrCode(url ?? string.Empty, QRCodeGenerator.ECCLevel.Q);
using PngByteQRCode qrCode = new(qrCodeData);
return qrCode.GetGraphic(20);
DataTooLongException? lastDtle = null;
var levels = new[]
{
QRCodeGenerator.ECCLevel.H,
QRCodeGenerator.ECCLevel.Q,
QRCodeGenerator.ECCLevel.M,
QRCodeGenerator.ECCLevel.L
};
foreach (var level in levels)
{
try
{
using var qrCodeData = qrGenerator.CreateQrCode(url, level);
using PngByteQRCode qrCode = new(qrCodeData);
return qrCode.GetGraphic(20);
}
catch (DataTooLongException ex)
{
lastDtle = ex;
continue;
}
catch
{
throw;
}
}
if (lastDtle != null)
{
throw lastDtle;
}
return null;
}
public static string? ParseBarcode(string? fileName)

View file

@ -85,13 +85,19 @@ public class Utils
/// Base64 Encode
/// </summary>
/// <param name="plainText"></param>
/// <param name="removePadding"></param>
/// <returns></returns>
public static string Base64Encode(string plainText)
public static string Base64Encode(string plainText, bool removePadding = false)
{
try
{
var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
var base64 = Convert.ToBase64String(plainTextBytes);
if (removePadding)
{
base64 = base64.TrimEnd('=');
}
return base64;
}
catch (Exception ex)
{
@ -112,7 +118,7 @@ public class Utils
{
if (plainText.IsNullOrEmpty())
{
return "";
return string.Empty;
}
plainText = plainText.Trim()
@ -331,6 +337,32 @@ public class Utils
.ToList();
}
public static Dictionary<string, List<string>> ParseHostsToDictionary(string hostsContent)
{
var userHostsMap = hostsContent
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
// skip full-line comments
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
// strip inline comments (truncate at '#')
.Select(line =>
{
var index = line.IndexOf('#');
return index >= 0 ? line.Substring(0, index).Trim() : line;
})
// ensure line still contains valid parts
.Where(line => !string.IsNullOrWhiteSpace(line) && line.Contains(' '))
.Select(line => line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries))
.Where(parts => parts.Length >= 2)
.GroupBy(parts => parts[0])
.ToDictionary(
group => group.Key,
group => group.SelectMany(parts => parts.Skip(1)).ToList()
);
return userHostsMap;
}
#endregion
#region
@ -582,9 +614,9 @@ public class Utils
if (host.StartsWith("#"))
continue;
var hostItem = host.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
if (hostItem.Length != 2)
if (hostItem.Length < 2)
continue;
systemHosts.Add(hostItem.Last(), hostItem.First());
systemHosts.Add(hostItem[1], hostItem[0]);
}
}
}
@ -857,6 +889,55 @@ public class Utils
return false;
}
public static bool IsPackagedInstall()
{
try
{
if (IsWindows() || IsOSX())
{
return false;
}
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPIMAGE")))
{
return true;
}
var exePath = GetExePath();
var baseDir = string.IsNullOrEmpty(exePath) ? StartupPath() : Path.GetDirectoryName(exePath) ?? "";
var p = baseDir.Replace('\\', '/');
if (string.IsNullOrEmpty(p))
{
return false;
}
if (p.Contains("/.mount_", StringComparison.Ordinal))
{
return true;
}
if (p.StartsWith("/opt/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (p.StartsWith("/usr/lib/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (p.StartsWith("/usr/share/v2rayN", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
catch
{
}
return false;
}
private static async Task<string?> GetLinuxUserId()
{
var arg = new List<string>() { "-c", "id -u" };
@ -872,7 +953,7 @@ public class Utils
if (SetUnixFileMode(fileName))
{
Logging.SaveLog($"Successfully set the file execution permission, {fileName}");
return "";
return string.Empty;
}
if (fileName.Contains(' '))

View file

@ -7,11 +7,11 @@ namespace ServiceLib.Common;
* http://stackoverflow.com/questions/6266820/working-example-of-createjobobject-setinformationjobobject-pinvoke-in-net
*/
public sealed class Job : IDisposable
public sealed class WindowsJob : IDisposable
{
private IntPtr handle = IntPtr.Zero;
public Job()
public WindowsJob()
{
handle = CreateJobObject(IntPtr.Zero, null);
var extendedInfoPtr = IntPtr.Zero;
@ -94,7 +94,7 @@ namespace ServiceLib.Common;
}
}
~Job()
~WindowsJob()
{
Dispose(false);
}

View file

@ -12,5 +12,7 @@ public enum EConfigType
TUIC = 8,
WireGuard = 9,
HTTP = 10,
Anytls = 11
Anytls = 11,
PolicyGroup = 101,
ProxyChain = 102,
}

View file

@ -15,5 +15,6 @@ public enum ECoreType
brook = 27,
overtls = 28,
shadowquic = 29,
mieru = 30,
v2rayN = 99
}

View file

@ -1,10 +0,0 @@
namespace ServiceLib.Enums;
public enum EMsgCommand
{
ClearMsg,
SendMsgView,
SendSnackMsg,
RefreshProfiles,
AppExit
}

View file

@ -2,8 +2,9 @@ namespace ServiceLib.Enums;
public enum EMultipleLoad
{
LeastPing,
Fallback,
Random,
RoundRobin,
LeastPing,
LeastLoad
}

View file

@ -0,0 +1,8 @@
namespace ServiceLib.Enums;
public enum ERuleType
{
ALL = 0,
Routing = 1,
DNS = 2,
}

View file

@ -6,17 +6,14 @@ public enum EViewAction
ShowYesNo,
SaveFileDialog,
AddBatchRoutingRulesYesNo,
AdjustMainLvColWidth,
SetClipboardData,
AddServerViaClipboard,
ImportRulesFromClipboard,
ProfilesFocus,
ShareSub,
ShareServer,
ShowHideWindow,
ScanScreenTask,
ScanImageTask,
Shutdown,
BrowseServer,
ImportRulesFromFile,
InitSettingFont,
@ -26,22 +23,14 @@ public enum EViewAction
RoutingRuleDetailsWindow,
AddServerWindow,
AddServer2Window,
AddGroupServerWindow,
DNSSettingWindow,
RoutingSettingWindow,
OptionSettingWindow,
FullConfigTemplateWindow,
GlobalHotkeySettingWindow,
SubSettingWindow,
DispatcherSpeedTest,
DispatcherRefreshConnections,
DispatcherRefreshProxyGroups,
DispatcherProxiesDelayTest,
DispatcherStatistics,
DispatcherServerAvailability,
DispatcherReload,
DispatcherRefreshServersBiz,
DispatcherRefreshIcon,
DispatcherCheckUpdate,
DispatcherCheckUpdateFinished,
DispatcherShowMsg,
}

View file

@ -0,0 +1,32 @@
using System.Reactive;
namespace ServiceLib.Events;
public static class AppEvents
{
public static readonly EventChannel<Unit> ReloadRequested = new();
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
public static readonly EventChannel<string> SendSnackMsgRequested = new();
public static readonly EventChannel<string> SendMsgViewRequested = new();
public static readonly EventChannel<Unit> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = new();
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
public static readonly EventChannel<string> SetDefaultServerRequested = new();
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
public static readonly EventChannel<Unit> TestServerRequested = new();
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
}

View file

@ -0,0 +1,29 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace ServiceLib.Events;
public sealed class EventChannel<T>
{
private readonly ISubject<T> _subject = Subject.Synchronize(new Subject<T>());
public IObservable<T> AsObservable()
{
return _subject.AsObservable();
}
public void Publish(T value)
{
_subject.OnNext(value);
}
public void Publish()
{
if (typeof(T) != typeof(Unit))
{
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<Unit>.");
}
_subject.OnNext((T)(object)Unit.Default);
}
}

View file

@ -40,6 +40,7 @@ public class Global
public const string ProxySetLinuxShellFileName = NamespaceSample + "proxy_set_linux_sh";
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
public const string DefaultSecurity = "auto";
public const string DefaultNetwork = "tcp";
@ -49,6 +50,7 @@ public class Global
public const string DirectTag = "direct";
public const string BlockTag = "block";
public const string DnsTag = "dns-module";
public const string BalancerTagSuffix = "-round";
public const string StreamSecurity = "tls";
public const string StreamSecurityReality = "reality";
public const string Loopback = "127.0.0.1";
@ -82,8 +84,7 @@ public class Global
public const string SingboxDirectDNSTag = "direct_dns";
public const string SingboxRemoteDNSTag = "remote_dns";
public const string SingboxOutboundResolverTag = "outbound_resolver";
public const string SingboxFinalResolverTag = "final_resolver";
public const string SingboxLocalDNSTag = "local_local";
public const string SingboxHostsDNSTag = "hosts_dns";
public const string SingboxFakeDNSTag = "fake_dns";
@ -314,6 +315,8 @@ public class Global
EConfigType.HTTP,
];
public static readonly HashSet<EConfigType> SingboxOnlyConfigType = SingboxSupportConfigType.Except(XraySupportConfigType).ToHashSet();
public static readonly List<string> DomainStrategies =
[
AsIs,
@ -448,6 +451,14 @@ public class Global
"none"
];
public static readonly Dictionary<string, string> LogLevelColors = new()
{
{ "debug", "#6C757D" },
{ "info", "#2ECC71" },
{ "warning", "#FFA500" },
{ "error", "#E74C3C" },
};
public static readonly List<string> InboundTags =
[
"socks",
@ -560,6 +571,7 @@ public class Global
{ ECoreType.brook, "txthinking/brook" },
{ ECoreType.overtls, "ShadowsocksR-Live/overtls" },
{ ECoreType.shadowquic, "spongebob888/shadowquic" },
{ ECoreType.mieru, "enfein/mieru" },
{ ECoreType.v2rayN, "2dust/v2rayN" },
};
@ -596,6 +608,7 @@ public class Global
{ "cloudflare-dns.com", new List<string> { "104.16.249.249", "104.16.248.249", "2606:4700::6810:f8f9", "2606:4700::6810:f9f9" } },
{ "dns.cloudflare.com", new List<string> { "104.16.132.229", "104.16.133.229", "2606:4700::6810:84e5", "2606:4700::6810:85e5" } },
{ "dot.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
{ "doh.pub", new List<string> { "1.12.12.12", "120.53.53.53" } },
{ "dns.quad9.net", new List<string> { "9.9.9.9", "149.112.112.112", "2620:fe::fe", "2620:fe::9" } },
{ "dns.yandex.net", new List<string> { "77.88.8.8", "77.88.8.1", "2a02:6b8::feed:0ff", "2a02:6b8:0:1::feed:0ff" } },
{ "dns.sb", new List<string> { "185.222.222.222", "2a09::" } },

View file

@ -1,6 +1,7 @@
global using ServiceLib.Base;
global using ServiceLib.Common;
global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler;
global using ServiceLib.Helper;
global using ServiceLib.Manager;

View file

@ -113,6 +113,10 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
if (config.SimpleDNSItem.GlobalFakeIp is null)
{
config.SimpleDNSItem.GlobalFakeIp = true;
}
config.SpeedTestItem ??= new();
if (config.SpeedTestItem.SpeedTestTimeout < 10)
@ -353,6 +357,11 @@ public static class ConfigHandler
{
}
}
else if (profileItem.ConfigType.IsGroupType())
{
var profileGroupItem = await AppManager.Instance.GetProfileGroupItem(it.IndexId);
await AddGroupServerCommon(config, profileItem, profileGroupItem, true);
}
else
{
await AddServerCommon(config, profileItem, true);
@ -1070,6 +1079,37 @@ public static class ConfigHandler
return 0;
}
public static async Task<int> AddGroupServerCommon(Config config, ProfileItem profileItem, ProfileGroupItem profileGroupItem, bool toFile = true)
{
var maxSort = -1;
if (profileItem.IndexId.IsNullOrEmpty())
{
profileItem.IndexId = Utils.GetGuid(false);
maxSort = ProfileExManager.Instance.GetMaxSort();
}
var groupType = profileItem.ConfigType == EConfigType.ProxyChain ? EConfigType.ProxyChain.ToString() : profileGroupItem.MultipleLoad.ToString();
profileItem.Address = $"{profileItem.CoreType}-{groupType}";
if (maxSort > 0)
{
ProfileExManager.Instance.SetSort(profileItem.IndexId, maxSort + 1);
}
if (toFile)
{
await SQLiteHelper.Instance.ReplaceAsync(profileItem);
if (profileGroupItem != null)
{
profileGroupItem.IndexId = profileItem.IndexId;
await ProfileGroupItemManager.Instance.SaveItemAsync(profileGroupItem);
}
else
{
ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(profileItem.IndexId);
await ProfileGroupItemManager.Instance.SaveTo();
}
}
return 0;
}
/// <summary>
/// Compare two profile items to determine if they represent the same server
/// Used for deduplication and server matching
@ -1141,7 +1181,7 @@ public static class ConfigHandler
}
/// <summary>
/// Create a custom server that combines multiple servers for load balancing
/// Create a group server that combines multiple servers for load balancing
/// Generates a configuration file that references multiple servers
/// </summary>
/// <param name="config">Current configuration</param>
@ -1149,45 +1189,54 @@ public static class ConfigHandler
/// <param name="coreType">Core type to use (Xray or sing_box)</param>
/// <param name="multipleLoad">Load balancing algorithm</param>
/// <returns>Result object with success state and data</returns>
public static async Task<RetResult> AddCustomServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad)
public static async Task<RetResult> AddGroupServer4Multiple(Config config, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId)
{
var indexId = Utils.GetMd5(Global.CoreMultipleLoadConfigFileName);
var configPath = Utils.GetConfigPath(Global.CoreMultipleLoadConfigFileName);
var result = new RetResult();
var result = await CoreConfigHandler.GenerateClientMultipleLoadConfig(config, configPath, selecteds, coreType, multipleLoad);
if (result.Success != true)
{
return result;
}
var indexId = Utils.GetGuid(false);
var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList());
if (!File.Exists(configPath))
{
return result;
}
var profileItem = await AppManager.Instance.GetProfileItem(indexId) ?? new();
profileItem.IndexId = indexId;
var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId)).Remarks} ";
if (coreType == ECoreType.Xray)
{
profileItem.Remarks = multipleLoad switch
remark += multipleLoad switch
{
EMultipleLoad.Random => ResUI.menuSetDefaultMultipleServerXrayRandom,
EMultipleLoad.RoundRobin => ResUI.menuSetDefaultMultipleServerXrayRoundRobin,
EMultipleLoad.LeastPing => ResUI.menuSetDefaultMultipleServerXrayLeastPing,
EMultipleLoad.LeastLoad => ResUI.menuSetDefaultMultipleServerXrayLeastLoad,
_ => ResUI.menuSetDefaultMultipleServerXrayRoundRobin,
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback,
EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom,
EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad,
_ => ResUI.menuGenGroupMultipleServerXrayRoundRobin,
};
}
else if (coreType == ECoreType.sing_box)
{
profileItem.Remarks = ResUI.menuSetDefaultMultipleServerSingBoxLeastPing;
remark += multipleLoad switch
{
EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback,
_ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing,
};
}
profileItem.Address = Global.CoreMultipleLoadConfigFileName;
profileItem.ConfigType = EConfigType.Custom;
profileItem.CoreType = coreType;
await AddServerCommon(config, profileItem, true);
var profile = new ProfileItem
{
IndexId = indexId,
CoreType = coreType,
ConfigType = EConfigType.PolicyGroup,
Remarks = remark,
};
if (!subId.IsNullOrEmpty())
{
profile.Subid = subId;
}
var profileGroup = new ProfileGroupItem
{
ChildItems = childProfileIndexId,
MultipleLoad = multipleLoad,
IndexId = indexId,
};
var ret = await AddGroupServerCommon(config, profile, profileGroup, true);
result.Success = ret == 0;
result.Data = indexId;
return result;
}
@ -1205,16 +1254,25 @@ public static class ConfigHandler
ProfileItem? itemSocks = null;
if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun)
{
var tun2SocksAddress = node.Address;
if (node.ConfigType.IsGroupType())
{
var lstAddresses = (await ProfileGroupItemManager.GetAllChildDomainAddresses(node.IndexId)).ToList();
if (lstAddresses.Count > 0)
{
tun2SocksAddress = Utils.List2String(lstAddresses);
}
}
itemSocks = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
Sni = node.Address, //Tun2SocksAddress
SpiderX = tun2SocksAddress, // Tun2SocksAddress
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
};
}
else if ((node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0))
else if (node.ConfigType == EConfigType.Custom && node.PreSocksPort > 0)
{
var preCoreType = config.RunningCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray;
itemSocks = new ProfileItem()
@ -1426,16 +1484,7 @@ public static class ConfigHandler
if (profileItem is null)
{
profileItem = Hysteria2Fmt.ResolveFull2(strData, subRemarks);
}
if (profileItem is null)
{
profileItem = Hysteria2Fmt.ResolveFull(strData, subRemarks);
}
//Is naiveproxy configuration
if (profileItem is null)
{
profileItem = NaiveproxyFmt.ResolveFull(strData, subRemarks);
}
}
if (profileItem is null || profileItem.Address.IsNullOrEmpty())
{
return -1;
@ -2221,11 +2270,10 @@ public static class ConfigHandler
UseSystemHosts = false,
AddCommonHosts = true,
FakeIP = false,
GlobalFakeIp = true,
BlockBindingQuery = true,
DirectDNS = Global.DomainDirectDNSAddress.FirstOrDefault(),
RemoteDNS = Global.DomainRemoteDNSAddress.FirstOrDefault(),
SingboxOutboundsResolveDNS = Global.DomainDirectDNSAddress.FirstOrDefault(),
SingboxFinalResolveDNS = Global.DomainPureIPDNSAddress.FirstOrDefault()
};
}
@ -2321,10 +2369,22 @@ public static class ConfigHandler
config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[1];
config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[1];
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json"));
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json"));
var xrayDnsRussia = await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[1] + "v2ray.json");
var singboxDnsRussia = await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[1] + "sing_box.json");
var simpleDnsRussia = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[1] + "simple_dns.json");
config.SimpleDNSItem = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[1] + "simple_dns.json") ?? InitBuiltinSimpleDNS();
if (simpleDnsRussia == null)
{
xrayDnsRussia.Enabled = true;
singboxDnsRussia.Enabled = true;
config.SimpleDNSItem = InitBuiltinSimpleDNS();
}
else
{
config.SimpleDNSItem = simpleDnsRussia;
}
await SaveDNSItems(config, xrayDnsRussia);
await SaveDNSItems(config, singboxDnsRussia);
break;
case EPresetType.Iran:
@ -2332,10 +2392,22 @@ public static class ConfigHandler
config.ConstItem.SrsSourceUrl = Global.SingboxRulesetSources[2];
config.ConstItem.RouteRulesTemplateSourceUrl = Global.RoutingRulesSources[2];
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json"));
await SaveDNSItems(config, await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json"));
var xrayDnsIran = await GetExternalDNSItem(ECoreType.Xray, Global.DNSTemplateSources[2] + "v2ray.json");
var singboxDnsIran = await GetExternalDNSItem(ECoreType.sing_box, Global.DNSTemplateSources[2] + "sing_box.json");
var simpleDnsIran = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[2] + "simple_dns.json");
config.SimpleDNSItem = await GetExternalSimpleDNSItem(Global.DNSTemplateSources[2] + "simple_dns.json") ?? InitBuiltinSimpleDNS();
if (simpleDnsIran == null)
{
xrayDnsIran.Enabled = true;
singboxDnsIran.Enabled = true;
config.SimpleDNSItem = InitBuiltinSimpleDNS();
}
else
{
config.SimpleDNSItem = simpleDnsIran;
}
await SaveDNSItems(config, xrayDnsIran);
await SaveDNSItems(config, singboxDnsIran);
break;
}

View file

@ -132,24 +132,4 @@ public static class CoreConfigHandler
await File.WriteAllTextAsync(fileName, result.Data.ToString());
return result;
}
public static async Task<RetResult> GenerateClientMultipleLoadConfig(Config config, string fileName, List<ProfileItem> selecteds, ECoreType coreType, EMultipleLoad multipleLoad)
{
var result = new RetResult();
if (coreType == ECoreType.sing_box)
{
result = await new CoreConfigSingboxService(config).GenerateClientMultipleLoadConfig(selecteds);
}
else
{
result = await new CoreConfigV2rayService(config).GenerateClientMultipleLoadConfig(selecteds, multipleLoad);
}
if (result.Success != true)
{
return result;
}
await File.WriteAllTextAsync(fileName, result.Data.ToString());
return result;
}
}

View file

@ -155,61 +155,60 @@ public class BaseFmt
protected static int ResolveStdTransport(NameValueCollection query, ref ProfileItem item)
{
item.Flow = query["flow"] ?? "";
item.StreamSecurity = query["security"] ?? "";
item.Sni = query["sni"] ?? "";
item.Alpn = Utils.UrlDecode(query["alpn"] ?? "");
item.Fingerprint = Utils.UrlDecode(query["fp"] ?? "");
item.PublicKey = Utils.UrlDecode(query["pbk"] ?? "");
item.ShortId = Utils.UrlDecode(query["sid"] ?? "");
item.SpiderX = Utils.UrlDecode(query["spx"] ?? "");
item.Mldsa65Verify = Utils.UrlDecode(query["pqv"] ?? "");
item.AllowInsecure = (query["allowInsecure"] ?? "") == "1" ? "true" : "";
item.Flow = GetQueryValue(query, "flow");
item.StreamSecurity = GetQueryValue(query, "security");
item.Sni = GetQueryValue(query, "sni");
item.Alpn = GetQueryDecoded(query, "alpn");
item.Fingerprint = GetQueryDecoded(query, "fp");
item.PublicKey = GetQueryDecoded(query, "pbk");
item.ShortId = GetQueryDecoded(query, "sid");
item.SpiderX = GetQueryDecoded(query, "spx");
item.Mldsa65Verify = GetQueryDecoded(query, "pqv");
item.AllowInsecure = new[] { "allowInsecure", "allow_insecure", "insecure" }.Any(k => (query[k] ?? "") == "1") ? "true" : "";
item.Network = query["type"] ?? nameof(ETransport.tcp);
item.Network = GetQueryValue(query, "type", nameof(ETransport.tcp));
switch (item.Network)
{
case nameof(ETransport.tcp):
item.HeaderType = query["headerType"] ?? Global.None;
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.RequestHost = GetQueryDecoded(query, "host");
break;
case nameof(ETransport.kcp):
item.HeaderType = query["headerType"] ?? Global.None;
item.Path = Utils.UrlDecode(query["seed"] ?? "");
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.Path = GetQueryDecoded(query, "seed");
break;
case nameof(ETransport.ws):
case nameof(ETransport.httpupgrade):
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
break;
case nameof(ETransport.xhttp):
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.HeaderType = Utils.UrlDecode(query["mode"] ?? "");
item.Extra = Utils.UrlDecode(query["extra"] ?? "");
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
item.HeaderType = GetQueryDecoded(query, "mode");
item.Extra = GetQueryDecoded(query, "extra");
break;
case nameof(ETransport.http):
case nameof(ETransport.h2):
item.Network = nameof(ETransport.h2);
item.RequestHost = Utils.UrlDecode(query["host"] ?? "");
item.Path = Utils.UrlDecode(query["path"] ?? "/");
item.RequestHost = GetQueryDecoded(query, "host");
item.Path = GetQueryDecoded(query, "path", "/");
break;
case nameof(ETransport.quic):
item.HeaderType = query["headerType"] ?? Global.None;
item.RequestHost = query["quicSecurity"] ?? Global.None;
item.Path = Utils.UrlDecode(query["key"] ?? "");
item.HeaderType = GetQueryValue(query, "headerType", Global.None);
item.RequestHost = GetQueryValue(query, "quicSecurity", Global.None);
item.Path = GetQueryDecoded(query, "key");
break;
case nameof(ETransport.grpc):
item.RequestHost = Utils.UrlDecode(query["authority"] ?? "");
item.Path = Utils.UrlDecode(query["serviceName"] ?? "");
item.HeaderType = Utils.UrlDecode(query["mode"] ?? Global.GrpcGunMode);
item.RequestHost = GetQueryDecoded(query, "authority");
item.Path = GetQueryDecoded(query, "serviceName");
item.HeaderType = GetQueryDecoded(query, "mode", Global.GrpcGunMode);
break;
default:
@ -239,4 +238,14 @@ public class BaseFmt
var url = $"{Utils.UrlEncode(userInfo)}@{GetIpv6(address)}:{port}";
return $"{Global.ProtocolShares[eConfigType]}{url}{query}{remark}";
}
protected static string GetQueryValue(NameValueCollection query, string key, string defaultValue = "")
{
return query[key] ?? defaultValue;
}
protected static string GetQueryDecoded(NameValueCollection query, string key, string defaultValue = "")
{
return Utils.UrlDecode(GetQueryValue(query, key, defaultValue));
}
}

View file

@ -4,7 +4,7 @@ public class ClashFmt : BaseFmt
{
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
{
if (Contains(strData, "port", "socks-port", "proxies"))
if (Contains(strData, "rules", "-port", "proxies"))
{
var fileName = WriteAllText(strData, "yaml");

View file

@ -27,7 +27,7 @@ public class FmtHandler
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return "";
return string.Empty;
}
}

View file

@ -21,10 +21,10 @@ public class Hysteria2Fmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
item.Path = Utils.UrlDecode(query["obfs-password"] ?? "");
item.AllowInsecure = (query["insecure"] ?? "") == "1" ? "true" : "false";
item.Path = GetQueryDecoded(query, "obfs-password");
item.AllowInsecure = GetQueryValue(query, "insecure") == "1" ? "true" : "false";
item.Ports = Utils.UrlDecode(query["mport"] ?? "");
item.Ports = GetQueryDecoded(query, "mport");
return item;
}
@ -63,24 +63,6 @@ public class Hysteria2Fmt : BaseFmt
return ToUri(EConfigType.Hysteria2, item.Address, item.Port, item.Id, dicQuery, remark);
}
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
{
if (Contains(strData, "server", "up", "down", "listen", "<html>", "<body>"))
{
var fileName = WriteAllText(strData);
var profileItem = new ProfileItem
{
CoreType = ECoreType.hysteria,
Address = fileName,
Remarks = subRemarks ?? "hysteria_custom"
};
return profileItem;
}
return null;
}
public static ProfileItem? ResolveFull2(string strData, string? subRemarks)
{
if (Contains(strData, "server", "auth", "up", "down", "listen"))

View file

@ -1,22 +0,0 @@
namespace ServiceLib.Handler.Fmt;
public class NaiveproxyFmt : BaseFmt
{
public static ProfileItem? ResolveFull(string strData, string? subRemarks)
{
if (Contains(strData, "listen", "proxy", "<html>", "<body>"))
{
var fileName = WriteAllText(strData);
var profileItem = new ProfileItem
{
CoreType = ECoreType.naiveproxy,
Address = fileName,
Remarks = subRemarks ?? "naiveproxy_custom"
};
return profileItem;
}
return null;
}
}

View file

@ -42,7 +42,7 @@ public class ShadowsocksFmt : BaseFmt
// item.port);
//url = Utile.Base64Encode(url);
//new Sip002
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
return ToUri(EConfigType.Shadowsocks, item.Address, item.Port, pw, null, remark);
}

View file

@ -33,7 +33,7 @@ public class SocksFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks);
}
//new
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}");
var pw = Utils.Base64Encode($"{item.Security}:{item.Id}", true);
return ToUri(EConfigType.SOCKS, item.Address, item.Port, pw, null, remark);
}

View file

@ -30,7 +30,7 @@ public class TuicFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
ResolveStdTransport(query, ref item);
item.HeaderType = query["congestion_control"] ?? "";
item.HeaderType = GetQueryValue(query, "congestion_control");
return item;
}

View file

@ -24,8 +24,8 @@ public class VLESSFmt : BaseFmt
item.Id = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query);
item.Security = query["encryption"] ?? Global.None;
item.StreamSecurity = query["security"] ?? "";
item.Security = GetQueryValue(query, "encryption", Global.None);
item.StreamSecurity = GetQueryValue(query, "security");
_ = ResolveStdTransport(query, ref item);
return item;

View file

@ -24,10 +24,10 @@ public class WireguardFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query);
item.PublicKey = Utils.UrlDecode(query["publickey"] ?? "");
item.Path = Utils.UrlDecode(query["reserved"] ?? "");
item.RequestHost = Utils.UrlDecode(query["address"] ?? "");
item.ShortId = Utils.UrlDecode(query["mtu"] ?? "");
item.PublicKey = GetQueryDecoded(query, "publickey");
item.Path = GetQueryDecoded(query, "reserved");
item.RequestHost = GetQueryDecoded(query, "address");
item.ShortId = GetQueryDecoded(query, "mtu");
return item;
}

View file

@ -2,17 +2,18 @@ namespace ServiceLib.Handler;
public static class SubscriptionHandler
{
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Action<bool, string> updateFunc)
public static async Task UpdateProcess(Config config, string subId, bool blProxy, Func<bool, string, Task> updateFunc)
{
updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
await updateFunc?.Invoke(false, ResUI.MsgUpdateSubscriptionStart);
var subItem = await AppManager.Instance.SubItems();
if (subItem is not { Count: > 0 })
{
updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription);
await updateFunc?.Invoke(false, ResUI.MsgNoValidSubscription);
return;
}
var successCount = 0;
foreach (var item in subItem)
{
try
@ -25,32 +26,35 @@ public static class SubscriptionHandler
var hashCode = $"{item.Remarks}->";
if (item.Enabled == false)
{
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}");
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSkipSubscriptionUpdate}");
continue;
}
// Create download handler
var downloadHandle = CreateDownloadHandler(hashCode, updateFunc);
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartGettingSubscriptions}");
// Get all subscription content (main subscription + additional subscriptions)
var result = await DownloadAllSubscriptions(config, item, blProxy, downloadHandle);
// Process download result
await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc);
if (await ProcessDownloadResult(config, item.Id, result, hashCode, updateFunc))
{
successCount++;
}
updateFunc?.Invoke(false, "-------------------------------------------------------");
await updateFunc?.Invoke(false, "-------------------------------------------------------");
}
catch (Exception ex)
{
var hashCode = $"{item.Remarks}->";
Logging.SaveLog("UpdateSubscription", ex);
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}");
updateFunc?.Invoke(false, "-------------------------------------------------------");
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgFailedImportSubscription}: {ex.Message}");
await updateFunc?.Invoke(false, "-------------------------------------------------------");
}
}
updateFunc?.Invoke(true, $"{ResUI.MsgUpdateSubscriptionEnd}");
await updateFunc?.Invoke(successCount > 0, $"{ResUI.MsgUpdateSubscriptionEnd}");
}
private static bool IsValidSubscription(SubItem item, string subId)
@ -76,7 +80,7 @@ public static class SubscriptionHandler
return true;
}
private static DownloadService CreateDownloadHandler(string hashCode, Action<bool, string> updateFunc)
private static DownloadService CreateDownloadHandler(string hashCode, Func<bool, string, Task> updateFunc)
{
var downloadHandle = new DownloadService();
downloadHandle.Error += (sender2, args) =>
@ -181,22 +185,24 @@ public static class SubscriptionHandler
return result;
}
private static async Task ProcessDownloadResult(Config config, string id, string result, string hashCode, Action<bool, string> updateFunc)
private static async Task<bool> ProcessDownloadResult(Config config, string id, string result, string hashCode, Func<bool, string, Task> updateFunc)
{
if (result.IsNullOrEmpty())
{
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}");
return;
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgSubscriptionDecodingFailed}");
return false;
}
updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}");
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgGetSubscriptionSuccessfully}");
// If result is too short, display content directly
if (result.Length < 99)
{
updateFunc?.Invoke(false, $"{hashCode}{result}");
await updateFunc?.Invoke(false, $"{hashCode}{result}");
}
await updateFunc?.Invoke(false, $"{hashCode}{ResUI.MsgStartParsingSubscription}");
// Add servers to configuration
var ret = await ConfigHandler.AddBatchServers(config, result, id, true);
if (ret <= 0)
@ -206,9 +212,10 @@ public static class SubscriptionHandler
}
// Update completion message
updateFunc?.Invoke(false,
ret > 0
await updateFunc?.Invoke(false, ret > 0
? $"{hashCode}{ResUI.MsgUpdateSubscriptionEnd}"
: $"{hashCode}{ResUI.MsgFailedImportSubscription}");
return ret > 0;
}
}

View file

@ -26,7 +26,7 @@ public sealed class SQLiteHelper
public async Task<int> InsertAllAsync(IEnumerable models)
{
return await _dbAsync.InsertAllAsync(models);
return await _dbAsync.InsertAllAsync(models, runInTransaction: true).ConfigureAwait(false);
}
public async Task<int> InsertAsync(object model)
@ -46,7 +46,7 @@ public sealed class SQLiteHelper
public async Task<int> UpdateAllAsync(IEnumerable models)
{
return await _dbAsync.UpdateAllAsync(models);
return await _dbAsync.UpdateAllAsync(models, runInTransaction: true).ConfigureAwait(false);
}
public async Task<int> DeleteAsync(object model)

View file

@ -0,0 +1,282 @@
namespace ServiceLib.Manager;
/// <summary>
/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.).
/// </summary>
public class ActionPrecheckManager(Config config)
{
private static readonly Lazy<ActionPrecheckManager> _instance = new(() => new ActionPrecheckManager(AppManager.Instance.Config));
public static ActionPrecheckManager Instance => _instance.Value;
private readonly Config _config = config;
public async Task<List<string>> Check(string? indexId)
{
if (indexId.IsNullOrEmpty())
{
return [ResUI.PleaseSelectServer];
}
var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null)
{
return [ResUI.PleaseSelectServer];
}
return await Check(item);
}
public async Task<List<string>> Check(ProfileItem? item)
{
if (item is null)
{
return [ResUI.PleaseSelectServer];
}
var errors = new List<string>();
errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item));
errors.AddRange(await ValidateRelatedNodesExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateCurrentNodeAndCoreSupport(ProfileItem item)
{
if (item.ConfigType == EConfigType.Custom)
{
return [];
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
return await ValidateNodeAndCoreSupport(item, coreType);
}
private async Task<List<string>> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null)
{
var errors = new List<string>();
coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType);
if (item.ConfigType is EConfigType.Custom)
{
errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString()));
return errors;
}
if (!item.IsComplex())
{
if (item.Address.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "Address"));
return errors;
}
if (item.Port is <= 0 or >= 65536)
{
errors.Add(string.Format(ResUI.InvalidProperty, "Port"));
return errors;
}
switch (item.ConfigType)
{
case EConfigType.VMess:
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
break;
case EConfigType.VLESS:
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id) && item.Id.Length > 30)
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
if (!Global.Flows.Contains(item.Flow))
errors.Add(string.Format(ResUI.InvalidProperty, "Flow"));
break;
case EConfigType.Shadowsocks:
if (item.Id.IsNullOrEmpty())
errors.Add(string.Format(ResUI.InvalidProperty, "Id"));
if (string.IsNullOrEmpty(item.Security) || !Global.SsSecuritiesInSingbox.Contains(item.Security))
errors.Add(string.Format(ResUI.InvalidProperty, "Security"));
break;
}
if (item.ConfigType is EConfigType.VLESS or EConfigType.Trojan
&& item.StreamSecurity == Global.StreamSecurityReality
&& item.PublicKey.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey"));
}
if (errors.Count > 0)
{
return errors;
}
}
if (item.ConfigType.IsGroupType())
{
ProfileGroupItemManager.Instance.TryGet(item.IndexId, out var group);
if (group is null || group.ChildItems.IsNullOrEmpty())
{
errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks));
return errors;
}
var hasCycle = ProfileGroupItemManager.HasCycle(item.IndexId);
if (hasCycle)
{
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));
return errors;
}
foreach (var child in Utils.String2List(group.ChildItems))
{
var childErrors = new List<string>();
if (child.IsNullOrEmpty())
{
continue;
}
var childItem = await AppManager.Instance.GetProfileItem(child);
if (childItem is null)
{
childErrors.Add(string.Format(ResUI.NodeTagNotExist, child));
continue;
}
if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain)
{
childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks));
continue;
}
childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType));
errors.AddRange(childErrors);
}
return errors;
}
var net = item.GetNetwork() ?? item.Network;
if (coreType == ECoreType.sing_box)
{
// sing-box does not support xhttp / kcp
// sing-box does not support transports like ws/http/httpupgrade/etc. when the node is not vmess/trojan/vless
if (net is nameof(ETransport.kcp) or nameof(ETransport.xhttp))
{
errors.Add(string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net));
return errors;
}
if (item.ConfigType is not (EConfigType.VMess or EConfigType.VLESS or EConfigType.Trojan))
{
if (net is nameof(ETransport.ws) or nameof(ETransport.http) or nameof(ETransport.h2) or nameof(ETransport.quic) or nameof(ETransport.httpupgrade))
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), item.ConfigType.ToString(), net));
return errors;
}
}
}
else if (coreType is ECoreType.Xray)
{
// Xray core does not support these protocols
if (!Global.XraySupportConfigType.Contains(item.ConfigType)
&& !item.IsComplex())
{
errors.Add(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType.ToString()));
return errors;
}
}
return errors;
}
private async Task<List<string>> ValidateRelatedNodesExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item));
errors.AddRange(await ValidateRoutingNodeExistAndValid(item));
return errors;
}
private async Task<List<string>> ValidateProxyChainedNodeExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
if (item is null)
{
return errors;
}
// prev node and next node
var subItem = await AppManager.Instance.GetSubItem(item.Subid);
if (subItem is null)
{
return errors;
}
var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile);
var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile);
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors);
await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors);
return errors;
}
private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List<string> errors)
{
if (node is not null)
{
var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType);
errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + s));
}
else if (tag.IsNotEmpty())
{
errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag));
}
}
private async Task<List<string>> ValidateRoutingNodeExistAndValid(ProfileItem? item)
{
var errors = new List<string>();
if (item is null)
{
return errors;
}
var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType);
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null)
{
return errors;
}
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var ruleItem in rules ?? [])
{
if (!ruleItem.Enabled)
{
continue;
}
var outboundTag = ruleItem.OutboundTag;
if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag))
{
continue;
}
var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (tagItem is null)
{
errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag));
continue;
}
var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType);
errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + s));
}
return errors;
}
}

View file

@ -8,7 +8,6 @@ public sealed class AppManager
private Config _config;
private int? _statePort;
private int? _statePort2;
private Job? _processJob;
public static AppManager Instance => _instance.Value;
public Config Config => _config;
@ -34,7 +33,7 @@ public sealed class AppManager
#endregion Property
#region Init
#region App
public bool InitApp()
{
@ -65,6 +64,7 @@ public sealed class AppManager
SQLiteHelper.Instance.CreateTable<ProfileExItem>();
SQLiteHelper.Instance.CreateTable<DNSItem>();
SQLiteHelper.Instance.CreateTable<FullConfigTemplateItem>();
SQLiteHelper.Instance.CreateTable<ProfileGroupItem>();
return true;
}
@ -87,7 +87,46 @@ public sealed class AppManager
return true;
}
#endregion Init
public async Task AppExitAsync(bool needShutdown)
{
try
{
Logging.SaveLog("AppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
AppEvents.AppExitRequested.Publish();
await Task.Delay(50); //Wait for AppExitRequested to be processed
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close();
Logging.SaveLog("AppExitAsync End");
}
catch { }
finally
{
if (needShutdown)
{
Shutdown(false);
}
}
}
public void Shutdown(bool byUser)
{
AppEvents.ShutdownRequested.Publish(byUser);
}
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await AppManager.Instance.AppExitAsync(true);
}
#endregion App
#region Config
@ -97,21 +136,6 @@ public sealed class AppManager
return localPort + (int)protocol;
}
public void AddProcess(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch
{
}
}
}
#endregion Config
#region SqliteHelper
@ -184,6 +208,15 @@ public sealed class AppManager
return await SQLiteHelper.Instance.TableAsync<ProfileItem>().FirstOrDefaultAsync(it => it.Remarks == remarks);
}
public async Task<ProfileGroupItem?> GetProfileGroupItem(string indexId)
{
if (indexId.IsNullOrEmpty())
{
return null;
}
return await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().FirstOrDefaultAsync(it => it.IndexId == indexId);
}
public async Task<List<RoutingItem>?> RoutingItems()
{
return await SQLiteHelper.Instance.TableAsync<RoutingItem>().OrderBy(t => t.Sort).ToListAsync();

View file

@ -35,7 +35,7 @@ public sealed class ClashApiManager
return null;
}
public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Action<ClashProxyModel?, string> updateFunc)
public void ClashProxiesDelayTest(bool blAll, List<ClashProxyModel> lstProxy, Func<ClashProxyModel?, string, Task> updateFunc)
{
Task.Run(async () =>
{
@ -79,12 +79,12 @@ public sealed class ClashApiManager
tasks.Add(Task.Run(async () =>
{
var result = await HttpClientHelper.Instance.TryGetAsync(url);
updateFunc?.Invoke(it, result);
await updateFunc?.Invoke(it, result);
}));
}
await Task.WhenAll(tasks);
await Task.Delay(1000);
updateFunc?.Invoke(null, "");
await updateFunc?.Invoke(null, "");
});
}

View file

@ -1,4 +1,3 @@
using System.Diagnostics;
using System.Text;
using CliWrap;
using CliWrap.Buffered;
@ -10,11 +9,11 @@ public class CoreAdminManager
private static readonly Lazy<CoreAdminManager> _instance = new(() => new());
public static CoreAdminManager Instance => _instance.Value;
private Config _config;
private Action<bool, string>? _updateFunc;
private Func<bool, string, Task>? _updateFunc;
private int _linuxSudoPid = -1;
private const string _tag = "CoreAdminHandler";
public async Task Init(Config config, Action<bool, string> updateFunc)
public async Task Init(Config config, Func<bool, string, Task> updateFunc)
{
if (_config != null)
{
@ -26,12 +25,12 @@ public class CoreAdminManager
await Task.CompletedTask;
}
private void UpdateFunc(bool notify, string msg)
private async Task UpdateFunc(bool notify, string msg)
{
_updateFunc?.Invoke(notify, msg);
await _updateFunc?.Invoke(notify, msg);
}
public async Task<Process?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
public async Task<ProcessService?> RunProcessAsLinuxSudo(string fileName, CoreInfo coreInfo, string configPath)
{
StringBuilder sb = new();
sb.AppendLine("#!/bin/bash");
@ -39,50 +38,25 @@ public class CoreAdminManager
sb.AppendLine($"sudo -S {cmdLine}");
var shFilePath = await FileManager.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
Process proc = new()
{
StartInfo = new()
{
FileName = shFilePath,
Arguments = "",
WorkingDirectory = Utils.GetBinConfigPath(),
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8,
}
};
var procService = new ProcessService(
fileName: shFilePath,
arguments: "",
workingDirectory: Utils.GetBinConfigPath(),
displayLog: true,
redirectInput: true,
environmentVars: null,
updateFunc: _updateFunc
);
void dataHandler(object sender, DataReceivedEventArgs e)
{
if (e.Data.IsNotEmpty())
{
UpdateFunc(false, e.Data + Environment.NewLine);
}
}
await procService.StartAsync(AppManager.Instance.LinuxSudoPwd);
proc.OutputDataReceived += dataHandler;
proc.ErrorDataReceived += dataHandler;
proc.Start();
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
await Task.Delay(10);
await proc.StandardInput.WriteLineAsync(AppManager.Instance.LinuxSudoPwd);
await Task.Delay(100);
if (proc is null or { HasExited: true })
if (procService is null or { HasExited: true })
{
throw new Exception(ResUI.FailedToRunCore);
}
_linuxSudoPid = procService.Id;
_linuxSudoPid = proc.Id;
return proc;
return procService;
}
public async Task KillProcessAsLinuxSudo()
@ -106,7 +80,7 @@ public class CoreAdminManager
.WithStandardInputPipe(PipeSource.FromString(AppManager.Instance.LinuxSudoPwd))
.ExecuteBufferedAsync();
UpdateFunc(false, result.StandardOutput.ToString());
await UpdateFunc(false, result.StandardOutput.ToString());
}
catch (Exception ex)
{

View file

@ -80,6 +80,10 @@ public sealed class CoreInfoManager
Url = GetCoreUrl(ECoreType.v2fly),
Match = "V2Ray",
VersionArg = "-version",
Environment = new Dictionary<string, string?>()
{
{ Global.V2RayLocalAsset, Utils.GetBinPath("") },
},
},
new CoreInfo
@ -90,6 +94,10 @@ public sealed class CoreInfoManager
Url = GetCoreUrl(ECoreType.v2fly_v5),
Match = "V2Ray",
VersionArg = "version",
Environment = new Dictionary<string, string?>()
{
{ Global.V2RayLocalAsset, Utils.GetBinPath("") },
},
},
new CoreInfo
@ -107,20 +115,25 @@ public sealed class CoreInfoManager
DownloadUrlOSXArm64 = urlXray + "/download/{0}/Xray-macos-arm64-v8a.zip",
Match = "Xray",
VersionArg = "-version",
Environment = new Dictionary<string, string?>()
{
{ Global.XrayLocalAsset, Utils.GetBinPath("") },
{ Global.XrayLocalCert, Utils.GetBinPath("") },
},
},
new CoreInfo
{
CoreType = ECoreType.mihomo,
CoreExes = ["mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"],
CoreExes = ["mihomo-windows-amd64-v1", "mihomo-windows-amd64-compatible", "mihomo-windows-amd64", "mihomo-linux-amd64", "clash", "mihomo"],
Arguments = "-f {0}" + PortableMode(),
Url = GetCoreUrl(ECoreType.mihomo),
ReleaseApiUrl = urlMihomo.Replace(Global.GithubUrl, Global.GithubApiUrl),
DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-compatible-{0}.zip",
DownloadUrlWin64 = urlMihomo + "/download/{0}/mihomo-windows-amd64-v1-{0}.zip",
DownloadUrlWinArm64 = urlMihomo + "/download/{0}/mihomo-windows-arm64-{0}.zip",
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-compatible-{0}.gz",
DownloadUrlLinux64 = urlMihomo + "/download/{0}/mihomo-linux-amd64-v1-{0}.gz",
DownloadUrlLinuxArm64 = urlMihomo + "/download/{0}/mihomo-linux-arm64-{0}.gz",
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-compatible-{0}.gz",
DownloadUrlOSX64 = urlMihomo + "/download/{0}/mihomo-darwin-amd64-v1-{0}.gz",
DownloadUrlOSXArm64 = urlMihomo + "/download/{0}/mihomo-darwin-arm64-{0}.gz",
Match = "Mihomo",
VersionArg = "-v",
@ -205,12 +218,24 @@ public sealed class CoreInfoManager
new CoreInfo
{
CoreType = ECoreType.shadowquic,
CoreExes = [ "shadowquic", "shadowquic"],
CoreExes = [ "shadowquic" ],
Arguments = "-c {0}",
Url = GetCoreUrl(ECoreType.shadowquic),
AbsolutePath = false,
}
},
new CoreInfo
{
CoreType = ECoreType.mieru,
CoreExes = [ "mieru" ],
Arguments = "run",
Url = GetCoreUrl(ECoreType.mieru),
AbsolutePath = false,
Environment = new Dictionary<string, string?>()
{
{ "MIERU_CONFIG_JSON_FILE", "{0}" },
},
},
];
}

View file

@ -1,6 +1,3 @@
using System.Diagnostics;
using System.Text;
namespace ServiceLib.Manager;
/// <summary>
@ -11,21 +8,18 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value;
private Config _config;
private Process? _process;
private Process? _processPre;
private WindowsJob? _processJob;
private ProcessService? _processService;
private ProcessService? _processPreService;
private bool _linuxSudo = false;
private Action<bool, string>? _updateFunc;
private Func<bool, string, Task>? _updateFunc;
private const string _tag = "CoreHandler";
public async Task Init(Config config, Action<bool, string> updateFunc)
public async Task Init(Config config, Func<bool, string, Task> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
Environment.SetEnvironmentVariable(Global.V2RayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(Global.XrayLocalAsset, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable(Global.XrayLocalCert, Utils.GetBinPath(""), EnvironmentVariableTarget.Process);
//Copy the bin folder to the storage location (for init)
if (Environment.GetEnvironmentVariable(Global.LocalAppData) == "1")
{
@ -67,7 +61,7 @@ public class CoreManager
{
if (node == null)
{
UpdateFunc(false, ResUI.CheckServerSettings);
await UpdateFunc(false, ResUI.CheckServerSettings);
return;
}
@ -75,13 +69,13 @@ public class CoreManager
var result = await CoreConfigHandler.GenerateClientConfig(node, fileName);
if (result.Success != true)
{
UpdateFunc(true, result.Msg);
await UpdateFunc(true, result.Msg);
return;
}
UpdateFunc(false, $"{node.GetSummary()}");
UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await UpdateFunc(false, $"{node.GetSummary()}");
await UpdateFunc(false, $"{Utils.GetRuntimeInfo()}");
await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await CoreStop();
await Task.Delay(100);
@ -93,43 +87,37 @@ public class CoreManager
await CoreStart(node);
await CoreStartPreService(node);
if (_process != null)
if (_processService != null)
{
UpdateFunc(true, $"{node.GetSummary()}");
await UpdateFunc(true, $"{node.GetSummary()}");
}
}
public async Task<int> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
public async Task<ProcessService?> LoadCoreConfigSpeedtest(List<ServerTestItem> selecteds)
{
var coreType = selecteds.Exists(t => t.ConfigType is EConfigType.Hysteria2 or EConfigType.TUIC or EConfigType.Anytls) ? ECoreType.sing_box : ECoreType.Xray;
var coreType = selecteds.Any(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)) ? ECoreType.sing_box : ECoreType.Xray;
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
var configPath = Utils.GetBinConfigPath(fileName);
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, configPath, selecteds, coreType);
UpdateFunc(false, result.Msg);
await UpdateFunc(false, result.Msg);
if (result.Success != true)
{
return -1;
return null;
}
UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
UpdateFunc(false, configPath);
await UpdateFunc(false, string.Format(ResUI.StartService, DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")));
await UpdateFunc(false, configPath);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
return await RunProcess(coreInfo, fileName, true, false);
}
public async Task<int> LoadCoreConfigSpeedtest(ServerTestItem testItem)
public async Task<ProcessService?> LoadCoreConfigSpeedtest(ServerTestItem testItem)
{
var node = await AppManager.Instance.GetProfileItem(testItem.IndexId);
if (node is null)
{
return -1;
return null;
}
var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false));
@ -137,18 +125,12 @@ public class CoreManager
var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, node, testItem, configPath);
if (result.Success != true)
{
return -1;
return null;
}
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var proc = await RunProcess(coreInfo, fileName, true, false);
if (proc is null)
{
return -1;
}
return proc.Id;
return await RunProcess(coreInfo, fileName, true, false);
}
public async Task CoreStop()
@ -161,16 +143,18 @@ public class CoreManager
_linuxSudo = false;
}
if (_process != null)
if (_processService != null)
{
await ProcUtils.ProcessKill(_process, Utils.IsWindows());
_process = null;
await _processService.StopAsync();
_processService.Dispose();
_processService = null;
}
if (_processPre != null)
if (_processPreService != null)
{
await ProcUtils.ProcessKill(_processPre, Utils.IsWindows());
_processPre = null;
await _processPreService.StopAsync();
_processPreService.Dispose();
_processPreService = null;
}
}
catch (Exception ex)
@ -192,12 +176,12 @@ public class CoreManager
{
return;
}
_process = proc;
_processService = proc;
}
private async Task CoreStartPreService(ProfileItem node)
{
if (_process != null && !_process.HasExited)
if (_processService != null && !_processService.HasExited)
{
var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType);
var itemSocks = await ConfigHandler.GetPreSocksItem(_config, node, coreType);
@ -214,27 +198,27 @@ public class CoreManager
{
return;
}
_processPre = proc;
_processPreService = proc;
}
}
}
}
private void UpdateFunc(bool notify, string msg)
private async Task UpdateFunc(bool notify, string msg)
{
_updateFunc?.Invoke(notify, msg);
await _updateFunc?.Invoke(notify, msg);
}
#endregion Private
#region Process
private async Task<Process?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo)
{
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
if (fileName.IsNullOrEmpty())
{
UpdateFunc(false, msg);
await UpdateFunc(false, msg);
return null;
}
@ -255,56 +239,53 @@ public class CoreManager
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
UpdateFunc(mayNeedSudo, ex.Message);
await UpdateFunc(mayNeedSudo, ex.Message);
return null;
}
}
private async Task<Process?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog)
private async Task<ProcessService?> RunProcessNormal(string fileName, CoreInfo? coreInfo, string configPath, bool displayLog)
{
Process proc = new()
var environmentVars = new Dictionary<string, string>();
foreach (var kv in coreInfo.Environment)
{
StartInfo = new()
{
FileName = fileName,
Arguments = string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
WorkingDirectory = Utils.GetBinConfigPath(),
UseShellExecute = false,
RedirectStandardOutput = displayLog,
RedirectStandardError = displayLog,
CreateNoWindow = true,
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
}
};
if (displayLog)
{
void dataHandler(object sender, DataReceivedEventArgs e)
{
if (e.Data.IsNotEmpty())
{
UpdateFunc(false, e.Data + Environment.NewLine);
}
}
proc.OutputDataReceived += dataHandler;
proc.ErrorDataReceived += dataHandler;
environmentVars[kv.Key] = string.Format(kv.Value, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath);
}
proc.Start();
if (displayLog)
{
proc.BeginOutputReadLine();
proc.BeginErrorReadLine();
}
var procService = new ProcessService(
fileName: fileName,
arguments: string.Format(coreInfo.Arguments, coreInfo.AbsolutePath ? Utils.GetBinConfigPath(configPath).AppendQuotes() : configPath),
workingDirectory: Utils.GetBinConfigPath(),
displayLog: displayLog,
redirectInput: false,
environmentVars: environmentVars,
updateFunc: _updateFunc
);
await procService.StartAsync();
await Task.Delay(100);
AppManager.Instance.AddProcess(proc.Handle);
if (proc is null or { HasExited: true })
if (procService is null or { HasExited: true })
{
throw new Exception(ResUI.FailedToRunCore);
}
return proc;
AddProcessJob(procService.Handle);
return procService;
}
private void AddProcessJob(nint processHandle)
{
if (Utils.IsWindows())
{
_processJob ??= new();
try
{
_processJob?.AddProcess(processHandle);
}
catch { }
}
}
#endregion Process

View file

@ -1,5 +1,3 @@
using ReactiveUI;
namespace ServiceLib.Manager;
public class NoticeManager
@ -13,7 +11,7 @@ public class NoticeManager
{
return;
}
MessageBus.Current.SendMessage(content, EMsgCommand.SendSnackMsg.ToString());
AppEvents.SendSnackMsgRequested.Publish(content);
}
public void SendMessage(string? content)
@ -22,7 +20,7 @@ public class NoticeManager
{
return;
}
MessageBus.Current.SendMessage(content, EMsgCommand.SendMsgView.ToString());
AppEvents.SendMsgViewRequested.Publish(content);
}
public void SendMessageEx(string? content)

View file

@ -0,0 +1,286 @@
using System.Collections.Concurrent;
namespace ServiceLib.Manager;
public class ProfileGroupItemManager
{
private static readonly Lazy<ProfileGroupItemManager> _instance = new(() => new());
private ConcurrentDictionary<string, ProfileGroupItem> _items = new();
public static ProfileGroupItemManager Instance => _instance.Value;
private static readonly string _tag = "ProfileGroupItemManager";
private ProfileGroupItemManager()
{
}
public async Task Init()
{
await InitData();
}
// Read-only getters: do not create or mark dirty
public bool TryGet(string indexId, out ProfileGroupItem? item)
{
item = null;
if (string.IsNullOrWhiteSpace(indexId))
{
return false;
}
return _items.TryGetValue(indexId, out item);
}
public ProfileGroupItem? GetOrDefault(string indexId)
{
return string.IsNullOrWhiteSpace(indexId) ? null : (_items.TryGetValue(indexId, out var v) ? v : null);
}
private async Task InitData()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem where IndexId not in ( select indexId from ProfileItem )");
var list = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
_items = new ConcurrentDictionary<string, ProfileGroupItem>(list.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!));
}
private ProfileGroupItem AddProfileGroupItem(string indexId)
{
var profileGroupItem = new ProfileGroupItem()
{
IndexId = indexId,
ChildItems = string.Empty,
MultipleLoad = EMultipleLoad.LeastPing
};
_items[indexId] = profileGroupItem;
return profileGroupItem;
}
private ProfileGroupItem GetProfileGroupItem(string indexId)
{
if (string.IsNullOrEmpty(indexId))
{
indexId = Utils.GetGuid(false);
}
return _items.GetOrAdd(indexId, AddProfileGroupItem);
}
public async Task ClearAll()
{
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileGroupItem ");
_items.Clear();
}
public async Task SaveTo()
{
try
{
var lstExists = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().ToListAsync();
var existsMap = lstExists.Where(t => !string.IsNullOrEmpty(t.IndexId)).ToDictionary(t => t.IndexId!);
var lstInserts = new List<ProfileGroupItem>();
var lstUpdates = new List<ProfileGroupItem>();
foreach (var item in _items.Values)
{
if (string.IsNullOrEmpty(item.IndexId))
{
continue;
}
if (existsMap.ContainsKey(item.IndexId))
{
lstUpdates.Add(item);
}
else
{
lstInserts.Add(item);
}
}
try
{
if (lstInserts.Count > 0)
{
await SQLiteHelper.Instance.InsertAllAsync(lstInserts);
}
if (lstUpdates.Count > 0)
{
await SQLiteHelper.Instance.UpdateAllAsync(lstUpdates);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
public ProfileGroupItem GetOrCreateAndMarkDirty(string indexId)
{
return GetProfileGroupItem(indexId);
}
public async ValueTask DisposeAsync()
{
await SaveTo();
}
public async Task SaveItemAsync(ProfileGroupItem item)
{
if (item is null)
{
throw new ArgumentNullException(nameof(item));
}
if (string.IsNullOrWhiteSpace(item.IndexId))
{
throw new ArgumentException("IndexId required", nameof(item));
}
_items[item.IndexId] = item;
try
{
var lst = await SQLiteHelper.Instance.TableAsync<ProfileGroupItem>().Where(t => t.IndexId == item.IndexId).ToListAsync();
if (lst != null && lst.Count > 0)
{
await SQLiteHelper.Instance.UpdateAllAsync(new List<ProfileGroupItem> { item });
}
else
{
await SQLiteHelper.Instance.InsertAllAsync(new List<ProfileGroupItem> { item });
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
}
#region Helper
public static bool HasCycle(string? indexId)
{
return HasCycle(indexId, new HashSet<string>(), new HashSet<string>());
}
public static bool HasCycle(string? indexId, HashSet<string> visited, HashSet<string> stack)
{
if (indexId.IsNullOrEmpty())
return false;
if (stack.Contains(indexId))
return true;
if (visited.Contains(indexId))
return false;
visited.Add(indexId);
stack.Add(indexId);
try
{
Instance.TryGet(indexId, out var groupItem);
if (groupItem == null || groupItem.ChildItems.IsNullOrEmpty())
{
return false;
}
var childIds = Utils.String2List(groupItem.ChildItems)
.Where(p => !string.IsNullOrEmpty(p))
.ToList();
if (childIds == null)
{
return false;
}
foreach (var child in childIds)
{
if (HasCycle(child, visited, stack))
{
return true;
}
}
return false;
}
finally
{
stack.Remove(indexId);
}
}
public static async Task<(List<ProfileItem> Items, ProfileGroupItem? Group)> GetChildProfileItems(string? indexId)
{
Instance.TryGet(indexId, out var profileGroupItem);
if (profileGroupItem == null || profileGroupItem.ChildItems.IsNullOrEmpty())
{
return (new List<ProfileItem>(), profileGroupItem);
}
var items = await GetChildProfileItems(profileGroupItem);
return (items, profileGroupItem);
}
public static async Task<List<ProfileItem>> GetChildProfileItems(ProfileGroupItem? group)
{
if (group == null || group.ChildItems.IsNullOrEmpty())
{
return new();
}
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p =>
p != null &&
p.IsValid() &&
p.ConfigType != EConfigType.Custom
)
.ToList();
return childProfiles;
}
public static async Task<HashSet<string>> GetAllChildDomainAddresses(string indexId)
{
// include grand children
var childAddresses = new HashSet<string>();
if (!Instance.TryGet(indexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType.IsGroupType())
{
var subAddresses = await GetAllChildDomainAddresses(childNode.IndexId);
foreach (var addr in subAddresses)
{
childAddresses.Add(addr);
}
}
}
return childAddresses;
}
#endregion Helper
}

View file

@ -8,14 +8,14 @@ public class StatisticsManager
private Config _config;
private ServerStatItem? _serverStatItem;
private List<ServerStatItem> _lstServerStat;
private Action<ServerSpeedItem>? _updateFunc;
private Func<ServerSpeedItem, Task>? _updateFunc;
private StatisticsXrayService? _statisticsXray;
private StatisticsSingboxService? _statisticsSingbox;
private static readonly string _tag = "StatisticsHandler";
public List<ServerStatItem> ServerStat => _lstServerStat;
public async Task Init(Config config, Action<ServerSpeedItem> updateFunc)
public async Task Init(Config config, Func<ServerSpeedItem, Task> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
@ -97,9 +97,9 @@ public class StatisticsManager
_lstServerStat = await SQLiteHelper.Instance.TableAsync<ServerStatItem>().ToListAsync();
}
private void UpdateServerStatHandler(ServerSpeedItem server)
private async Task UpdateServerStatHandler(ServerSpeedItem server)
{
_ = UpdateServerStat(server);
await UpdateServerStat(server);
}
private async Task UpdateServerStat(ServerSpeedItem server)
@ -123,7 +123,7 @@ public class StatisticsManager
server.TodayDown = _serverStatItem.TodayDown;
server.TotalUp = _serverStatItem.TotalUp;
server.TotalDown = _serverStatItem.TotalDown;
_updateFunc?.Invoke(server);
await _updateFunc?.Invoke(server);
}
private async Task GetServerStatItem(string indexId)

View file

@ -4,13 +4,18 @@ public class TaskManager
{
private static readonly Lazy<TaskManager> _instance = new(() => new());
public static TaskManager Instance => _instance.Value;
private Config _config;
private Func<bool, string, Task>? _updateFunc;
public void RegUpdateTask(Config config, Action<bool, string> updateFunc)
public void RegUpdateTask(Config config, Func<bool, string, Task> updateFunc)
{
Task.Run(() => ScheduledTasks(config, updateFunc));
_config = config;
_updateFunc = updateFunc;
Task.Run(ScheduledTasks);
}
private async Task ScheduledTasks(Config config, Action<bool, string> updateFunc)
private async Task ScheduledTasks()
{
Logging.SaveLog("Setup Scheduled Tasks");
@ -21,14 +26,14 @@ public class TaskManager
await Task.Delay(1000 * 60);
//Execute once 1 minute
await UpdateTaskRunSubscription(config, updateFunc);
await UpdateTaskRunSubscription();
//Execute once 20 minute
if (numOfExecuted % 20 == 0)
{
//Logging.SaveLog("Execute save config");
await ConfigHandler.SaveConfig(config);
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
}
@ -42,14 +47,14 @@ public class TaskManager
FileManager.DeleteExpiredFiles(Utils.GetTempPath(), DateTime.Now.AddMonths(-1));
//Check once 1 hour
await UpdateTaskRunGeo(config, numOfExecuted / 60, updateFunc);
await UpdateTaskRunGeo(numOfExecuted / 60);
}
numOfExecuted++;
}
}
private async Task UpdateTaskRunSubscription(Config config, Action<bool, string> updateFunc)
private async Task UpdateTaskRunSubscription()
{
var updateTime = ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds();
var lstSubs = (await AppManager.Instance.SubItems())?
@ -66,30 +71,30 @@ public class TaskManager
foreach (var item in lstSubs)
{
await SubscriptionHandler.UpdateProcess(config, item.Id, true, (success, msg) =>
await SubscriptionHandler.UpdateProcess(_config, item.Id, true, async (success, msg) =>
{
updateFunc?.Invoke(success, msg);
await _updateFunc?.Invoke(success, msg);
if (success)
{
Logging.SaveLog($"Update subscription end. {msg}");
}
});
item.UpdateTime = updateTime;
await ConfigHandler.AddSubItem(config, item);
await ConfigHandler.AddSubItem(_config, item);
await Task.Delay(1000);
}
}
private async Task UpdateTaskRunGeo(Config config, int hours, Action<bool, string> updateFunc)
private async Task UpdateTaskRunGeo(int hours)
{
if (config.GuiItem.AutoUpdateInterval > 0 && hours > 0 && hours % config.GuiItem.AutoUpdateInterval == 0)
if (_config.GuiItem.AutoUpdateInterval > 0 && hours > 0 && hours % _config.GuiItem.AutoUpdateInterval == 0)
{
Logging.SaveLog("Execute update geo files");
var updateHandle = new UpdateService();
await updateHandle.UpdateGeoFileAll(config, (success, msg) =>
await updateHandle.UpdateGeoFileAll(_config, async (success, msg) =>
{
updateFunc?.Invoke(false, msg);
await _updateFunc?.Invoke(false, msg);
});
}
}

View file

@ -1,10 +1,13 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.Models;
public class CheckUpdateModel
public class CheckUpdateModel : ReactiveObject
{
public bool? IsSelected { get; set; }
public string? CoreType { get; set; }
public string? Remarks { get; set; }
[Reactive] public string? Remarks { get; set; }
public string? FileName { get; set; }
public bool? IsFinished { get; set; }
}

View file

@ -1,7 +1,10 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.Models;
[Serializable]
public class ClashProxyModel
public class ClashProxyModel : ReactiveObject
{
public string? Name { get; set; }
@ -9,9 +12,9 @@ public class ClashProxyModel
public string? Now { get; set; }
public int Delay { get; set; }
[Reactive] public int Delay { get; set; }
public string? DelayName { get; set; }
[Reactive] public string? DelayName { get; set; }
public bool IsActive { get; set; }
}

View file

@ -260,11 +260,10 @@ public class SimpleDNSItem
public bool? UseSystemHosts { get; set; }
public bool? AddCommonHosts { get; set; }
public bool? FakeIP { get; set; }
public bool? GlobalFakeIp { get; set; }
public bool? BlockBindingQuery { get; set; }
public string? DirectDNS { get; set; }
public string? RemoteDNS { get; set; }
public string? SingboxOutboundsResolveDNS { get; set; }
public string? SingboxFinalResolveDNS { get; set; }
public string? RayStrategy4Freedom { get; set; }
public string? SingboxStrategy4Direct { get; set; }
public string? SingboxStrategy4Proxy { get; set; }

View file

@ -17,4 +17,5 @@ public class CoreInfo
public string? Match { get; set; }
public string? VersionArg { get; set; }
public bool AbsolutePath { get; set; }
public IDictionary<string, string?> Environment { get; set; } = new Dictionary<string, string?>();
}

View file

@ -0,0 +1,14 @@
using SQLite;
namespace ServiceLib.Models;
[Serializable]
public class ProfileGroupItem
{
[PrimaryKey]
public string IndexId { get; set; }
public string ChildItems { get; set; }
public EMultipleLoad MultipleLoad { get; set; } = EMultipleLoad.LeastPing;
}

View file

@ -32,18 +32,21 @@ public class ProfileItem : ReactiveObject
public string GetSummary()
{
var summary = $"[{(ConfigType).ToString()}] ";
var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.');
var addr = arrAddr.Length switch
if (IsComplex())
{
> 2 => $"{arrAddr.First()}***{arrAddr.Last()}",
> 1 => $"***{arrAddr.Last()}",
_ => Address
};
summary += ConfigType switch
summary += $"[{CoreType.ToString()}]{Remarks}";
}
else
{
EConfigType.Custom => $"[{CoreType.ToString()}]{Remarks}",
_ => $"{Remarks}({addr}:{Port})"
};
var arrAddr = Address.Contains(':') ? Address.Split(':') : Address.Split('.');
var addr = arrAddr.Length switch
{
> 2 => $"{arrAddr.First()}***{arrAddr.Last()}",
> 1 => $"***{arrAddr.Last()}",
_ => Address
};
summary += $"{Remarks}({addr}:{Port})";
}
return summary;
}
@ -61,6 +64,51 @@ public class ProfileItem : ReactiveObject
return Network.TrimEx();
}
public bool IsComplex()
{
return ConfigType.IsComplexType();
}
public bool IsValid()
{
if (IsComplex())
return true;
if (Address.IsNullOrEmpty() || Port is <= 0 or >= 65536)
return false;
switch (ConfigType)
{
case EConfigType.VMess:
if (Id.IsNullOrEmpty() || !Utils.IsGuidByParse(Id))
return false;
break;
case EConfigType.VLESS:
if (Id.IsNullOrEmpty() || (!Utils.IsGuidByParse(Id) && Id.Length > 30))
return false;
if (!Global.Flows.Contains(Flow))
return false;
break;
case EConfigType.Shadowsocks:
if (Id.IsNullOrEmpty())
return false;
if (string.IsNullOrEmpty(Security) || !Global.SsSecuritiesInSingbox.Contains(Security))
return false;
break;
}
if ((ConfigType is EConfigType.VLESS or EConfigType.Trojan)
&& StreamSecurity == Global.StreamSecurityReality
&& PublicKey.IsNullOrEmpty())
{
return false;
}
return true;
}
#endregion function
[PrimaryKey]

View file

@ -15,4 +15,5 @@ public class RulesItem
public List<string>? Process { get; set; }
public bool Enabled { get; set; } = true;
public string? Remarks { get; set; }
public ERuleType? RuleType { get; set; }
}

View file

@ -7,4 +7,5 @@ public class RulesItemModel : RulesItem
public string Ips { get; set; }
public string Domains { get; set; }
public string Protocols { get; set; }
public string RuleTypeName { get; set; }
}

View file

@ -145,6 +145,7 @@ public class Outbound4Sbox : BaseServer4Sbox
public string? plugin_opts { get; set; }
public List<string>? outbounds { get; set; }
public bool? interrupt_exist_connections { get; set; }
public int? tolerance { get; set; }
}
public class Endpoints4Sbox : BaseServer4Sbox

File diff suppressed because it is too large Load diff

View file

@ -675,8 +675,8 @@
<data name="TbSettingsCore" xml:space="preserve">
<value>هسته: تنظیمات اولیه</value>
</data>
<data name="TbSettingsCoreDns" xml:space="preserve">
<value>تنظیمات V2ray DNS</value>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>هسته: تنظیمات KCP</value>
@ -1011,8 +1011,8 @@
<data name="menuDNSSetting" xml:space="preserve">
<value>تنظیمات DNS</value>
</data>
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
<value>تنظیمات DNS sing-box</value>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>لطفا ساختار DNS را پر کنید، برای مشاهده سند کلیک کنید</value>
@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>مخفی و پورت می شود، با کاما (،) جدا می شود</value>
</data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
<value>چند سرور به پیکربندی سفارشی</value>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>چند سرور تصادفی توسط Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>چند سرور RoundRobin توسط Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>چند سرور LeastPing توسط Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>چند سرور LeastLoad توسط Xray</value>
</data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>LeastPing چند سرور توسط sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
@ -1419,19 +1419,10 @@
<data name="TbDomesticDNS" xml:space="preserve">
<value>Domestic DNS</value>
</data>
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
<value>Outbound DNS Resolution (sing-box)</value>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>Via proxy — please ensure remote availability</value>
</data>
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value>
</data>
<data name="TbSBDoHResolverServer" xml:space="preserve">
<value>sing-box DoH Resolver Server</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
@ -1443,9 +1434,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
@ -1455,9 +1443,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value>
</data>
@ -1477,7 +1462,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Prevent domain-based routing rules from failing</value>
<value>Block ECH and HTTP/3 availability checks when enabled</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value>
@ -1509,4 +1494,103 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data>
</root>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>Rule Type</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
</data>
</root>

View file

@ -675,8 +675,8 @@
<data name="TbSettingsCore" xml:space="preserve">
<value>Core: alapbeállítások</value>
</data>
<data name="TbSettingsCoreDns" xml:space="preserve">
<value>V2ray DNS beállítások</value>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Core: KCP beállítások</value>
@ -1011,8 +1011,8 @@
<data name="menuDNSSetting" xml:space="preserve">
<value>DNS beállítások</value>
</data>
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
<value>sing-box DNS beállítások</value>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Kérjük, töltse ki a DNS struktúrát, kattintson a dokumentum megtekintéséhez</value>
@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>A portot lefedi, vesszővel (,) elválasztva</value>
</data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
<value>Több konfiguráció egyéni konfigurációra</value>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Több konfiguráció véletlenszerűen Xray szerint</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Több konfiguráció RoundRobin Xray szerint</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Több konfiguráció legkisebb pinggel Xray szerint</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Több konfiguráció legkisebb terheléssel Xray szerint</value>
</data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Több konfiguráció legkisebb pinggel sing-box szerint</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
@ -1419,19 +1419,10 @@
<data name="TbDomesticDNS" xml:space="preserve">
<value>Domestic DNS</value>
</data>
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
<value>Outbound DNS Resolution (sing-box)</value>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>Via proxy — please ensure remote availability</value>
</data>
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value>
</data>
<data name="TbSBDoHResolverServer" xml:space="preserve">
<value>sing-box DoH Resolver Server</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
@ -1443,9 +1434,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
@ -1455,9 +1443,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value>
</data>
@ -1477,7 +1462,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Prevent domain-based routing rules from failing</value>
<value>Block ECH and HTTP/3 availability checks when enabled</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value>
@ -1509,4 +1494,103 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data>
</root>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>Rule Type</value>
</data>
</root>

View file

@ -675,8 +675,8 @@
<data name="TbSettingsCore" xml:space="preserve">
<value>Core: basic settings</value>
</data>
<data name="TbSettingsCoreDns" xml:space="preserve">
<value>V2ray DNS settings</value>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Core: KCP settings</value>
@ -1011,8 +1011,8 @@
<data name="menuDNSSetting" xml:space="preserve">
<value>DNS Settings</value>
</data>
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
<value>sing-box DNS settings</value>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Please fill in DNS Structure, Click to view the document</value>
@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>Will cover the port, separate with commas (,)</value>
</data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
<value>Multi-Configuration to custom configuration</value>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Multi-Configuration Random by Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Multi-Configuration RoundRobin by Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Multi-Configuration LeastPing by Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Multi-Configuration LeastLoad by Xray</value>
</data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Multi-Configuration LeastPing by sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
@ -1419,19 +1419,10 @@
<data name="TbDomesticDNS" xml:space="preserve">
<value>Domestic DNS</value>
</data>
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
<value>Outbound DNS Resolution (sing-box)</value>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>Via proxy — please ensure remote availability</value>
</data>
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value>
</data>
<data name="TbSBDoHResolverServer" xml:space="preserve">
<value>sing-box DoH Resolver Server</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
@ -1443,9 +1434,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
@ -1455,9 +1443,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value>
</data>
@ -1477,7 +1462,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Prevent domain-based routing rules from failing</value>
<value>Block ECH and HTTP/3 availability checks when enabled</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value>
@ -1509,4 +1494,103 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data>
</root>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>Rule Type</value>
</data>
</root>

View file

@ -675,8 +675,8 @@
<data name="TbSettingsCore" xml:space="preserve">
<value>Ядро: базовые настройки</value>
</data>
<data name="TbSettingsCoreDns" xml:space="preserve">
<value>Настройки DNS V2ray</value>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Ядро: настройки KCP</value>
@ -1011,8 +1011,8 @@
<data name="menuDNSSetting" xml:space="preserve">
<value>Настройки DNS</value>
</data>
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
<value>Настройки DNS sing-box</value>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>Заполните структуру DNS, нажмите, чтобы открыть документ</value>
@ -1377,22 +1377,22 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>Заменит указанный порт, перечисляйте через запятую (,)</value>
</data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
<value>От мультиконфигурации к пользовательской конфигурации</value>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>Случайный (Xray)</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>Круговой (Xray)</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>Минимальное RTT (минимальное время туда-обратно) (Xray)</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>Минимальная нагрузка (Xray)</value>
</data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>Минимальное RTT (минимальное время туда-обратно) (sing-box)</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
@ -1419,19 +1419,10 @@
<data name="TbDomesticDNS" xml:space="preserve">
<value>Внутренний DNS</value>
</data>
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
<value>Резолвер DNS для исходящих (sing-box)</value>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>Via proxy — please ensure remote availability</value>
</data>
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Разрешать домены для исходящих соединений</value>
</data>
<data name="TbSBDoHResolverServer" xml:space="preserve">
<value>Сервер DoH-резолвера (sing-box)</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Резервное DNS-разрешение (рекомендуется указывать IP)</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>Стратегия резолвинга Freedom (Xray)</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
@ -1443,9 +1434,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Добавить стандартные записи hosts (DNS)</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>Сервер DoH-резолвера sing-box можно переопределить</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
@ -1455,9 +1443,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS hosts: (каждая строка в формате "domain1 ip1 ip2")</value>
</data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Применять только к доменам через прокси</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>Базовые настройки DNS</value>
</data>
@ -1477,7 +1462,7 @@
<value>Включён пользовательский DNS — настройки на этой странице не применяются</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Предотвращает сбои доменных правил маршрутизации</value>
<value>Block ECH and HTTP/3 availability checks when enabled</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Пожалуйста, заполните корректный шаблон конфигурации</value>
@ -1509,4 +1494,103 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>Эта функция предназначена для продвинутых пользователей и особых случаев. После включения игнорируются базовые настройки ядра, DNS и маршрутизации. Вы должны самостоятельно корректно задать порт системного прокси, учёт трафика и другие связанные параметры — всё настраивается вручную.</value>
</data>
</root>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>Start parsing and processing subscription content</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} Group cannot reference itself or have a circular reference</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>If the system does not have a tray function, please do not enable it</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>You can set separate rules for Routing and DNS, or select "ALL" to apply to both</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>Rule Type</value>
</data>
</root>

View file

@ -675,8 +675,8 @@
<data name="TbSettingsCore" xml:space="preserve">
<value>Core: 基础设置</value>
</data>
<data name="TbSettingsCoreDns" xml:space="preserve">
<value>v2ray DNS 设置</value>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>v2ray 自定义 DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Core: KCP 设置</value>
@ -1008,8 +1008,8 @@
<data name="menuDNSSetting" xml:space="preserve">
<value>DNS 设置</value>
</data>
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
<value>sing-box DNS 设置</value>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box 自定义 DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>请填写 DNS JSON 结构,点击查看文档</value>
@ -1374,22 +1374,22 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>会覆盖端口,多组时用逗号 (,) 隔开</value>
</data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
<value>多配置文件产生自定义配置 (多选)</value>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>多配置文件生成策略组</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多配置文件随机 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多配置文件负载均衡 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多配置文件最低延迟 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多配置文件最稳定 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多配置文件最低延迟 sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
@ -1416,19 +1416,10 @@
<data name="TbDomesticDNS" xml:space="preserve">
<value>直连 DNS</value>
</data>
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
<value>出站 DNS 解析sing-box</value>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>通过代理,请确保远程可用</value>
</data>
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>解析出站域名</value>
</data>
<data name="TbSBDoHResolverServer" xml:space="preserve">
<value>sing-box DoH 解析服务器</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>兜底解析其他 DNS 域名,建议设为 ip</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>xray freedom 解析策略</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
@ -1440,9 +1431,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>添加常用 DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>开启后可覆盖 sing-box DoH 解析服务器</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
@ -1452,9 +1440,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts“域名1 ip1 ip2” 一行一个)</value>
</data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>仅对代理域名生效</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>DNS 基础设置</value>
</data>
@ -1474,7 +1459,7 @@
<value>自定义 DNS 已启用,此页面配置将无效</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>避免域名分流规则失效</value>
<value>开启后将阻止 ECH 和 HTTP/3 可用性查询</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>请填写正确的配置模板</value>
@ -1506,4 +1491,103 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>此功能供高级用户和有特殊需求的用户使用。 启用此功能后,将忽略 Core 的基础设置DNS 设置 ,路由设置。你需要保证系统代理的端口和流量统计等功能的配置正确,一切都由你来设置。</value>
</data>
</root>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>开始解析和处理订阅内容</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>选择配置文件</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>请至少添加一个配置文件</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>策略组</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>链式代理</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>最低延迟</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>随机</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>负载均衡</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>最稳定</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>策略组类型</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>添加策略组配置文件</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>添加链式代理配置文件</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>添加子配置文件</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>删除子配置文件</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>服务器列表</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>故障转移</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>多配置文件故障转移 sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>多配置文件故障转移 Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>核心 '{0}' 不支持网络类型 '{1}'。</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>核心 '{0}' 不支持协议 '{1}'。</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>代理链: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>路由规则出站: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>策略组: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>节点别名 '{0}' 不存在。</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>组“{0}”为空。请至少添加一个节点。</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>{0}属性无效,请检查</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分组不能引用自身或循环引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>不支持协议 '{0}'。</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系统没有托盘功能,请不要开启</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>规则类型</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>可对 Routing 和 DNS 单独设定规则ALL 则都生效</value>
</data>
</root>

View file

@ -675,8 +675,8 @@
<data name="TbSettingsCore" xml:space="preserve">
<value>Core: 基礎設定</value>
</data>
<data name="TbSettingsCoreDns" xml:space="preserve">
<value>V2ray DNS 設定</value>
<data name="TbCustomDnsRay" xml:space="preserve">
<value>V2ray Custom DNS</value>
</data>
<data name="TbSettingsCoreKcp" xml:space="preserve">
<value>Core: KCP 設定</value>
@ -1008,8 +1008,8 @@
<data name="menuDNSSetting" xml:space="preserve">
<value>DNS 設定</value>
</data>
<data name="TbSettingsCoreDnsSingbox" xml:space="preserve">
<value>sing-box DNS 設定</value>
<data name="TbCustomDnsSingbox" xml:space="preserve">
<value>sing-box Custom DNS</value>
</data>
<data name="TbDnsSingboxObjectDoc" xml:space="preserve">
<value>請填寫 DNS JSON 結構,點擊查看檔案</value>
@ -1374,22 +1374,22 @@
<data name="TbPorts7Tips" xml:space="preserve">
<value>會覆蓋埠,多組時用逗號 (,) 隔開</value>
</data>
<data name="menuSetDefaultMultipleServer" xml:space="preserve">
<value>多設定檔產生自訂配置 (多選)</value>
<data name="menuGenGroupMultipleServer" xml:space="preserve">
<value>Generate Policy Group from Multiple Profiles</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRandom" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRandom" xml:space="preserve">
<value>多設定檔隨機 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayRoundRobin" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayRoundRobin" xml:space="preserve">
<value>多設定檔負載平衡 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastPing" xml:space="preserve">
<value>多設定檔最低延遲 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerXrayLeastLoad" xml:space="preserve">
<data name="menuGenGroupMultipleServerXrayLeastLoad" xml:space="preserve">
<value>多設定檔最穩定 Xray</value>
</data>
<data name="menuSetDefaultMultipleServerSingBoxLeastPing" xml:space="preserve">
<data name="menuGenGroupMultipleServerSingBoxLeastPing" xml:space="preserve">
<value>多設定檔最低延遲 sing-box</value>
</data>
<data name="menuExportConfig" xml:space="preserve">
@ -1416,19 +1416,10 @@
<data name="TbDomesticDNS" xml:space="preserve">
<value>Domestic DNS</value>
</data>
<data name="TbSBOutboundsResolverDNS" xml:space="preserve">
<value>Outbound DNS Resolution (sing-box)</value>
<data name="TbRemoteDNSTips" xml:space="preserve">
<value>Via proxy — please ensure remote availability</value>
</data>
<data name="TbSBOutboundDomainResolve" xml:space="preserve">
<value>Resolve Outbound Domains</value>
</data>
<data name="TbSBDoHResolverServer" xml:space="preserve">
<value>sing-box DoH Resolver Server</value>
</data>
<data name="TbSBFallbackDNSResolve" xml:space="preserve">
<value>Fallback DNS Resolution, Suggest IP</value>
</data>
<data name="TbXrayFreedomResolveStrategy" xml:space="preserve">
<data name="TbXrayFreedomStrategy" xml:space="preserve">
<value>xray Freedom Resolution Strategy</value>
</data>
<data name="TbSBDirectResolveStrategy" xml:space="preserve">
@ -1440,9 +1431,6 @@
<data name="TbAddCommonDNSHosts" xml:space="preserve">
<value>Add Common DNS Hosts</value>
</data>
<data name="TbSBDoHOverride" xml:space="preserve">
<value>The sing-box DoH resolution server can be overwritten</value>
</data>
<data name="TbFakeIP" xml:space="preserve">
<value>FakeIP</value>
</data>
@ -1452,9 +1440,6 @@
<data name="TbDNSHostsConfig" xml:space="preserve">
<value>DNS Hosts: ("domain1 ip1 ip2" per line)</value>
</data>
<data name="TbApplyProxyDomainsOnly" xml:space="preserve">
<value>Apply to Proxy Domains Only</value>
</data>
<data name="ThBasicDNSSettings" xml:space="preserve">
<value>Basic DNS Settings</value>
</data>
@ -1474,7 +1459,7 @@
<value>Custom DNS Enabled, This Page's Settings Invalid</value>
</data>
<data name="TbBlockSVCBHTTPSQueriesTips" xml:space="preserve">
<value>Prevent domain-based routing rules from failing</value>
<value>Block ECH and HTTP/3 availability checks when enabled</value>
</data>
<data name="FillCorrectConfigTemplateText" xml:space="preserve">
<value>Please fill in the correct config template</value>
@ -1506,4 +1491,103 @@
<data name="TbFullConfigTemplateDesc" xml:space="preserve">
<value>This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you.</value>
</data>
</root>
<data name="MsgStartParsingSubscription" xml:space="preserve">
<value>開始解析和處理訂閱內容</value>
</data>
<data name="TbSelectProfile" xml:space="preserve">
<value>Select Profile</value>
</data>
<data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value>
</data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value>
</data>
<data name="TbConfigTypePolicyGroup" xml:space="preserve">
<value>Policy Group</value>
</data>
<data name="TbConfigTypeProxyChain" xml:space="preserve">
<value>Proxy Chain</value>
</data>
<data name="TbLeastPing" xml:space="preserve">
<value>Lowest Latency</value>
</data>
<data name="TbRandom" xml:space="preserve">
<value>Random</value>
</data>
<data name="TbRoundRobin" xml:space="preserve">
<value>Round Robin</value>
</data>
<data name="TbLeastLoad" xml:space="preserve">
<value>Most Stable</value>
</data>
<data name="TbPolicyGroupType" xml:space="preserve">
<value>Policy Group Type</value>
</data>
<data name="menuAddPolicyGroupServer" xml:space="preserve">
<value>Add Policy Group Configuration</value>
</data>
<data name="menuAddProxyChainServer" xml:space="preserve">
<value>Add Proxy Chain Configuration</value>
</data>
<data name="menuAddChildServer" xml:space="preserve">
<value>Add Child Configuration</value>
</data>
<data name="menuRemoveChildServer" xml:space="preserve">
<value>Remove Child Configuration</value>
</data>
<data name="menuServerList" xml:space="preserve">
<value>Server List</value>
</data>
<data name="TbFallback" xml:space="preserve">
<value>Fallback</value>
</data>
<data name="menuGenGroupMultipleServerSingBoxFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by sing-box</value>
</data>
<data name="menuGenGroupMultipleServerXrayFallback" xml:space="preserve">
<value>Multi-Configuration Fallback by Xray</value>
</data>
<data name="CoreNotSupportNetwork" xml:space="preserve">
<value>Core '{0}' does not support network type '{1}'.</value>
</data>
<data name="CoreNotSupportProtocolTransport" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}' when using transport '{2}'.</value>
</data>
<data name="CoreNotSupportProtocol" xml:space="preserve">
<value>Core '{0}' does not support protocol '{1}'.</value>
</data>
<data name="ProxyChainedPrefix" xml:space="preserve">
<value>Proxy chained: </value>
</data>
<data name="RoutingRuleOutboundPrefix" xml:space="preserve">
<value>Routing rule outbound: </value>
</data>
<data name="PolicyGroupPrefix" xml:space="preserve">
<value>Policy group: </value>
</data>
<data name="NodeTagNotExist" xml:space="preserve">
<value>Node alias '{0}' does not exist.</value>
</data>
<data name="GroupEmpty" xml:space="preserve">
<value>Group '{0}' is empty. Please add at least one node.</value>
</data>
<data name="InvalidProperty" xml:space="preserve">
<value>The {0} property is invalid, please check.</value>
</data>
<data name="GroupSelfReference" xml:space="preserve">
<value>{0} 分組不能引用自身或循環引用</value>
</data>
<data name="NotSupportProtocol" xml:space="preserve">
<value>Not support protocol '{0}'.</value>
</data>
<data name="TbSettingsHide2TrayWhenCloseTip" xml:space="preserve">
<value>如果系統沒有托盤功能,請不要開啟</value>
</data>
<data name="TbRuleType" xml:space="preserve">
<value>规则类型</value>
</data>
<data name="TbRuleTypeTips" xml:space="preserve">
<value>可对 Routing 和 DNS 单独设定规则ALL 则都生效</value>
</data>
</root>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
{
"domain": [
"amobile.music.tc.qq.com",
"api-jooxtt.sanook.com",
"api.joox.com",
"aqqmusic.tc.qq.com",
"dl.stream.qqmusic.qq.com",
"ff.dorado.sdo.com",
"heartbeat.belkin.com",
"isure.stream.qqmusic.qq.com",
"joox.com",
"lens.l.google.com",
"localhost.ptlogin2.qq.com",
"localhost.sec.qq.com",
"mesu.apple.com",
"mobileoc.music.tc.qq.com",
"music.taihe.com",
"musicapi.taihe.com",
"na.b.g-tun.com",
"proxy.golang.org",
"ps.res.netease.com",
"shark007.net",
"songsearch.kugou.com",
"static.adtidy.org",
"streamoc.music.tc.qq.com",
"swcdn.apple.com",
"swdist.apple.com",
"swdownload.apple.com",
"swquery.apple.com",
"swscan.apple.com",
"turn.cloudflare.com",
"trackercdn.kugou.com",
"xnotify.xboxlive.com"
],
"domain_keyword": [
"ntp",
"stun",
"time"
],
"domain_regex": [
"^[^.]+$",
"^[^.]+\\.[^.]+\\.xboxlive\\.com$",
"^localhost\\.[^.]+\\.weixin\\.qq\\.com$",
"^mijia\\scloud$",
"^xbox\\.[^.]+\\.microsoft\\.com$",
"^xbox\\.[^.]+\\.[^.]+\\.microsoft\\.com$"
],
"domain_suffix": [
"126.net",
"3gppnetwork.org",
"battle.net",
"battlenet.com.cn",
"cdn.nintendo.net",
"cmbchina.com",
"cmbimg.com",
"ff14.sdo.com",
"ffxiv.com",
"finalfantasyxiv.com",
"gcloudcs.com",
"home.arpa",
"invalid",
"kuwo.cn",
"lan",
"linksys.com",
"linksyssmartwifi.com",
"local",
"localdomain",
"localhost",
"market.xiaomi.com",
"mcdn.bilivideo.cn",
"media.dssott.com",
"msftconnecttest.com",
"msftncsi.com",
"music.163.com",
"music.migu.cn",
"n0808.com",
"nflxvideo.net",
"oray.com",
"orayimg.com",
"router.asus.com",
"sandai.net",
"square-enix.com",
"srv.nintendo.net",
"steamcontent.com",
"uu.163.com",
"wargaming.net",
"wggames.cn",
"wotgame.cn",
"wowsgame.cn",
"xiami.com",
"y.qq.com"
]
}

View file

@ -11,7 +11,7 @@
</PackageReference>
<PackageReference Include="ReactiveUI.Fody" />
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="Splat.NLog" />
<PackageReference Include="NLog" />
<PackageReference Include="WebDav.Client" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="QRCoder" />
@ -44,6 +44,7 @@
<EmbeddedResource Include="Sample\tun_singbox_inbound" />
<EmbeddedResource Include="Sample\tun_singbox_rules" />
<EmbeddedResource Include="Sample\linux_autostart_config" />
<EmbeddedResource Include="Sample\singbox_fakeip_filter" />
</ItemGroup>
<ItemGroup>

View file

@ -79,6 +79,7 @@ public class CoreConfigClashService
//external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret");
//allow-lan
if (_config.Inbound.First().AllowLANConn)
{

View file

@ -1,5 +1,6 @@
using System.Net;
using System.Net.NetworkInformation;
using ServiceLib.Common;
namespace ServiceLib.Services.CoreConfig;
@ -16,7 +17,7 @@ public partial class CoreConfigSingboxService(Config config)
try
{
if (node == null
|| node.Port <= 0)
|| !node.IsValid())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
@ -29,6 +30,18 @@ public partial class CoreConfigSingboxService(Config config)
ret.Msg = ResUI.InitialConfiguration;
if (node.ConfigType.IsGroupType())
{
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
return await GenerateClientMultipleLoadConfig(node);
case EConfigType.ProxyChain:
return await GenerateClientChainConfig(node);
}
}
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
if (result.IsNullOrEmpty())
{
@ -142,12 +155,9 @@ public partial class CoreConfigSingboxService(Config config)
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
if (item is null || item.IsComplex() || !item.IsValid())
{
if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
continue;
}
//find unused port
@ -187,27 +197,6 @@ public partial class CoreConfigSingboxService(Config config)
singboxConfig.inbounds.Add(inbound);
//outbound
if (item is null)
{
continue;
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS
&& !Global.Flows.Contains(item.Flow))
{
continue;
}
if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan
&& item.StreamSecurity == Global.StreamSecurityReality
&& item.PublicKey.IsNullOrEmpty())
{
continue;
}
var server = await GenServer(item);
if (server is null)
{
@ -246,7 +235,7 @@ public partial class CoreConfigSingboxService(Config config)
}
singboxConfig.route.default_domain_resolver = new()
{
server = Global.SingboxFinalResolverTag
server = Global.SingboxLocalDNSTag,
};
ret.Success = true;
@ -266,7 +255,8 @@ public partial class CoreConfigSingboxService(Config config)
var ret = new RetResult();
try
{
if (node is not { Port: > 0 })
if (node == null
|| !node.IsValid())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
@ -318,7 +308,7 @@ public partial class CoreConfigSingboxService(Config config)
}
singboxConfig.route.default_domain_resolver = new()
{
server = Global.SingboxFinalResolverTag
server = Global.SingboxLocalDNSTag,
};
singboxConfig.route.rules.Clear();
@ -344,7 +334,7 @@ public partial class CoreConfigSingboxService(Config config)
}
}
public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds)
public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode)
{
var ret = new RetResult();
try
@ -371,56 +361,77 @@ public partial class CoreConfigSingboxService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
singboxConfig.outbounds.RemoveAt(0);
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (!Global.SingboxSupportConfigType.Contains(it.ConfigType))
{
continue;
}
if (it.Port <= 0)
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (item is null)
{
continue;
}
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
{
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow))
{
continue;
}
//outbound
proxyProfiles.Add(item);
}
if (proxyProfiles.Count <= 0)
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenOutboundsList(proxyProfiles, singboxConfig);
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(singboxConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
var result = EmbedUtils.GetEmbedText(Global.SingboxSampleClient);
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var singboxConfig = JsonUtils.Deserialize<SingboxConfig>(result);
if (singboxConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
singboxConfig.outbounds.RemoveAt(0);
await GenLog(singboxConfig);
await GenInbounds(singboxConfig);
var groupRet = await GenGroupOutbound(parentNode, singboxConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenRouting(singboxConfig);
await GenExperimental(singboxConfig);
await GenDns(null, singboxConfig);
await ConvertGeo2Ruleset(singboxConfig);

View file

@ -33,17 +33,17 @@ public partial class CoreConfigSingboxService
lastRule.Ip?.Contains("0.0.0.0/0") == true);
}
singboxConfig.dns.final = useDirectDns ? Global.SingboxDirectDNSTag : Global.SingboxRemoteDNSTag;
// Tun2SocksAddress
if (node != null && Utils.IsDomain(node.Address))
if ((!useDirectDns) && simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
{
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
singboxConfig.dns.rules.Add(new()
{
server = Global.SingboxOutboundResolverTag,
domain = [node.Address],
server = Global.SingboxFakeDNSTag,
query_type = new List<int> { 1, 28 }, // A and AAAA
rewrite_ttl = 1,
});
}
await GenOutboundDnsRule(node, singboxConfig);
}
catch (Exception ex)
{
@ -58,16 +58,12 @@ public partial class CoreConfigSingboxService
var directDns = ParseDnsAddress(simpleDNSItem.DirectDNS);
directDns.tag = Global.SingboxDirectDNSTag;
directDns.domain_resolver = Global.SingboxFinalResolverTag;
directDns.domain_resolver = Global.SingboxLocalDNSTag;
var remoteDns = ParseDnsAddress(simpleDNSItem.RemoteDNS);
remoteDns.tag = Global.SingboxRemoteDNSTag;
remoteDns.detour = Global.ProxyTag;
remoteDns.domain_resolver = Global.SingboxFinalResolverTag;
var resolverDns = ParseDnsAddress(simpleDNSItem.SingboxOutboundsResolveDNS);
resolverDns.tag = Global.SingboxOutboundResolverTag;
resolverDns.domain_resolver = Global.SingboxFinalResolverTag;
remoteDns.domain_resolver = Global.SingboxLocalDNSTag;
var hostsDns = new Server4Sbox
{
@ -80,25 +76,21 @@ public partial class CoreConfigSingboxService
hostsDns.predefined = Global.PredefinedHosts;
}
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts != null && systemHosts.Count > 0)
{
foreach (var host in systemHosts)
{
hostsDns.predefined.TryAdd(host.Key, new List<string> { host.Value });
}
}
}
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = simpleDNSItem.Hosts?
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line => !string.IsNullOrWhiteSpace(line))
.Where(line => line.Contains(' '))
.ToDictionary(
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
return parts[0];
},
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
var values = parts.Skip(1).ToList();
return values;
}
) ?? new Dictionary<string, List<string>>();
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
foreach (var kvp in userHostsMap)
{
@ -106,22 +98,6 @@ public partial class CoreConfigSingboxService
}
}
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts.Count > 0)
{
foreach (var host in systemHosts)
{
if (hostsDns.predefined[host.Key] != null)
{
continue;
}
hostsDns.predefined[host.Key] = new List<string> { host.Value };
}
}
}
foreach (var host in hostsDns.predefined)
{
if (finalDns.server == host.Key)
@ -132,10 +108,6 @@ public partial class CoreConfigSingboxService
{
remoteDns.domain_resolver = Global.SingboxHostsDNSTag;
}
if (resolverDns.server == host.Key)
{
resolverDns.domain_resolver = Global.SingboxHostsDNSTag;
}
if (directDns.server == host.Key)
{
directDns.domain_resolver = Global.SingboxHostsDNSTag;
@ -146,7 +118,6 @@ public partial class CoreConfigSingboxService
singboxConfig.dns.servers ??= new List<Server4Sbox>();
singboxConfig.dns.servers.Add(remoteDns);
singboxConfig.dns.servers.Add(directDns);
singboxConfig.dns.servers.Add(resolverDns);
singboxConfig.dns.servers.Add(hostsDns);
// fake ip
@ -167,8 +138,13 @@ public partial class CoreConfigSingboxService
private async Task<Server4Sbox> GenDnsDomains(SingboxConfig singboxConfig, SimpleDNSItem? simpleDNSItem)
{
var finalDns = ParseDnsAddress(simpleDNSItem.SingboxFinalResolveDNS);
finalDns.tag = Global.SingboxFinalResolverTag;
var finalDnsAddress = "local";
if (_config.TunModeItem.EnableTun)
{
finalDnsAddress = "dhcp://auto";
}
var finalDns = ParseDnsAddress(finalDnsAddress);
finalDns.tag = Global.SingboxLocalDNSTag;
singboxConfig.dns ??= new Dns4Sbox();
singboxConfig.dns.servers ??= new List<Server4Sbox>();
singboxConfig.dns.servers.Add(finalDns);
@ -207,6 +183,28 @@ public partial class CoreConfigSingboxService
});
}
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == true)
{
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
fakeipFilterRule.invert = true;
var rule4Fake = new Rule4Sbox
{
server = Global.SingboxFakeDNSTag,
type = "logical",
mode = "and",
rewrite_ttl = 1,
rules = new List<Rule4Sbox>
{
new() {
query_type = new List<int> { 1, 28 }, // A and AAAA
},
fakeipFilterRule,
}
};
singboxConfig.dns.rules.Add(rule4Fake);
}
var routing = await ConfigHandler.GetDefaultRouting(_config);
if (routing == null)
return 0;
@ -251,6 +249,11 @@ public partial class CoreConfigSingboxService
continue;
}
if (item.RuleType == ERuleType.Routing)
{
continue;
}
var rule = new Rule4Sbox();
var validDomains = item.Domain.Count(it => ParseV2Domain(it, rule));
if (validDomains <= 0)
@ -286,10 +289,12 @@ public partial class CoreConfigSingboxService
}
else
{
if (simpleDNSItem.FakeIP == true)
if (simpleDNSItem.FakeIP == true && simpleDNSItem.GlobalFakeIp == false)
{
var rule4Fake = JsonUtils.DeepCopy(rule);
rule4Fake.server = Global.SingboxFakeDNSTag;
rule4Fake.query_type = new List<int> { 1, 28 }; // A and AAAA
rule4Fake.rewrite_ttl = 1;
singboxConfig.dns.rules.Add(rule4Fake);
}
rule.server = Global.SingboxRemoteDNSTag;
@ -333,16 +338,7 @@ public partial class CoreConfigSingboxService
await GenDnsDomainsLegacyCompatible(singboxConfig, item);
}
// Tun2SocksAddress
if (node != null && Utils.IsDomain(node.Address))
{
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = Global.SingboxFinalResolverTag,
domain = [node.Address],
});
}
await GenOutboundDnsRule(node, singboxConfig);
}
catch (Exception ex)
{
@ -351,16 +347,17 @@ public partial class CoreConfigSingboxService
return 0;
}
private async Task<int> GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem)
private async Task<int> GenDnsDomainsCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem)
{
var dns4Sbox = singboxConfig.dns ?? new();
dns4Sbox.servers ??= [];
dns4Sbox.rules ??= [];
var tag = Global.SingboxFinalResolverTag;
var localDnsAddress = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress;
var tag = Global.SingboxLocalDNSTag;
var localDnsServer = ParseDnsAddress(localDnsAddress);
var finalDnsAddress = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress;
var localDnsServer = ParseDnsAddress(finalDnsAddress);
localDnsServer.tag = tag;
dns4Sbox.servers.Add(localDnsServer);
@ -369,19 +366,19 @@ public partial class CoreConfigSingboxService
return await Task.FromResult(0);
}
private async Task<int> GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dNSItem)
private async Task<int> GenDnsDomainsLegacyCompatible(SingboxConfig singboxConfig, DNSItem? dnsItem)
{
var dns4Sbox = singboxConfig.dns ?? new();
dns4Sbox.servers ??= [];
dns4Sbox.rules ??= [];
var tag = Global.SingboxFinalResolverTag;
var tag = Global.SingboxLocalDNSTag;
dns4Sbox.servers.Add(new()
{
tag = tag,
address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress,
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
detour = Global.DirectTag,
strategy = string.IsNullOrEmpty(dNSItem?.DomainStrategy4Freedom) ? null : dNSItem?.DomainStrategy4Freedom,
strategy = string.IsNullOrEmpty(dnsItem?.DomainStrategy4Freedom) ? null : dnsItem?.DomainStrategy4Freedom,
});
dns4Sbox.rules.Insert(0, new()
{
@ -412,6 +409,40 @@ public partial class CoreConfigSingboxService
return await Task.FromResult(0);
}
private async Task<int> GenOutboundDnsRule(ProfileItem? node, SingboxConfig singboxConfig)
{
if (node == null)
{
return 0;
}
List<string> domain = new();
if (Utils.IsDomain(node.Address)) // normal outbound
{
domain.Add(node.Address);
}
if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty()) // Tun2SocksAddress
{
domain.AddRange(Utils.String2List(node.SpiderX)
.Where(Utils.IsDomain)
.Distinct()
.ToList());
}
if (domain.Count == 0)
{
return 0;
}
singboxConfig.dns.rules ??= new List<Rule4Sbox>();
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = Global.SingboxLocalDNSTag,
domain = domain,
});
return await Task.FromResult(0);
}
private static Server4Sbox? ParseDnsAddress(string address)
{
var addressFirst = address?.Split(address.Contains(',') ? ',' : ';').FirstOrDefault()?.Trim();

View file

@ -10,7 +10,7 @@ public partial class CoreConfigSingboxService
singboxConfig.inbounds = [];
if (!_config.TunModeItem.EnableTun
|| _config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box)
|| (_config.TunModeItem.EnableTun && _config.TunModeItem.EnableExInbound && _config.RunningCoreType == ECoreType.sing_box))
{
var inbound = new Inbound4Sbox()
{
@ -78,7 +78,7 @@ public partial class CoreConfigSingboxService
{
Logging.SaveLog(_tag, ex);
}
return 0;
return await Task.FromResult(0);
}
private Inbound4Sbox GetInbound(Inbound4Sbox inItem, EInboundProtocol protocol, bool bSocks)

View file

@ -179,13 +179,21 @@ public partial class CoreConfigSingboxService
if (node.ConfigType == EConfigType.WireGuard)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
await GenEndpoint(node, endpoint);
var ret = await GenEndpoint(node, endpoint);
if (ret != 0)
{
return null;
}
return endpoint;
}
else
{
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(node, outbound);
var ret = await GenOutbound(node, outbound);
if (ret != 0)
{
return null;
}
return outbound;
}
}
@ -196,6 +204,54 @@ public partial class CoreConfigSingboxService
return await Task.FromResult<BaseServer4Sbox?>(null);
}
private async Task<int> GenGroupOutbound(ProfileItem node, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
{
try
{
if (!node.ConfigType.IsGroupType())
{
return -1;
}
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (hasCycle)
{
return -1;
}
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
return -1;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
if (ignoreOriginChain)
{
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
}
else
{
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, baseTagName);
}
break;
case EConfigType.ProxyChain:
await GenChainOutboundsList(childProfiles, singboxConfig, baseTagName);
break;
default:
break;
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenOutboundMux(ProfileItem node, Outbound4Sbox outbound)
{
try
@ -410,7 +466,7 @@ public partial class CoreConfigSingboxService
return 0;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig)
private async Task<int> GenOutboundsListWithChain(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{
try
{
@ -438,6 +494,29 @@ public partial class CoreConfigSingboxService
{
index++;
if (node.ConfigType.IsGroupType())
{
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{index}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (ret == 0)
{
proxyTags.Add(childBaseTagName);
}
continue;
}
// Handle proxy chain
string? prevTag = null;
var currentServer = await GenServer(node);
@ -450,7 +529,7 @@ public partial class CoreConfigSingboxService
var subItem = await AppManager.Instance.GetSubItem(node.Subid);
// current proxy
currentServer.tag = $"{Global.ProxyTag}-{index}";
currentServer.tag = $"{baseTagName}-{index}";
proxyTags.Add(currentServer.tag);
if (!node.Subid.IsNullOrEmpty())
@ -467,7 +546,7 @@ public partial class CoreConfigSingboxService
{
var prevOutbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
prevTag = $"prev-{baseTagName}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
@ -508,16 +587,21 @@ public partial class CoreConfigSingboxService
var outUrltest = new Outbound4Sbox
{
type = "urltest",
tag = $"{Global.ProxyTag}-auto",
tag = $"{baseTagName}-auto",
outbounds = proxyTags,
interrupt_exist_connections = false,
};
if (multipleLoad == EMultipleLoad.Fallback)
{
outUrltest.tolerance = 5000;
}
// Add selector outbound (manual selection)
var outSelector = new Outbound4Sbox
{
type = "selector",
tag = Global.ProxyTag,
tag = baseTagName,
outbounds = JsonUtils.DeepCopy(proxyTags),
interrupt_exist_connections = false,
};
@ -529,12 +613,12 @@ public partial class CoreConfigSingboxService
}
// Merge results: first the selector/urltest/proxies, then other outbounds, and finally prev outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(singboxConfig.outbounds);
singboxConfig.outbounds = resultOutbounds;
singboxConfig.endpoints ??= new List<Endpoints4Sbox>();
resultEndpoints.AddRange(singboxConfig.endpoints);
singboxConfig.endpoints = resultEndpoints;
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(prevOutbounds)
.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
}
catch (Exception ex)
{
@ -574,4 +658,163 @@ public partial class CoreConfigSingboxService
}
return null;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{
var resultOutbounds = new List<Outbound4Sbox>();
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
var proxyTags = new List<string>(); // For selector and urltest outbounds
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node == null)
continue;
if (node.ConfigType.IsGroupType())
{
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsList(childProfiles, singboxConfig, profileGroupItem.MultipleLoad, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, singboxConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
if (ret == 0)
{
proxyTags.Add(childBaseTagName);
}
continue;
}
var server = await GenServer(node);
if (server is null)
{
break;
}
server.tag = baseTagName + (i + 1).ToString();
if (server is Endpoints4Sbox endpoint)
{
resultEndpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
resultOutbounds.Add(outbound);
}
proxyTags.Add(server.tag);
}
// Add urltest outbound (auto selection based on latency)
if (proxyTags.Count > 0)
{
var outUrltest = new Outbound4Sbox
{
type = "urltest",
tag = $"{baseTagName}-auto",
outbounds = proxyTags,
interrupt_exist_connections = false,
};
if (multipleLoad == EMultipleLoad.Fallback)
{
outUrltest.tolerance = 5000;
}
// Add selector outbound (manual selection)
var outSelector = new Outbound4Sbox
{
type = "selector",
tag = baseTagName,
outbounds = JsonUtils.DeepCopy(proxyTags),
interrupt_exist_connections = false,
};
outSelector.outbounds.Insert(0, outUrltest.tag);
// Insert these at the beginning
resultOutbounds.Insert(0, outUrltest);
resultOutbounds.Insert(0, outSelector);
}
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
return await Task.FromResult(0);
}
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, SingboxConfig singboxConfig, string baseTagName = Global.ProxyTag)
{
// Based on actual network flow instead of data packets
var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
var resultOutbounds = new List<Outbound4Sbox>();
var resultEndpoints = new List<Endpoints4Sbox>(); // For endpoints
for (var i = 0; i < nodesReverse.Count; i++)
{
var node = nodesReverse[i];
var server = await GenServer(node);
if (server is null)
{
break;
}
if (i == 0)
{
server.tag = baseTagName;
}
else
{
server.tag = baseTagName + i.ToString();
}
if (i != nodesReverse.Count - 1)
{
server.detour = baseTagName + (i + 1).ToString();
}
if (server is Endpoints4Sbox endpoint)
{
resultEndpoints.Add(endpoint);
}
else if (server is Outbound4Sbox outbound)
{
resultOutbounds.Add(outbound);
}
}
var serverList = new List<BaseServer4Sbox>();
serverList = serverList.Concat(resultOutbounds)
.Concat(resultEndpoints)
.ToList();
await AddRangeOutbounds(serverList, singboxConfig, baseTagName == Global.ProxyTag);
return await Task.FromResult(0);
}
private async Task<int> AddRangeOutbounds(List<BaseServer4Sbox> servers, SingboxConfig singboxConfig, bool prepend = true)
{
try
{
if (servers is null || servers.Count <= 0)
{
return 0;
}
var outbounds = servers.Where(s => s is Outbound4Sbox).Cast<Outbound4Sbox>().ToList();
var endpoints = servers.Where(s => s is Endpoints4Sbox).Cast<Endpoints4Sbox>().ToList();
singboxConfig.endpoints ??= new();
if (prepend)
{
singboxConfig.outbounds.InsertRange(0, outbounds);
singboxConfig.endpoints.InsertRange(0, endpoints);
}
else
{
singboxConfig.outbounds.AddRange(outbounds);
singboxConfig.endpoints.AddRange(endpoints);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
}

View file

@ -9,13 +9,13 @@ public partial class CoreConfigSingboxService
singboxConfig.route.final = Global.ProxyTag;
var item = _config.SimpleDNSItem;
var defaultDomainResolverTag = Global.SingboxOutboundResolverTag;
var defaultDomainResolverTag = Global.SingboxDirectDNSTag;
var directDNSStrategy = item.SingboxStrategy4Direct.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : item.SingboxStrategy4Direct;
var rawDNSItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (rawDNSItem != null && rawDNSItem.Enabled == true)
{
defaultDomainResolverTag = Global.SingboxFinalResolverTag;
defaultDomainResolverTag = Global.SingboxLocalDNSTag;
directDNSStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? Global.SingboxDomainStrategy4Out.FirstOrDefault() : rawDNSItem.DomainStrategy4Freedom;
}
singboxConfig.route.default_domain_resolver = new()
@ -71,6 +71,37 @@ public partial class CoreConfigSingboxService
});
}
var hostsDomains = new List<string>();
var dnsItem = await AppManager.Instance.GetDNSItem(ECoreType.sing_box);
if (dnsItem == null || dnsItem.Enabled == false)
{
var simpleDNSItem = _config.SimpleDNSItem;
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
foreach (var kvp in userHostsMap)
{
hostsDomains.Add(kvp.Key);
}
}
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHostsMap = Utils.GetSystemHosts();
foreach (var kvp in systemHostsMap)
{
hostsDomains.Add(kvp.Key);
}
}
}
if (hostsDomains.Count > 0)
{
singboxConfig.route.rules.Add(new()
{
action = "resolve",
domain = hostsDomains,
});
}
singboxConfig.route.rules.Add(new()
{
outbound = Global.DirectTag,
@ -105,13 +136,21 @@ public partial class CoreConfigSingboxService
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item1 in rules ?? [])
{
if (item1.Enabled)
if (!item1.Enabled)
{
await GenRoutingUserRule(item1, singboxConfig);
if (item1.Ip != null && item1.Ip.Count > 0)
{
ipRules.Add(item1);
}
continue;
}
if (item1.RuleType == ERuleType.DNS)
{
continue;
}
await GenRoutingUserRule(item1, singboxConfig);
if (item1.Ip?.Count > 0)
{
ipRules.Add(item1);
}
}
}
@ -337,19 +376,38 @@ public partial class CoreConfigSingboxService
}
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (node == null
|| !Global.SingboxSupportConfigType.Contains(node.ConfigType))
|| (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
&& !node.ConfigType.IsGroupType()))
{
return Global.ProxyTag;
}
var tag = $"{node.IndexId}-{Global.ProxyTag}";
if (singboxConfig.outbounds.Any(o => o.tag == tag)
|| (singboxConfig.endpoints != null && singboxConfig.endpoints.Any(e => e.tag == tag)))
{
return tag;
}
if (node.ConfigType.IsGroupType())
{
var ret = await GenGroupOutbound(node, singboxConfig, tag);
if (ret == 0)
{
return tag;
}
return Global.ProxyTag;
}
var server = await GenServer(node);
if (server is null)
{
return Global.ProxyTag;
}
server.tag = Global.ProxyTag + node.IndexId.ToString();
server.tag = tag;
if (server is Endpoints4Sbox endpoint)
{
singboxConfig.endpoints ??= new();

View file

@ -16,7 +16,7 @@ public partial class CoreConfigV2rayService(Config config)
try
{
if (node == null
|| node.Port <= 0)
|| !node.IsValid())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
@ -30,6 +30,18 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.InitialConfiguration;
if (node.ConfigType.IsGroupType())
{
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
return await GenerateClientMultipleLoadConfig(node);
case EConfigType.ProxyChain:
return await GenerateClientChainConfig(node);
}
}
var result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
if (result.IsNullOrEmpty())
{
@ -71,7 +83,7 @@ public partial class CoreConfigV2rayService(Config config)
}
}
public async Task<RetResult> GenerateClientMultipleLoadConfig(List<ProfileItem> selecteds, EMultipleLoad multipleLoad)
public async Task<RetResult> GenerateClientMultipleLoadConfig(ProfileItem parentNode)
{
var ret = new RetResult();
@ -99,70 +111,50 @@ public partial class CoreConfigV2rayService(Config config)
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
v2rayConfig.outbounds.RemoveAt(0);
var proxyProfiles = new List<ProfileItem>();
foreach (var it in selecteds)
{
if (!Global.XraySupportConfigType.Contains(it.ConfigType))
{
continue;
}
if (it.Port <= 0)
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (item is null)
{
continue;
}
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
{
if (item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInSingbox.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS && !Global.Flows.Contains(item.Flow))
{
continue;
}
//outbound
proxyProfiles.Add(item);
}
if (proxyProfiles.Count <= 0)
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenOutboundsList(proxyProfiles, v2rayConfig);
//add balancers
await GenBalancer(v2rayConfig, multipleLoad);
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
var balancer = v2rayConfig.routing.balancers.First();
var defaultBalancerTag = $"{Global.ProxyTag}{Global.BalancerTagSuffix}";
//add rule
var rules = v2rayConfig.routing.rules.Where(t => t.outboundTag == Global.ProxyTag).ToList();
if (rules?.Count > 0)
var rules = v2rayConfig.routing.rules;
if (rules?.Count > 0 && ((v2rayConfig.routing.balancers?.Count ?? 0) > 0))
{
var balancerTagSet = v2rayConfig.routing.balancers
.Select(b => b.tag)
.ToHashSet();
foreach (var rule in rules)
{
rule.outboundTag = null;
rule.balancerTag = balancer.tag;
if (rule.outboundTag == null)
continue;
if (balancerTagSet.Contains(rule.outboundTag))
{
rule.balancerTag = rule.outboundTag;
rule.outboundTag = null;
continue;
}
var outboundWithSuffix = rule.outboundTag + Global.BalancerTagSuffix;
if (balancerTagSet.Contains(outboundWithSuffix))
{
rule.balancerTag = outboundWithSuffix;
rule.outboundTag = null;
}
}
}
if (v2rayConfig.routing.domainStrategy == Global.IPIfNonMatch)
@ -170,7 +162,7 @@ public partial class CoreConfigV2rayService(Config config)
v2rayConfig.routing.rules.Add(new()
{
ip = ["0.0.0.0/0", "::/0"],
balancerTag = balancer.tag,
balancerTag = defaultBalancerTag,
type = "field"
});
}
@ -179,14 +171,71 @@ public partial class CoreConfigV2rayService(Config config)
v2rayConfig.routing.rules.Add(new()
{
network = "tcp,udp",
balancerTag = balancer.tag,
balancerTag = defaultBalancerTag,
type = "field"
});
}
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig, true);
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
}
public async Task<RetResult> GenerateClientChainConfig(ProfileItem parentNode)
{
var ret = new RetResult();
try
{
if (_config == null)
{
ret.Msg = ResUI.CheckServerSettings;
return ret;
}
ret.Msg = ResUI.InitialConfiguration;
string result = EmbedUtils.GetEmbedText(Global.V2raySampleClient);
string txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (result.IsNullOrEmpty() || txtOutbound.IsNullOrEmpty())
{
ret.Msg = ResUI.FailedGetDefaultConfiguration;
return ret;
}
var v2rayConfig = JsonUtils.Deserialize<V2rayConfig>(result);
if (v2rayConfig == null)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
v2rayConfig.outbounds.RemoveAt(0);
await GenLog(v2rayConfig);
await GenInbounds(v2rayConfig);
var groupRet = await GenGroupOutbound(parentNode, v2rayConfig);
if (groupRet != 0)
{
ret.Msg = ResUI.FailedGenDefaultConfiguration;
return ret;
}
await GenRouting(v2rayConfig);
await GenDns(null, v2rayConfig);
await GenStatistic(v2rayConfig);
ret.Success = true;
ret.Data = await ApplyFullConfigTemplate(v2rayConfig);
return ret;
}
catch (Exception ex)
@ -255,12 +304,9 @@ public partial class CoreConfigV2rayService(Config config)
continue;
}
var item = await AppManager.Instance.GetProfileItem(it.IndexId);
if (it.ConfigType is EConfigType.VMess or EConfigType.VLESS)
if (item is null || item.IsComplex() || !item.IsValid())
{
if (item is null || item.Id.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Id))
{
continue;
}
continue;
}
//find unused port
@ -289,28 +335,6 @@ public partial class CoreConfigV2rayService(Config config)
it.Port = port;
it.AllowTest = true;
//outbound
if (item is null)
{
continue;
}
if (item.ConfigType == EConfigType.Shadowsocks
&& !Global.SsSecuritiesInXray.Contains(item.Security))
{
continue;
}
if (item.ConfigType == EConfigType.VLESS
&& !Global.Flows.Contains(item.Flow))
{
continue;
}
if (it.ConfigType is EConfigType.VLESS or EConfigType.Trojan
&& item.StreamSecurity == Global.StreamSecurityReality
&& item.PublicKey.IsNullOrEmpty())
{
continue;
}
//inbound
Inbounds4Ray inbound = new()
{
@ -321,6 +345,7 @@ public partial class CoreConfigV2rayService(Config config)
inbound.tag = inbound.protocol + inbound.port.ToString();
v2rayConfig.inbounds.Add(inbound);
//outbound
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(item, outbound);
outbound.tag = Global.ProxyTag + inbound.port.ToString();
@ -354,7 +379,8 @@ public partial class CoreConfigV2rayService(Config config)
var ret = new RetResult();
try
{
if (node is not { Port: > 0 })
if (node == null
|| !node.IsValid())
{
ret.Msg = ResUI.CheckServerSettings;
return ret;

View file

@ -2,34 +2,87 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<int> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad)
private async Task<int> GenObservatory(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string baseTagName = Global.ProxyTag)
{
if (multipleLoad == EMultipleLoad.LeastPing)
// Collect all existing subject selectors from both observatories
var subjectSelectors = new List<string>();
subjectSelectors.AddRange(v2rayConfig.burstObservatory?.subjectSelector ?? []);
subjectSelectors.AddRange(v2rayConfig.observatory?.subjectSelector ?? []);
// Case 1: exact match already exists -> nothing to do
if (subjectSelectors.Any(baseTagName.StartsWith))
return await Task.FromResult(0);
// Case 2: prefix match exists -> reuse it and move to the first position
var matched = subjectSelectors.FirstOrDefault(s => s.StartsWith(baseTagName));
if (matched is not null)
{
var observatory = new Observatory4Ray
baseTagName = matched;
if (v2rayConfig.burstObservatory?.subjectSelector?.Contains(baseTagName) == true)
{
subjectSelector = [Global.ProxyTag],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
v2rayConfig.observatory = observatory;
v2rayConfig.burstObservatory.subjectSelector.Remove(baseTagName);
v2rayConfig.burstObservatory.subjectSelector.Insert(0, baseTagName);
}
if (v2rayConfig.observatory?.subjectSelector?.Contains(baseTagName) == true)
{
v2rayConfig.observatory.subjectSelector.Remove(baseTagName);
v2rayConfig.observatory.subjectSelector.Insert(0, baseTagName);
}
return await Task.FromResult(0);
}
else if (multipleLoad == EMultipleLoad.LeastLoad)
// Case 3: need to create or insert based on multipleLoad type
if (multipleLoad is EMultipleLoad.LeastLoad or EMultipleLoad.Fallback)
{
var burstObservatory = new BurstObservatory4Ray
if (v2rayConfig.burstObservatory is null)
{
subjectSelector = [Global.ProxyTag],
pingConfig = new()
// Create new burst observatory with default ping config
v2rayConfig.burstObservatory = new BurstObservatory4Ray
{
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
v2rayConfig.burstObservatory = burstObservatory;
subjectSelector = [baseTagName],
pingConfig = new()
{
destination = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
interval = "5m",
timeout = "30s",
sampling = 2,
}
};
}
else
{
v2rayConfig.burstObservatory.subjectSelector ??= new();
v2rayConfig.burstObservatory.subjectSelector.Add(baseTagName);
}
}
else if (multipleLoad is EMultipleLoad.LeastPing)
{
if (v2rayConfig.observatory is null)
{
// Create new observatory with default probe config
v2rayConfig.observatory = new Observatory4Ray
{
subjectSelector = [baseTagName],
probeUrl = AppManager.Instance.Config.SpeedTestItem.SpeedPingTestUrl,
probeInterval = "3m",
enableConcurrency = true,
};
}
else
{
v2rayConfig.observatory.subjectSelector ??= new();
v2rayConfig.observatory.subjectSelector.Add(baseTagName);
}
}
return await Task.FromResult(0);
}
private async Task<string> GenBalancer(V2rayConfig v2rayConfig, EMultipleLoad multipleLoad, string selector = Global.ProxyTag)
{
var strategyType = multipleLoad switch
{
EMultipleLoad.Random => "random",
@ -38,13 +91,22 @@ public partial class CoreConfigV2rayService
EMultipleLoad.LeastLoad => "leastLoad",
_ => "roundRobin",
};
var balancerTag = $"{selector}{Global.BalancerTagSuffix}";
var balancer = new BalancersItem4Ray
{
selector = [Global.ProxyTag],
strategy = new() { type = strategyType },
tag = $"{Global.ProxyTag}-round",
selector = [selector],
strategy = new()
{
type = strategyType,
settings = new()
{
expected = 1,
},
},
tag = balancerTag,
};
v2rayConfig.routing.balancers = [balancer];
return await Task.FromResult(0);
v2rayConfig.routing.balancers ??= new();
v2rayConfig.routing.balancers.Add(balancer);
return await Task.FromResult(balancerTag);
}
}

View file

@ -4,7 +4,7 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService
{
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig, bool handleBalancerAndRules = false)
private async Task<string> ApplyFullConfigTemplate(V2rayConfig v2rayConfig)
{
var fullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(ECoreType.Xray);
if (fullConfigTemplate == null || !fullConfigTemplate.Enabled || fullConfigTemplate.Config.IsNullOrEmpty())
@ -19,7 +19,7 @@ public partial class CoreConfigV2rayService
}
// Handle balancer and rules modifications (for multiple load scenarios)
if (handleBalancerAndRules && v2rayConfig.routing?.balancers?.Count > 0)
if (v2rayConfig.routing?.balancers?.Count > 0)
{
var balancer = v2rayConfig.routing.balancers.First();
@ -60,6 +60,34 @@ public partial class CoreConfigV2rayService
}
}
if (v2rayConfig.observatory != null)
{
if (fullConfigTemplateNode["observatory"] == null)
{
fullConfigTemplateNode["observatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.observatory));
}
else
{
var subjectSelector = v2rayConfig.observatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
}
}
if (v2rayConfig.burstObservatory != null)
{
if (fullConfigTemplateNode["burstObservatory"] == null)
{
fullConfigTemplateNode["burstObservatory"] = JsonNode.Parse(JsonUtils.Serialize(v2rayConfig.burstObservatory));
}
else
{
var subjectSelector = v2rayConfig.burstObservatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList()));
}
}
// Handle outbounds - append instead of override
var customOutboundsNode = fullConfigTemplateNode["outbounds"] is JsonArray outbounds ? outbounds : new JsonArray();
foreach (var outbound in v2rayConfig.outbounds)

View file

@ -142,6 +142,11 @@ public partial class CoreConfigV2rayService
continue;
}
if (item.RuleType == ERuleType.Routing)
{
continue;
}
foreach (var domain in item.Domain)
{
if (domain.StartsWith('#'))
@ -248,43 +253,21 @@ public partial class CoreConfigV2rayService
if (simpleDNSItem.UseSystemHosts == true)
{
var systemHosts = Utils.GetSystemHosts();
if (systemHosts.Count > 0)
var normalHost = v2rayConfig?.dns?.hosts;
if (normalHost != null && systemHosts?.Count > 0)
{
var normalHost = v2rayConfig.dns.hosts;
if (normalHost != null)
foreach (var host in systemHosts)
{
foreach (var host in systemHosts)
{
if (normalHost[host.Key] != null)
{
continue;
}
normalHost[host.Key] = new List<string> { host.Value };
}
normalHost.TryAdd(host.Key, new List<string> { host.Value });
}
}
}
var userHostsMap = simpleDNSItem.Hosts?
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line => !string.IsNullOrWhiteSpace(line))
.Where(line => line.Contains(' '))
.ToDictionary(
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
return parts[0];
},
line =>
{
var parts = line.Trim().Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
var values = parts.Skip(1).ToList();
return values;
}
);
if (userHostsMap != null)
if (!simpleDNSItem.Hosts.IsNullOrEmpty())
{
var userHostsMap = Utils.ParseHostsToDictionary(simpleDNSItem.Hosts);
foreach (var kvp in userHostsMap)
{
v2rayConfig.dns.hosts[kvp.Key] = kvp.Value;
@ -371,7 +354,7 @@ public partial class CoreConfigV2rayService
return 0;
}
private async Task<int> GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dNSItem)
private async Task<int> GenDnsDomainsCompatible(ProfileItem? node, JsonNode dns, DNSItem? dnsItem)
{
if (node == null)
{
@ -410,7 +393,7 @@ public partial class CoreConfigV2rayService
{
var dnsServer = new DnsServer4Ray()
{
address = string.IsNullOrEmpty(dNSItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dNSItem?.DomainDNSAddress,
address = string.IsNullOrEmpty(dnsItem?.DomainDNSAddress) ? Global.DomainPureIPDNSAddress.FirstOrDefault() : dnsItem?.DomainDNSAddress,
skipFallback = true,
domains = domainList
};

View file

@ -480,6 +480,60 @@ public partial class CoreConfigV2rayService
return 0;
}
private async Task<int> GenGroupOutbound(ProfileItem node, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag, bool ignoreOriginChain = false)
{
try
{
if (!node.ConfigType.IsGroupType())
{
return -1;
}
var hasCycle = ProfileGroupItemManager.HasCycle(node.IndexId);
if (hasCycle)
{
return -1;
}
var (childProfiles, profileGroupItem) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
return -1;
}
switch (node.ConfigType)
{
case EConfigType.PolicyGroup:
if (ignoreOriginChain)
{
await GenOutboundsList(childProfiles, v2rayConfig, baseTagName);
}
else
{
await GenOutboundsListWithChain(childProfiles, v2rayConfig, baseTagName);
}
break;
case EConfigType.ProxyChain:
await GenChainOutboundsList(childProfiles, v2rayConfig, baseTagName);
break;
default:
break;
}
//add balancers
if (node.ConfigType == EConfigType.PolicyGroup)
{
await GenObservatory(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
await GenBalancer(v2rayConfig, profileGroupItem.MultipleLoad, baseTagName);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
}
return await Task.FromResult(0);
}
private async Task<int> GenMoreOutbounds(ProfileItem node, V2rayConfig v2rayConfig)
{
//fragment proxy
@ -552,7 +606,7 @@ public partial class CoreConfigV2rayService
return 0;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig)
private async Task<int> GenOutboundsListWithChain(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{
try
{
@ -577,6 +631,25 @@ public partial class CoreConfigV2rayService
{
index++;
if (node.ConfigType.IsGroupType())
{
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{index}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue;
}
// Handle proxy chain
string? prevTag = null;
var currentOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
@ -590,7 +663,7 @@ public partial class CoreConfigV2rayService
// current proxy
await GenOutbound(node, currentOutbound);
currentOutbound.tag = $"{Global.ProxyTag}-{index}";
currentOutbound.tag = $"{baseTagName}-{index}";
if (!node.Subid.IsNullOrEmpty())
{
@ -606,7 +679,7 @@ public partial class CoreConfigV2rayService
{
var prevOutbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(prevNode, prevOutbound);
prevTag = $"prev-{Global.ProxyTag}-{++prevIndex}";
prevTag = $"prev-{baseTagName}-{++prevIndex}";
prevOutbound.tag = prevTag;
prevOutbounds.Add(prevOutbound);
}
@ -628,9 +701,17 @@ public partial class CoreConfigV2rayService
}
// Merge results: first the main chain outbounds, then other outbounds, and finally utility outbounds
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(prevOutbounds);
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(prevOutbounds);
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
}
catch (Exception ex)
{
@ -692,4 +773,110 @@ public partial class CoreConfigV2rayService
}
return null;
}
private async Task<int> GenOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{
var resultOutbounds = new List<Outbounds4Ray>();
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node == null)
continue;
if (node.ConfigType.IsGroupType())
{
var (childProfiles, _) = await ProfileGroupItemManager.GetChildProfileItems(node.IndexId);
if (childProfiles.Count <= 0)
{
continue;
}
var childBaseTagName = $"{baseTagName}-{i + 1}";
var ret = node.ConfigType switch
{
EConfigType.PolicyGroup =>
await GenOutboundsListWithChain(childProfiles, v2rayConfig, childBaseTagName),
EConfigType.ProxyChain =>
await GenChainOutboundsList(childProfiles, v2rayConfig, childBaseTagName),
_ => throw new NotImplementedException()
};
continue;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
break;
}
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var result = await GenOutbound(node, outbound);
if (result != 0)
{
break;
}
outbound.tag = baseTagName + (i + 1).ToString();
resultOutbounds.Add(outbound);
}
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0);
}
private async Task<int> GenChainOutboundsList(List<ProfileItem> nodes, V2rayConfig v2rayConfig, string baseTagName = Global.ProxyTag)
{
// Based on actual network flow instead of data packets
var nodesReverse = nodes.AsEnumerable().Reverse().ToList();
var resultOutbounds = new List<Outbounds4Ray>();
for (var i = 0; i < nodesReverse.Count; i++)
{
var node = nodesReverse[i];
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
if (txtOutbound.IsNullOrEmpty())
{
break;
}
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
var result = await GenOutbound(node, outbound);
if (result != 0)
{
break;
}
if (i == 0)
{
outbound.tag = baseTagName;
}
else
{
// avoid v2ray observe
outbound.tag = "chain-" + baseTagName + i.ToString();
}
if (i != nodesReverse.Count - 1)
{
outbound.streamSettings.sockopt = new()
{
dialerProxy = "chain-" + baseTagName + (i + 1).ToString()
};
}
resultOutbounds.Add(outbound);
}
if (baseTagName == Global.ProxyTag)
{
resultOutbounds.AddRange(v2rayConfig.outbounds);
v2rayConfig.outbounds = resultOutbounds;
}
else
{
v2rayConfig.outbounds.AddRange(resultOutbounds);
}
return await Task.FromResult(0);
}
}

View file

@ -20,11 +20,18 @@ public partial class CoreConfigV2rayService
var rules = JsonUtils.Deserialize<List<RulesItem>>(routing.RuleSet);
foreach (var item in rules)
{
if (item.Enabled)
if (!item.Enabled)
{
var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item));
await GenRoutingUserRule(item2, v2rayConfig);
continue;
}
if (item.RuleType == ERuleType.DNS)
{
continue;
}
var item2 = JsonUtils.Deserialize<RulesItem4Ray>(JsonUtils.Serialize(item));
await GenRoutingUserRule(item2, v2rayConfig);
}
}
}
@ -125,16 +132,34 @@ public partial class CoreConfigV2rayService
}
var node = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag);
if (node == null
|| !Global.XraySupportConfigType.Contains(node.ConfigType))
|| (!Global.XraySupportConfigType.Contains(node.ConfigType)
&& !node.ConfigType.IsGroupType()))
{
return Global.ProxyTag;
}
var tag = $"{node.IndexId}-{Global.ProxyTag}";
if (v2rayConfig.outbounds.Any(p => p.tag == tag))
{
return tag;
}
if (node.ConfigType.IsGroupType())
{
var ret = await GenGroupOutbound(node, v2rayConfig, tag);
if (ret == 0)
{
return tag;
}
return Global.ProxyTag;
}
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
await GenOutbound(node, outbound);
outbound.tag = Global.ProxyTag + node.IndexId.ToString();
outbound.tag = tag;
v2rayConfig.outbounds.Add(outbound);
return outbound.tag;

View file

@ -15,7 +15,7 @@ public class DownloadService
private static readonly string _tag = "DownloadService";
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Action<bool, string> updateFunc)
public async Task<int> DownloadDataAsync(string url, WebProxy webProxy, int downloadTimeout, Func<bool, string, Task> updateFunc)
{
try
{
@ -31,10 +31,10 @@ public class DownloadService
}
catch (Exception ex)
{
updateFunc?.Invoke(false, ex.Message);
await updateFunc?.Invoke(false, ex.Message);
if (ex.InnerException != null)
{
updateFunc?.Invoke(false, ex.InnerException.Message);
await updateFunc?.Invoke(false, ex.InnerException.Message);
}
}
return 0;

View file

@ -0,0 +1,183 @@
using System.Diagnostics;
using System.Text;
namespace ServiceLib.Services;
public class ProcessService : IDisposable
{
private readonly Process _process;
private readonly Func<bool, string, Task>? _updateFunc;
private bool _isDisposed;
public int Id => _process.Id;
public IntPtr Handle => _process.Handle;
public bool HasExited => _process.HasExited;
public ProcessService(
string fileName,
string arguments,
string workingDirectory,
bool displayLog,
bool redirectInput,
Dictionary<string, string>? environmentVars,
Func<bool, string, Task>? updateFunc)
{
_updateFunc = updateFunc;
_process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory,
UseShellExecute = false,
RedirectStandardInput = redirectInput,
RedirectStandardOutput = displayLog,
RedirectStandardError = displayLog,
CreateNoWindow = true,
StandardOutputEncoding = displayLog ? Encoding.UTF8 : null,
StandardErrorEncoding = displayLog ? Encoding.UTF8 : null,
},
EnableRaisingEvents = true
};
if (environmentVars != null)
{
foreach (var kv in environmentVars)
{
_process.StartInfo.Environment[kv.Key] = kv.Value;
}
}
if (displayLog)
{
RegisterEventHandlers();
}
}
public async Task StartAsync(string pwd = null)
{
_process.Start();
if (_process.StartInfo.RedirectStandardOutput)
{
_process.BeginOutputReadLine();
_process.BeginErrorReadLine();
}
if (_process.StartInfo.RedirectStandardInput)
{
await Task.Delay(10);
await _process.StandardInput.WriteLineAsync(pwd);
}
}
public async Task StopAsync()
{
if (_process.HasExited)
{
return;
}
try
{
if (_process.StartInfo.RedirectStandardOutput)
{
try
{
_process.CancelOutputRead();
}
catch { }
try
{
_process.CancelErrorRead();
}
catch { }
}
try
{
if (Utils.IsNonWindows())
{
_process.Kill(true);
}
}
catch { }
try
{
_process.Kill();
}
catch { }
await Task.Delay(100);
}
catch (Exception ex)
{
await _updateFunc?.Invoke(true, ex.Message);
}
}
private void RegisterEventHandlers()
{
void dataHandler(object sender, DataReceivedEventArgs e)
{
if (e.Data.IsNotEmpty())
{
_ = _updateFunc?.Invoke(false, e.Data + Environment.NewLine);
}
}
_process.OutputDataReceived += dataHandler;
_process.ErrorDataReceived += dataHandler;
_process.Exited += (s, e) =>
{
try
{
_process.OutputDataReceived -= dataHandler;
_process.ErrorDataReceived -= dataHandler;
}
catch
{
}
};
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
try
{
if (!_process.HasExited)
{
try
{
_process.CancelOutputRead();
}
catch { }
try
{
_process.CancelErrorRead();
}
catch { }
_process.Kill();
}
_process.Dispose();
}
catch (Exception ex)
{
_updateFunc?.Invoke(true, ex.Message);
}
_isDisposed = true;
GC.SuppressFinalize(this);
}
}

View file

@ -5,26 +5,20 @@ using System.Net.Sockets;
namespace ServiceLib.Services;
public class SpeedtestService
public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateFunc)
{
private static readonly string _tag = "SpeedtestService";
private Config? _config;
private Action<SpeedTestResult>? _updateFunc;
private readonly Config? _config = config;
private readonly Func<SpeedTestResult, Task>? _updateFunc = updateFunc;
private static readonly ConcurrentBag<string> _lstExitLoop = new();
public SpeedtestService(Config config, Action<SpeedTestResult> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
}
public void RunLoop(ESpeedActionType actionType, List<ProfileItem> selecteds)
{
Task.Run(async () =>
{
await RunAsync(actionType, selecteds);
await ProfileExManager.Instance.SaveTo();
UpdateFunc("", ResUI.SpeedtestingCompleted);
await UpdateFunc("", ResUI.SpeedtestingCompleted);
});
}
@ -43,7 +37,7 @@ public class SpeedtestService
var exitLoopKey = Utils.GetGuid(false);
_lstExitLoop.Add(exitLoopKey);
var lstSelected = GetClearItem(actionType, selecteds);
var lstSelected = await GetClearItem(actionType, selecteds);
switch (actionType)
{
@ -65,12 +59,12 @@ public class SpeedtestService
}
}
private List<ServerTestItem> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds)
private async Task<List<ServerTestItem>> GetClearItem(ESpeedActionType actionType, List<ProfileItem> selecteds)
{
var lstSelected = new List<ServerTestItem>();
foreach (var it in selecteds)
{
if (it.ConfigType == EConfigType.Custom)
if (it.ConfigType.IsComplexType())
{
continue;
}
@ -97,17 +91,17 @@ public class SpeedtestService
{
case ESpeedActionType.Tcping:
case ESpeedActionType.Realping:
UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
await UpdateFunc(it.IndexId, ResUI.Speedtesting, "");
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
break;
case ESpeedActionType.Speedtest:
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
case ESpeedActionType.Mixedtest:
UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
await UpdateFunc(it.IndexId, ResUI.Speedtesting, ResUI.SpeedtestingWait);
ProfileExManager.Instance.SetTestDelay(it.IndexId, 0);
ProfileExManager.Instance.SetTestSpeed(it.IndexId, 0);
break;
@ -122,10 +116,6 @@ public class SpeedtestService
List<Task> tasks = [];
foreach (var it in selecteds)
{
if (it.ConfigType == EConfigType.Custom)
{
continue;
}
tasks.Add(Task.Run(async () =>
{
try
@ -133,7 +123,7 @@ public class SpeedtestService
var responseTime = await GetTcpingTime(it.Address, it.Port);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
UpdateFunc(it.IndexId, responseTime.ToString());
await UpdateFunc(it.IndexId, responseTime.ToString());
}
catch (Exception ex)
{
@ -169,11 +159,11 @@ public class SpeedtestService
{
if (_lstExitLoop.Any(p => p == exitLoopKey) == false)
{
UpdateFunc("", ResUI.SpeedtestingSkip);
await UpdateFunc("", ResUI.SpeedtestingSkip);
return;
}
UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
await UpdateFunc("", string.Format(ResUI.SpeedtestingTestFailedPart, lstFailed.Count));
if (pageSizeNext > _config.SpeedTestItem.MixedConcurrencyCount)
{
@ -188,11 +178,11 @@ public class SpeedtestService
private async Task<bool> RunRealPingAsync(List<ServerTestItem> selecteds, string exitLoopKey)
{
var pid = -1;
ProcessService processService = null;
try
{
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
if (pid < 0)
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(selecteds);
if (processService is null)
{
return false;
}
@ -205,10 +195,6 @@ public class SpeedtestService
{
continue;
}
if (it.ConfigType == EConfigType.Custom)
{
continue;
}
tasks.Add(Task.Run(async () =>
{
await DoRealPing(it);
@ -222,9 +208,9 @@ public class SpeedtestService
}
finally
{
if (pid > 0)
if (processService != null)
{
await ProcUtils.ProcessKill(pid);
await processService?.StopAsync();
}
}
return true;
@ -239,24 +225,20 @@ public class SpeedtestService
{
if (_lstExitLoop.Any(p => p == exitLoopKey) == false)
{
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
continue;
}
if (it.ConfigType == EConfigType.Custom)
{
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
continue;
}
await concurrencySemaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
{
var pid = -1;
ProcessService processService = null;
try
{
pid = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (pid < 0)
processService = await CoreManager.Instance.LoadCoreConfigSpeedtest(it);
if (processService is null)
{
UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
await UpdateFunc(it.IndexId, "", ResUI.FailedToRunCore);
}
else
{
@ -270,7 +252,7 @@ public class SpeedtestService
}
else
{
UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
await UpdateFunc(it.IndexId, "", ResUI.SpeedtestingSkip);
}
}
}
@ -281,9 +263,9 @@ public class SpeedtestService
}
finally
{
if (pid > 0)
if (processService != null)
{
await ProcUtils.ProcessKill(pid);
await processService?.StopAsync();
}
concurrencySemaphore.Release();
}
@ -298,25 +280,25 @@ public class SpeedtestService
var responseTime = await HttpClientHelper.Instance.GetRealPingTime(_config.SpeedTestItem.SpeedPingTestUrl, webProxy, 10);
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
UpdateFunc(it.IndexId, responseTime.ToString());
await UpdateFunc(it.IndexId, responseTime.ToString());
return responseTime;
}
private async Task DoSpeedTest(DownloadService downloadHandle, ServerTestItem it)
{
UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
await UpdateFunc(it.IndexId, "", ResUI.Speedtesting);
var webProxy = new WebProxy($"socks5://{Global.Loopback}:{it.Port}");
var url = _config.SpeedTestItem.SpeedTestUrl;
var timeout = _config.SpeedTestItem.SpeedTestTimeout;
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, (success, msg) =>
await downloadHandle.DownloadDataAsync(url, webProxy, timeout, async (success, msg) =>
{
decimal.TryParse(msg, out var dec);
if (dec > 0)
{
ProfileExManager.Instance.SetTestSpeed(it.IndexId, dec);
}
UpdateFunc(it.IndexId, "", msg);
await UpdateFunc(it.IndexId, "", msg);
});
}
@ -357,7 +339,7 @@ public class SpeedtestService
{
List<List<ServerTestItem>> lstTest = new();
var lst1 = lstSelected.Where(t => Global.XraySupportConfigType.Contains(t.ConfigType)).ToList();
var lst2 = lstSelected.Where(t => Global.SingboxSupportConfigType.Contains(t.ConfigType) && !Global.XraySupportConfigType.Contains(t.ConfigType)).ToList();
var lst2 = lstSelected.Where(t => Global.SingboxOnlyConfigType.Contains(t.ConfigType)).ToList();
for (var num = 0; num < (int)Math.Ceiling(lst1.Count * 1.0 / pageSize); num++)
{
@ -371,9 +353,9 @@ public class SpeedtestService
return lstTest;
}
private void UpdateFunc(string indexId, string delay, string speed = "")
private async Task UpdateFunc(string indexId, string delay, string speed = "")
{
_updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed });
await _updateFunc?.Invoke(new() { IndexId = indexId, Delay = delay, Speed = speed });
if (indexId.IsNotEmpty() && speed.IsNotEmpty())
{
ProfileExManager.Instance.SetTestMessage(indexId, speed);

View file

@ -8,11 +8,11 @@ public class StatisticsSingboxService
private readonly Config _config;
private bool _exitFlag;
private ClientWebSocket? webSocket;
private Action<ServerSpeedItem>? _updateFunc;
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"ws://{Global.Loopback}:{AppManager.Instance.StatePort2}/traffic";
private static readonly string _tag = "StatisticsSingboxService";
public StatisticsSingboxService(Config config, Action<ServerSpeedItem> updateFunc)
public StatisticsSingboxService(Config config, Func<ServerSpeedItem, Task> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
@ -90,7 +90,7 @@ public class StatisticsSingboxService
{
ParseOutput(result, out var up, out var down);
_updateFunc?.Invoke(new ServerSpeedItem()
await _updateFunc?.Invoke(new ServerSpeedItem()
{
ProxyUp = (long)(up / 1000),
ProxyDown = (long)(down / 1000)

View file

@ -6,10 +6,10 @@ public class StatisticsXrayService
private ServerSpeedItem _serverSpeedItem = new();
private readonly Config _config;
private bool _exitFlag;
private Action<ServerSpeedItem>? _updateFunc;
private readonly Func<ServerSpeedItem, Task>? _updateFunc;
private string Url => $"{Global.HttpProtocol}{Global.Loopback}:{AppManager.Instance.StatePort}/debug/vars";
public StatisticsXrayService(Config config, Action<ServerSpeedItem> updateFunc)
public StatisticsXrayService(Config config, Func<ServerSpeedItem, Task> updateFunc)
{
_config = config;
_updateFunc = updateFunc;
@ -39,7 +39,7 @@ public class StatisticsXrayService
if (result != null)
{
var server = ParseOutput(result) ?? new ServerSpeedItem();
_updateFunc?.Invoke(server);
await _updateFunc?.Invoke(server);
}
}
catch

View file

@ -5,11 +5,11 @@ namespace ServiceLib.Services;
public class UpdateService
{
private Action<bool, string>? _updateFunc;
private Func<bool, string, Task>? _updateFunc;
private readonly int _timeout = 30;
private static readonly string _tag = "UpdateService";
public async Task CheckUpdateGuiN(Config config, Action<bool, string> updateFunc, bool preRelease)
public async Task CheckUpdateGuiN(Config config, Func<bool, string, Task> updateFunc, bool preRelease)
{
_updateFunc = updateFunc;
var url = string.Empty;
@ -20,25 +20,25 @@ public class UpdateService
{
if (args.Success)
{
_updateFunc?.Invoke(false, ResUI.MsgDownloadV2rayCoreSuccessfully);
_updateFunc?.Invoke(true, Utils.UrlEncode(fileName));
UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully);
UpdateFunc(true, Utils.UrlEncode(fileName));
}
else
{
_updateFunc?.Invoke(false, args.Msg);
UpdateFunc(false, args.Msg);
}
};
downloadHandle.Error += (sender2, args) =>
{
_updateFunc?.Invoke(false, args.GetException().Message);
UpdateFunc(false, args.GetException().Message);
};
_updateFunc?.Invoke(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN));
await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, ECoreType.v2rayN));
var result = await CheckUpdateAsync(downloadHandle, ECoreType.v2rayN, preRelease);
if (result.Success)
{
_updateFunc?.Invoke(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN));
_updateFunc?.Invoke(false, result.Msg);
await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, ECoreType.v2rayN));
await UpdateFunc(false, result.Msg);
url = result.Data?.ToString();
fileName = Utils.GetTempPath(Utils.GetGuid());
@ -46,11 +46,11 @@ public class UpdateService
}
else
{
_updateFunc?.Invoke(false, result.Msg);
await UpdateFunc(false, result.Msg);
}
}
public async Task CheckUpdateCore(ECoreType type, Config config, Action<bool, string> updateFunc, bool preRelease)
public async Task CheckUpdateCore(ECoreType type, Config config, Func<bool, string, Task> updateFunc, bool preRelease)
{
_updateFunc = updateFunc;
var url = string.Empty;
@ -61,34 +61,34 @@ public class UpdateService
{
if (args.Success)
{
_updateFunc?.Invoke(false, ResUI.MsgDownloadV2rayCoreSuccessfully);
_updateFunc?.Invoke(false, ResUI.MsgUnpacking);
UpdateFunc(false, ResUI.MsgDownloadV2rayCoreSuccessfully);
UpdateFunc(false, ResUI.MsgUnpacking);
try
{
_updateFunc?.Invoke(true, fileName);
UpdateFunc(true, fileName);
}
catch (Exception ex)
{
_updateFunc?.Invoke(false, ex.Message);
UpdateFunc(false, ex.Message);
}
}
else
{
_updateFunc?.Invoke(false, args.Msg);
UpdateFunc(false, args.Msg);
}
};
downloadHandle.Error += (sender2, args) =>
{
_updateFunc?.Invoke(false, args.GetException().Message);
UpdateFunc(false, args.GetException().Message);
};
_updateFunc?.Invoke(false, string.Format(ResUI.MsgStartUpdating, type));
await UpdateFunc(false, string.Format(ResUI.MsgStartUpdating, type));
var result = await CheckUpdateAsync(downloadHandle, type, preRelease);
if (result.Success)
{
_updateFunc?.Invoke(false, string.Format(ResUI.MsgParsingSuccessfully, type));
_updateFunc?.Invoke(false, result.Msg);
await UpdateFunc(false, string.Format(ResUI.MsgParsingSuccessfully, type));
await UpdateFunc(false, result.Msg);
url = result.Data?.ToString();
var ext = url.Contains(".tar.gz") ? ".tar.gz" : Path.GetExtension(url);
@ -99,17 +99,17 @@ public class UpdateService
{
if (!result.Msg.IsNullOrEmpty())
{
_updateFunc?.Invoke(false, result.Msg);
await UpdateFunc(false, result.Msg);
}
}
}
public async Task UpdateGeoFileAll(Config config, Action<bool, string> updateFunc)
public async Task UpdateGeoFileAll(Config config, Func<bool, string, Task> updateFunc)
{
await UpdateGeoFiles(config, updateFunc);
await UpdateOtherFiles(config, updateFunc);
await UpdateSrsFileAll(config, updateFunc);
_updateFunc?.Invoke(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
await UpdateFunc(true, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, "geo"));
}
#region CheckUpdate private
@ -128,7 +128,7 @@ public class UpdateService
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
_updateFunc?.Invoke(false, ex.Message);
await UpdateFunc(false, ex.Message);
return new RetResult(false, ex.Message);
}
}
@ -212,7 +212,7 @@ public class UpdateService
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
_updateFunc?.Invoke(false, ex.Message);
await UpdateFunc(false, ex.Message);
return new SemanticVersion("");
}
}
@ -272,7 +272,7 @@ public class UpdateService
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
_updateFunc?.Invoke(false, ex.Message);
await UpdateFunc(false, ex.Message);
return new RetResult(false, ex.Message);
}
}
@ -333,7 +333,7 @@ public class UpdateService
#region Geo private
private async Task UpdateGeoFiles(Config config, Action<bool, string> updateFunc)
private async Task UpdateGeoFiles(Config config, Func<bool, string, Task> updateFunc)
{
_updateFunc = updateFunc;
@ -352,7 +352,7 @@ public class UpdateService
}
}
private async Task UpdateOtherFiles(Config config, Action<bool, string> updateFunc)
private async Task UpdateOtherFiles(Config config, Func<bool, string, Task> updateFunc)
{
//If it is not in China area, no update is required
if (config.ConstItem.GeoSourceUrl.IsNotEmpty())
@ -371,7 +371,7 @@ public class UpdateService
}
}
private async Task UpdateSrsFileAll(Config config, Action<bool, string> updateFunc)
private async Task UpdateSrsFileAll(Config config, Func<bool, string, Task> updateFunc)
{
_updateFunc = updateFunc;
@ -426,7 +426,7 @@ public class UpdateService
}
}
private async Task UpdateSrsFile(string type, string srsName, Config config, Action<bool, string> updateFunc)
private async Task UpdateSrsFile(string type, string srsName, Config config, Func<bool, string, Task> updateFunc)
{
var srsUrl = string.IsNullOrEmpty(config.ConstItem.SrsSourceUrl)
? Global.SingboxRulesetUrl
@ -439,7 +439,7 @@ public class UpdateService
await DownloadGeoFile(url, fileName, targetPath, updateFunc);
}
private async Task DownloadGeoFile(string url, string fileName, string targetPath, Action<bool, string> updateFunc)
private async Task DownloadGeoFile(string url, string fileName, string targetPath, Func<bool, string, Task> updateFunc)
{
var tmpFileName = Utils.GetTempPath(Utils.GetGuid());
@ -448,7 +448,7 @@ public class UpdateService
{
if (args.Success)
{
_updateFunc?.Invoke(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName));
UpdateFunc(false, string.Format(ResUI.MsgDownloadGeoFileSuccessfully, fileName));
try
{
@ -457,26 +457,31 @@ public class UpdateService
File.Copy(tmpFileName, targetPath, true);
File.Delete(tmpFileName);
//_updateFunc?.Invoke(true, "");
//await UpdateFunc(true, "");
}
}
catch (Exception ex)
{
_updateFunc?.Invoke(false, ex.Message);
UpdateFunc(false, ex.Message);
}
}
else
{
_updateFunc?.Invoke(false, args.Msg);
UpdateFunc(false, args.Msg);
}
};
downloadHandle.Error += (sender2, args) =>
{
_updateFunc?.Invoke(false, args.GetException().Message);
UpdateFunc(false, args.GetException().Message);
};
await downloadHandle.DownloadFileAsync(url, tmpFileName, true, _timeout);
}
#endregion Geo private
private async Task UpdateFunc(bool notify, string msg)
{
await _updateFunc?.Invoke(notify, msg);
}
}

View file

@ -0,0 +1,238 @@
using System.Reactive;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.ViewModels;
public class AddGroupServerViewModel : MyReactiveObject
{
[Reactive]
public ProfileItem SelectedSource { get; set; }
[Reactive]
public ProfileItem SelectedChild { get; set; }
[Reactive]
public IList<ProfileItem> SelectedChildren { get; set; }
[Reactive]
public string? CoreType { get; set; }
[Reactive]
public string? PolicyGroupType { get; set; }
public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
public ReactiveCommand<Unit, Unit> MoveUpCmd { get; }
public ReactiveCommand<Unit, Unit> MoveDownCmd { get; }
public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddGroupServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue(
x => x.SelectedChild,
SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty());
RemoveCmd = ReactiveCommand.CreateFromTask(async () =>
{
await ChildRemoveAsync();
}, canEditRemove);
MoveTopCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Top);
}, canEditRemove);
MoveUpCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Up);
}, canEditRemove);
MoveDownCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Down);
}, canEditRemove);
MoveBottomCmd = ReactiveCommand.CreateFromTask(async () =>
{
await MoveServer(EMove.Bottom);
}, canEditRemove);
SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SaveServerAsync();
});
SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem);
CoreType = (SelectedSource?.CoreType ?? ECoreType.Xray).ToString();
ProfileGroupItemManager.Instance.TryGet(profileItem.IndexId, out var profileGroup);
PolicyGroupType = (profileGroup?.MultipleLoad ?? EMultipleLoad.LeastPing) switch
{
EMultipleLoad.LeastPing => ResUI.TbLeastPing,
EMultipleLoad.Fallback => ResUI.TbFallback,
EMultipleLoad.Random => ResUI.TbRandom,
EMultipleLoad.RoundRobin => ResUI.TbRoundRobin,
EMultipleLoad.LeastLoad => ResUI.TbLeastLoad,
_ => ResUI.TbLeastPing,
};
_ = Init();
}
public async Task Init()
{
var childItemMulti = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource?.IndexId);
if (childItemMulti != null)
{
var childIndexIds = childItemMulti.ChildItems.IsNullOrEmpty() ? new List<string>() : Utils.String2List(childItemMulti.ChildItems);
foreach (var item in childIndexIds)
{
var child = await AppManager.Instance.GetProfileItem(item);
if (child == null)
{
continue;
}
ChildItemsObs.Add(child);
}
}
}
public async Task ChildRemoveAsync()
{
if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
foreach (var it in SelectedChildren ?? [SelectedChild])
{
if (it != null)
{
ChildItemsObs.Remove(it);
}
}
await Task.CompletedTask;
}
public async Task MoveServer(EMove eMove)
{
if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
var index = ChildItemsObs.IndexOf(SelectedChild);
if (index < 0)
{
return;
}
var selectedChild = JsonUtils.DeepCopy(SelectedChild);
switch (eMove)
{
case EMove.Top:
if (index == 0)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Insert(0, selectedChild);
break;
case EMove.Up:
if (index == 0)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Insert(index - 1, selectedChild);
break;
case EMove.Down:
if (index == ChildItemsObs.Count - 1)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Insert(index + 1, selectedChild);
break;
case EMove.Bottom:
if (index == ChildItemsObs.Count - 1)
{
return;
}
ChildItemsObs.RemoveAt(index);
ChildItemsObs.Add(selectedChild);
break;
default:
break;
}
await Task.CompletedTask;
}
private async Task SaveServerAsync()
{
var remarks = SelectedSource.Remarks;
if (remarks.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.PleaseFillRemarks);
return;
}
if (ChildItemsObs.Count == 0)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseAddAtLeastOneServer);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? ECoreType.Xray : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType);
if (SelectedSource.CoreType is not (ECoreType.Xray or ECoreType.sing_box) ||
SelectedSource.ConfigType is not (EConfigType.ProxyChain or EConfigType.PolicyGroup))
{
return;
}
var childIndexIds = new List<string>();
foreach (var item in ChildItemsObs)
{
if (item.IndexId.IsNullOrEmpty())
{
continue;
}
childIndexIds.Add(item.IndexId);
}
var profileGroup = ProfileGroupItemManager.Instance.GetOrCreateAndMarkDirty(SelectedSource.IndexId);
profileGroup.ChildItems = Utils.List2String(childIndexIds);
profileGroup.MultipleLoad = PolicyGroupType switch
{
var s when s == ResUI.TbLeastPing => EMultipleLoad.LeastPing,
var s when s == ResUI.TbFallback => EMultipleLoad.Fallback,
var s when s == ResUI.TbRandom => EMultipleLoad.Random,
var s when s == ResUI.TbRoundRobin => EMultipleLoad.RoundRobin,
var s when s == ResUI.TbLeastLoad => EMultipleLoad.LeastLoad,
_ => EMultipleLoad.LeastPing,
};
var hasCycle = ProfileGroupItemManager.HasCycle(profileGroup.IndexId);
if (hasCycle)
{
NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks));
return;
}
if (await ConfigHandler.AddGroupServerCommon(_config, SelectedSource, profileGroup, true) == 0)
{
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null);
}
else
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
}
}
}

View file

@ -1,7 +1,6 @@
using System.Reactive;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
@ -136,8 +135,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
var result = await CreateZipFileFromDirectory(fileBackup);
if (result)
{
var service = Locator.Current.GetService<MainWindowViewModel>();
await service?.MyAppExitAsync(true);
await AppManager.Instance.AppExitAsync(false);
await SQLiteHelper.Instance.DisposeDbConnectionAsync();
var toPath = Utils.GetConfigPath();
@ -154,7 +152,7 @@ public class BackupAndRestoreViewModel : MyReactiveObject
_ = ProcUtils.ProcessStart(upgradeFileName, Global.RebootAs, Utils.StartupPath());
}
}
service?.Shutdown(true);
AppManager.Instance.Shutdown(true);
}
else
{

View file

@ -1,21 +1,21 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
public class CheckUpdateViewModel : MyReactiveObject
{
private const string _geo = "GeoFiles";
private string _v2rayN = ECoreType.v2rayN.ToString();
private readonly string _v2rayN = ECoreType.v2rayN.ToString();
private List<CheckUpdateModel> _lstUpdated = [];
private static readonly string _tag = "CheckUpdateViewModel";
private IObservableCollection<CheckUpdateModel> _checkUpdateModel = new ObservableCollectionExtended<CheckUpdateModel>();
public IObservableCollection<CheckUpdateModel> CheckUpdateModels => _checkUpdateModel;
public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>();
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; }
@ -24,9 +24,11 @@ public class CheckUpdateViewModel : MyReactiveObject
_config = AppManager.Instance.Config;
_updateView = updateView;
CheckUpdateCmd = ReactiveCommand.CreateFromTask(async () =>
CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate);
CheckUpdateCmd.ThrownExceptions.Subscribe(ex =>
{
await CheckUpdate();
Logging.SaveLog(_tag, ex);
_ = UpdateView(_v2rayN, ex.Message);
});
EnableCheckPreReleaseUpdate = _config.CheckUpdateItem.CheckPreReleaseUpdate;
@ -34,31 +36,41 @@ public class CheckUpdateViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.EnableCheckPreReleaseUpdate,
y => y == true)
.Subscribe(c => { _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate; });
.Subscribe(c => _config.CheckUpdateItem.CheckPreReleaseUpdate = EnableCheckPreReleaseUpdate);
RefreshCheckUpdateItems();
}
private void RefreshCheckUpdateItems()
{
_checkUpdateModel.Clear();
CheckUpdateModels.Clear();
if (RuntimeInformation.ProcessArchitecture != Architecture.X86)
{
_checkUpdateModel.Add(GetCheckUpdateModel(_v2rayN));
CheckUpdateModels.Add(GetCheckUpdateModel(_v2rayN));
//Not Windows and under Win10
if (!(Utils.IsWindows() && Environment.OSVersion.Version.Major < 10))
{
_checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.Xray.ToString()));
_checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString()));
_checkUpdateModel.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString()));
CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.Xray.ToString()));
CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.mihomo.ToString()));
CheckUpdateModels.Add(GetCheckUpdateModel(ECoreType.sing_box.ToString()));
}
}
_checkUpdateModel.Add(GetCheckUpdateModel(_geo));
CheckUpdateModels.Add(GetCheckUpdateModel(_geo));
}
private CheckUpdateModel GetCheckUpdateModel(string coreType)
{
if (coreType == _v2rayN && Utils.IsPackagedInstall())
{
return new()
{
IsSelected = false,
CoreType = coreType,
Remarks = ResUI.menuCheckUpdate + " (Not Support)",
};
}
return new()
{
IsSelected = _config.CheckUpdateItem.SelectedCoreTypes?.Contains(coreType) ?? true,
@ -69,7 +81,7 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task SaveSelectedCoreTypes()
{
_config.CheckUpdateItem.SelectedCoreTypes = _checkUpdateModel.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList();
_config.CheckUpdateItem.SelectedCoreTypes = CheckUpdateModels.Where(t => t.IsSelected == true).Select(t => t.CoreType ?? "").ToList();
await ConfigHandler.SaveConfig(_config);
}
@ -81,23 +93,30 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task CheckUpdateTask()
{
_lstUpdated.Clear();
_lstUpdated = _checkUpdateModel.Where(x => x.IsSelected == true)
_lstUpdated = CheckUpdateModels.Where(x => x.IsSelected == true)
.Select(x => new CheckUpdateModel() { CoreType = x.CoreType }).ToList();
await SaveSelectedCoreTypes();
for (var k = _checkUpdateModel.Count - 1; k >= 0; k--)
for (var k = CheckUpdateModels.Count - 1; k >= 0; k--)
{
var item = _checkUpdateModel[k];
var item = CheckUpdateModels[k];
if (item.IsSelected != true)
{
continue;
}
UpdateView(item.CoreType, "...");
await UpdateView(item.CoreType, "...");
if (item.CoreType == _geo)
{
await CheckUpdateGeo();
}
else if (item.CoreType == _v2rayN)
{
if (Utils.IsPackagedInstall())
{
await UpdateView(_v2rayN, "Not Support");
continue;
}
await CheckUpdateN(EnableCheckPreReleaseUpdate);
}
else if (item.CoreType == ECoreType.Xray.ToString())
@ -129,90 +148,90 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task CheckUpdateGeo()
{
void _updateUI(bool success, string msg)
async Task _updateUI(bool success, string msg)
{
UpdateView(_geo, msg);
await UpdateView(_geo, msg);
if (success)
{
UpdatedPlusPlus(_geo, "");
}
}
await (new UpdateService()).UpdateGeoFileAll(_config, _updateUI)
.ContinueWith(t =>
{
UpdatedPlusPlus(_geo, "");
});
await new UpdateService().UpdateGeoFileAll(_config, _updateUI)
.ContinueWith(t => UpdatedPlusPlus(_geo, ""));
}
private async Task CheckUpdateN(bool preRelease)
{
void _updateUI(bool success, string msg)
async Task _updateUI(bool success, string msg)
{
UpdateView(_v2rayN, msg);
await UpdateView(_v2rayN, msg);
if (success)
{
UpdateView(_v2rayN, ResUI.OperationSuccess);
await UpdateView(_v2rayN, ResUI.OperationSuccess);
UpdatedPlusPlus(_v2rayN, msg);
}
}
await (new UpdateService()).CheckUpdateGuiN(_config, _updateUI, preRelease)
.ContinueWith(t =>
{
UpdatedPlusPlus(_v2rayN, "");
});
await new UpdateService().CheckUpdateGuiN(_config, _updateUI, preRelease)
.ContinueWith(t => UpdatedPlusPlus(_v2rayN, ""));
}
private async Task CheckUpdateCore(CheckUpdateModel model, bool preRelease)
{
void _updateUI(bool success, string msg)
async Task _updateUI(bool success, string msg)
{
UpdateView(model.CoreType, msg);
await UpdateView(model.CoreType, msg);
if (success)
{
UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore);
await UpdateView(model.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfullyMore);
UpdatedPlusPlus(model.CoreType, msg);
}
}
var type = (ECoreType)Enum.Parse(typeof(ECoreType), model.CoreType);
await (new UpdateService()).CheckUpdateCore(type, _config, _updateUI, preRelease)
.ContinueWith(t =>
{
UpdatedPlusPlus(model.CoreType, "");
});
await new UpdateService().CheckUpdateCore(type, _config, _updateUI, preRelease)
.ContinueWith(t => UpdatedPlusPlus(model.CoreType, ""));
}
private async Task UpdateFinished()
{
if (_lstUpdated.Count > 0 && _lstUpdated.Count(x => x.IsFinished == true) == _lstUpdated.Count)
{
_updateView?.Invoke(EViewAction.DispatcherCheckUpdateFinished, false);
await UpdateFinishedSub(false);
await Task.Delay(2000);
await UpgradeCore();
if (_lstUpdated.Any(x => x.CoreType == _v2rayN && x.IsFinished == true))
{
await Task.Delay(1000);
UpgradeN();
await UpgradeN();
}
await Task.Delay(1000);
_updateView?.Invoke(EViewAction.DispatcherCheckUpdateFinished, true);
await UpdateFinishedSub(true);
}
}
public void UpdateFinishedResult(bool blReload)
private async Task UpdateFinishedSub(bool blReload)
{
RxApp.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) =>
{
_ = UpdateFinishedResult(blReload);
return Disposable.Empty;
});
}
public async Task UpdateFinishedResult(bool blReload)
{
if (blReload)
{
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
AppEvents.ReloadRequested.Publish();
}
else
{
Locator.Current.GetService<MainWindowViewModel>()?.CloseCore();
await CoreManager.Instance.CoreStop();
}
}
private void UpgradeN()
private async Task UpgradeN()
{
try
{
@ -221,16 +240,23 @@ public class CheckUpdateViewModel : MyReactiveObject
{
return;
}
if (!Utils.UpgradeAppExists(out _))
if (!Utils.UpgradeAppExists(out var upgradeFileName))
{
UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip);
await UpdateView(_v2rayN, ResUI.UpgradeAppNotExistTip);
NoticeManager.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip);
Logging.SaveLog("UpgradeApp does not exist");
return;
}
Locator.Current.GetService<MainWindowViewModel>()?.UpgradeApp(fileName);
var id = ProcUtils.ProcessStart(upgradeFileName, fileName, Utils.StartupPath());
if (id > 0)
{
await AppManager.Instance.AppExitAsync(true);
}
}
catch (Exception ex)
{
UpdateView(_v2rayN, ex.Message);
await UpdateView(_v2rayN, ex.Message);
}
}
@ -274,14 +300,14 @@ public class CheckUpdateViewModel : MyReactiveObject
if (Utils.IsNonWindows())
{
var filesList = (new DirectoryInfo(toPath)).GetFiles().Select(u => u.FullName).ToList();
var filesList = new DirectoryInfo(toPath).GetFiles().Select(u => u.FullName).ToList();
foreach (var file in filesList)
{
await Utils.SetLinuxChmod(Path.Combine(toPath, item.CoreType.ToLower()));
}
}
UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully);
await UpdateView(item.CoreType, ResUI.MsgUpdateV2rayCoreSuccessfully);
if (File.Exists(fileName))
{
@ -290,23 +316,28 @@ public class CheckUpdateViewModel : MyReactiveObject
}
}
private void UpdateView(string coreType, string msg)
private async Task UpdateView(string coreType, string msg)
{
var item = new CheckUpdateModel()
{
CoreType = coreType,
Remarks = msg,
};
_updateView?.Invoke(EViewAction.DispatcherCheckUpdate, item);
RxApp.MainThreadScheduler.Schedule(item, (scheduler, model) =>
{
_ = UpdateViewResult(model);
return Disposable.Empty;
});
}
public void UpdateViewResult(CheckUpdateModel model)
public async Task UpdateViewResult(CheckUpdateModel model)
{
var found = _checkUpdateModel.FirstOrDefault(t => t.CoreType == model.CoreType);
var found = CheckUpdateModels.FirstOrDefault(t => t.CoreType == model.CoreType);
if (found == null)
{
return;
var itemCopy = JsonUtils.DeepCopy(found);
itemCopy.Remarks = model.Remarks;
_checkUpdateModel.Replace(found, itemCopy);
}
found.Remarks = model.Remarks;
}
}

View file

@ -1,4 +1,5 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
@ -9,8 +10,7 @@ namespace ServiceLib.ViewModels;
public class ClashConnectionsViewModel : MyReactiveObject
{
private IObservableCollection<ClashConnectionModel> _connectionItems = new ObservableCollectionExtended<ClashConnectionModel>();
public IObservableCollection<ClashConnectionModel> ConnectionItems => _connectionItems;
public IObservableCollection<ClashConnectionModel> ConnectionItems { get; } = new ObservableCollectionExtended<ClashConnectionModel>();
[Reactive]
public ClashConnectionModel SelectedSource { get; set; }
@ -64,12 +64,16 @@ public class ClashConnectionsViewModel : MyReactiveObject
return;
}
_ = _updateView?.Invoke(EViewAction.DispatcherRefreshConnections, ret?.connections);
RxApp.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) =>
{
_ = RefreshConnections(model);
return Disposable.Empty;
});
}
public void RefreshConnections(List<ConnectionItem>? connections)
public async Task RefreshConnections(List<ConnectionItem>? connections)
{
_connectionItems.Clear();
ConnectionItems.Clear();
var dtNow = DateTime.Now;
var lstModel = new List<ClashConnectionModel>();
@ -99,7 +103,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
return;
}
_connectionItems.AddRange(lstModel);
ConnectionItems.AddRange(lstModel);
}
public async Task ClashConnectionClose(bool all)
@ -116,7 +120,7 @@ public class ClashConnectionsViewModel : MyReactiveObject
}
else
{
_connectionItems.Clear();
ConnectionItems.Clear();
}
await ClashApiManager.Instance.ClashConnectionClose(id);
await GetClashConnections();

View file

@ -1,4 +1,6 @@
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
@ -15,11 +17,8 @@ public class ClashProxiesViewModel : MyReactiveObject
private Dictionary<string, ProvidersItem>? _providers;
private readonly int _delayTimeout = 99999999;
private IObservableCollection<ClashProxyModel> _proxyGroups = new ObservableCollectionExtended<ClashProxyModel>();
private IObservableCollection<ClashProxyModel> _proxyDetails = new ObservableCollectionExtended<ClashProxyModel>();
public IObservableCollection<ClashProxyModel> ProxyGroups => _proxyGroups;
public IObservableCollection<ClashProxyModel> ProxyDetails => _proxyDetails;
public IObservableCollection<ClashProxyModel> ProxyGroups { get; } = new ObservableCollectionExtended<ClashProxyModel>();
public IObservableCollection<ClashProxyModel> ProxyDetails { get; } = new ObservableCollectionExtended<ClashProxyModel>();
[Reactive]
public ClashProxyModel SelectedGroup { get; set; }
@ -70,6 +69,8 @@ public class ClashProxiesViewModel : MyReactiveObject
SortingSelected = _config.ClashUIItem.ProxiesSorting;
RuleModeSelected = (int)_config.ClashUIItem.RuleMode;
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue(
x => x.SelectedGroup,
y => y != null && y.Name.IsNotEmpty())
@ -90,6 +91,17 @@ public class ClashProxiesViewModel : MyReactiveObject
y => y == true)
.Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
#endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ProxiesReloadRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await ProxiesReload());
#endregion AppEvents
_ = Init();
}
@ -168,11 +180,11 @@ public class ClashProxiesViewModel : MyReactiveObject
if (refreshUI)
{
_updateView?.Invoke(EViewAction.DispatcherRefreshProxyGroups, null);
RxApp.MainThreadScheduler.Schedule(() => _ = RefreshProxyGroups());
}
}
public void RefreshProxyGroups()
public async Task RefreshProxyGroups()
{
if (_proxies == null)
{
@ -180,7 +192,7 @@ public class ClashProxiesViewModel : MyReactiveObject
}
var selectedName = SelectedGroup?.Name;
_proxyGroups.Clear();
ProxyGroups.Clear();
var proxyGroups = ClashApiManager.Instance.GetClashProxyGroups();
if (proxyGroups != null && proxyGroups.Count > 0)
@ -196,7 +208,7 @@ public class ClashProxiesViewModel : MyReactiveObject
{
continue;
}
_proxyGroups.Add(new ClashProxyModel()
ProxyGroups.Add(new ClashProxyModel()
{
Now = item.now,
Name = item.name,
@ -212,12 +224,12 @@ public class ClashProxiesViewModel : MyReactiveObject
{
continue;
}
var item = _proxyGroups.FirstOrDefault(t => t.Name == kv.Key);
var item = ProxyGroups.FirstOrDefault(t => t.Name == kv.Key);
if (item != null && item.Name.IsNotEmpty())
{
continue;
}
_proxyGroups.Add(new ClashProxyModel()
ProxyGroups.Add(new ClashProxyModel()
{
Now = kv.Value.now,
Name = kv.Key,
@ -225,15 +237,15 @@ public class ClashProxiesViewModel : MyReactiveObject
});
}
if (_proxyGroups != null && _proxyGroups.Count > 0)
if (ProxyGroups != null && ProxyGroups.Count > 0)
{
if (selectedName != null && _proxyGroups.Any(t => t.Name == selectedName))
if (selectedName != null && ProxyGroups.Any(t => t.Name == selectedName))
{
SelectedGroup = _proxyGroups.FirstOrDefault(t => t.Name == selectedName);
SelectedGroup = ProxyGroups.FirstOrDefault(t => t.Name == selectedName);
}
else
{
SelectedGroup = _proxyGroups.First();
SelectedGroup = ProxyGroups.First();
}
}
else
@ -244,7 +256,7 @@ public class ClashProxiesViewModel : MyReactiveObject
private void RefreshProxyDetails(bool c)
{
_proxyDetails.Clear();
ProxyDetails.Clear();
if (!c)
{
return;
@ -297,7 +309,7 @@ public class ClashProxiesViewModel : MyReactiveObject
default:
break;
}
_proxyDetails.AddRange(lstDetails);
ProxyDetails.AddRange(lstDetails);
}
private ProxiesItem? TryGetProxy(string name)
@ -359,12 +371,12 @@ public class ClashProxiesViewModel : MyReactiveObject
await ClashApiManager.Instance.ClashSetActiveProxy(name, nameNode);
selectedProxy.now = nameNode;
var group = _proxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name);
var group = ProxyGroups.FirstOrDefault(it => it.Name == SelectedGroup.Name);
if (group != null)
{
group.Now = nameNode;
var group2 = JsonUtils.DeepCopy(group);
_proxyGroups.Replace(group, group2);
ProxyGroups.Replace(group, group2);
SelectedGroup = group2;
}
@ -373,22 +385,26 @@ public class ClashProxiesViewModel : MyReactiveObject
private async Task ProxiesDelayTest(bool blAll = true)
{
ClashApiManager.Instance.ClashProxiesDelayTest(blAll, _proxyDetails.ToList(), (item, result) =>
ClashApiManager.Instance.ClashProxiesDelayTest(blAll, ProxyDetails.ToList(), async (item, result) =>
{
if (item == null || result.IsNullOrEmpty())
{
return;
}
_updateView?.Invoke(EViewAction.DispatcherProxiesDelayTest, new SpeedTestResult() { IndexId = item.Name, Delay = result });
var model = new SpeedTestResult() { IndexId = item.Name, Delay = result };
RxApp.MainThreadScheduler.Schedule(model, (scheduler, model) =>
{
_ = ProxiesDelayTestResult(model);
return Disposable.Empty;
});
});
await Task.CompletedTask;
}
public void ProxiesDelayTestResult(SpeedTestResult result)
public async Task ProxiesDelayTestResult(SpeedTestResult result)
{
//UpdateHandler(false, $"{item.name}={result}");
var detail = _proxyDetails.FirstOrDefault(it => it.Name == result.IndexId);
var detail = ProxyDetails.FirstOrDefault(it => it.Name == result.IndexId);
if (detail == null)
{
return;
@ -410,7 +426,6 @@ public class ClashProxiesViewModel : MyReactiveObject
detail.Delay = _delayTimeout;
detail.DelayName = string.Empty;
}
_proxyDetails.Replace(detail, JsonUtils.DeepCopy(detail));
}
#endregion proxy function

View file

@ -1,4 +1,5 @@
using System.Reactive;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
@ -12,8 +13,6 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public bool? BlockBindingQuery { get; set; }
[Reactive] public string? DirectDNS { get; set; }
[Reactive] public string? RemoteDNS { get; set; }
[Reactive] public string? SingboxOutboundsResolveDNS { get; set; }
[Reactive] public string? SingboxFinalResolveDNS { get; set; }
[Reactive] public string? RayStrategy4Freedom { get; set; }
[Reactive] public string? SingboxStrategy4Direct { get; set; }
[Reactive] public string? SingboxStrategy4Proxy { get; set; }
@ -32,6 +31,8 @@ public class DNSSettingViewModel : MyReactiveObject
[Reactive] public bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public bool SBCustomDNSEnableCompatible { get; set; }
[ObservableAsProperty] public bool IsSimpleDNSEnabled { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; }
@ -55,6 +56,10 @@ public class DNSSettingViewModel : MyReactiveObject
await Task.CompletedTask;
});
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Select(x => !(x.Item1 && x.Item2))
.ToPropertyEx(this, x => x.IsSimpleDNSEnabled);
_ = Init();
}
@ -69,8 +74,6 @@ public class DNSSettingViewModel : MyReactiveObject
DirectDNS = item.DirectDNS;
RemoteDNS = item.RemoteDNS;
RayStrategy4Freedom = item.RayStrategy4Freedom;
SingboxOutboundsResolveDNS = item.SingboxOutboundsResolveDNS;
SingboxFinalResolveDNS = item.SingboxFinalResolveDNS;
SingboxStrategy4Direct = item.SingboxStrategy4Direct;
SingboxStrategy4Proxy = item.SingboxStrategy4Proxy;
Hosts = item.Hosts;
@ -100,8 +103,6 @@ public class DNSSettingViewModel : MyReactiveObject
_config.SimpleDNSItem.DirectDNS = DirectDNS;
_config.SimpleDNSItem.RemoteDNS = RemoteDNS;
_config.SimpleDNSItem.RayStrategy4Freedom = RayStrategy4Freedom;
_config.SimpleDNSItem.SingboxOutboundsResolveDNS = SingboxOutboundsResolveDNS;
_config.SimpleDNSItem.SingboxFinalResolveDNS = SingboxFinalResolveDNS;
_config.SimpleDNSItem.SingboxStrategy4Direct = SingboxStrategy4Direct;
_config.SimpleDNSItem.SingboxStrategy4Proxy = SingboxStrategy4Proxy;
_config.SimpleDNSItem.Hosts = Hosts;

View file

@ -1,7 +1,8 @@
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
@ -22,6 +23,8 @@ public class MainWindowViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddPolicyGroupServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddProxyChainServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaImageCmd { get; }
@ -121,6 +124,14 @@ public class MainWindowViewModel : MyReactiveObject
{
await AddServerAsync(true, EConfigType.Custom);
});
AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.PolicyGroup);
});
AddProxyChainServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(true, EConfigType.ProxyChain);
});
AddServerViaClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerViaClipboardAsync(null);
@ -183,7 +194,7 @@ public class MainWindowViewModel : MyReactiveObject
});
RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () =>
{
await RebootAsAdmin();
await AppManager.Instance.RebootAsAdmin();
});
ClearServerStatisticsCmd = ReactiveCommand.CreateFromTask(async () =>
{
@ -216,6 +227,30 @@ public class MainWindowViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ReloadRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await Reload());
AppEvents.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
AppEvents.AddServerViaClipboardRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null));
AppEvents.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
#endregion AppEvents
_ = Init();
}
@ -223,10 +258,11 @@ public class MainWindowViewModel : MyReactiveObject
{
_config.UiItem.ShowInTaskbar = true;
await ConfigHandler.InitBuiltinRouting(_config);
//await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinFullConfigTemplate(_config);
await ProfileExManager.Instance.Init();
await ProfileGroupItemManager.Instance.Init();
await CoreManager.Instance.Init(_config, UpdateHandler);
TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler);
@ -234,18 +270,17 @@ public class MainWindowViewModel : MyReactiveObject
{
await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler);
}
await RefreshServers();
BlReloadEnabled = true;
await Reload();
await AutoHideStartup();
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
}
#endregion Init
#region Actions
private void UpdateHandler(bool notify, string msg)
private async Task UpdateHandler(bool notify, string msg)
{
NoticeManager.Instance.SendMessage(msg);
if (notify)
@ -254,110 +289,47 @@ public class MainWindowViewModel : MyReactiveObject
}
}
private void UpdateTaskHandler(bool success, string msg)
private async Task UpdateTaskHandler(bool success, string msg)
{
NoticeManager.Instance.SendMessageEx(msg);
if (success)
{
var indexIdOld = _config.IndexId;
RefreshServers();
await RefreshServers();
if (indexIdOld != _config.IndexId)
{
_ = Reload();
await Reload();
}
if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{
_updateView?.Invoke(EViewAction.AdjustMainLvColWidth, null);
AppEvents.AdjustMainLvColWidthRequested.Publish();
}
}
}
private void UpdateStatisticsHandler(ServerSpeedItem update)
private async Task UpdateStatisticsHandler(ServerSpeedItem update)
{
if (!_config.UiItem.ShowInTaskbar)
{
return;
}
_updateView?.Invoke(EViewAction.DispatcherStatistics, update);
}
public void SetStatisticsResult(ServerSpeedItem update)
{
if (_config.GuiItem.DisplayRealTimeSpeed)
{
Locator.Current.GetService<StatusBarViewModel>()?.UpdateStatistics(update);
}
if (_config.GuiItem.EnableStatistics && (update.ProxyUp + update.ProxyDown) > 0 && DateTime.Now.Second % 9 == 0)
{
Locator.Current.GetService<ProfilesViewModel>()?.UpdateStatistics(update);
}
}
public async Task MyAppExitAsync(bool blWindowsShutDown)
{
try
{
Logging.SaveLog("MyAppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
MessageBus.Current.SendMessage("", EMsgCommand.AppExit.ToString());
await ConfigHandler.SaveConfig(_config);
await ProfileExManager.Instance.SaveTo();
await StatisticsManager.Instance.SaveTo();
await CoreManager.Instance.CoreStop();
StatisticsManager.Instance.Close();
Logging.SaveLog("MyAppExitAsync End");
}
catch { }
finally
{
if (!blWindowsShutDown)
{
_updateView?.Invoke(EViewAction.Shutdown, false);
}
}
}
public async Task UpgradeApp(string arg)
{
if (!Utils.UpgradeAppExists(out var upgradeFileName))
{
NoticeManager.Instance.SendMessageAndEnqueue(ResUI.UpgradeAppNotExistTip);
Logging.SaveLog("UpgradeApp does not exist");
return;
}
var id = ProcUtils.ProcessStart(upgradeFileName, arg, Utils.StartupPath());
if (id > 0)
{
await MyAppExitAsync(false);
}
}
public void ShowHideWindow(bool? blShow)
{
_updateView?.Invoke(EViewAction.ShowHideWindow, blShow);
}
public void Shutdown(bool byUser)
{
_updateView?.Invoke(EViewAction.Shutdown, byUser);
AppEvents.DispatcherStatisticsRequested.Publish(update);
}
#endregion Actions
#region Servers && Groups
private void RefreshServers()
private async Task RefreshServers()
{
MessageBus.Current.SendMessage("", EMsgCommand.RefreshProfiles.ToString());
AppEvents.ProfilesRefreshRequested.Publish();
await Task.Delay(200);
}
private void RefreshSubscriptions()
{
Locator.Current.GetService<ProfilesViewModel>()?.RefreshSubscriptions();
AppEvents.SubscriptionsRefreshRequested.Publish();
}
#endregion Servers && Groups
@ -378,13 +350,17 @@ public class MainWindowViewModel : MyReactiveObject
{
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item);
}
else if (eConfigType.IsGroupType())
{
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item);
}
else
{
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item);
}
if (ret == true)
{
RefreshServers();
await RefreshServers();
if (item.IndexId == _config.IndexId)
{
await Reload();
@ -399,11 +375,11 @@ public class MainWindowViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null);
return;
}
int ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false);
var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false);
if (ret > 0)
{
RefreshSubscriptions();
RefreshServers();
await RefreshServers();
NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret));
}
else
@ -449,11 +425,11 @@ public class MainWindowViewModel : MyReactiveObject
}
else
{
int ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false);
var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false);
if (ret > 0)
{
RefreshSubscriptions();
RefreshServers();
await RefreshServers();
NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan);
}
else
@ -477,7 +453,7 @@ public class MainWindowViewModel : MyReactiveObject
public async Task UpdateSubscriptionProcess(string subId, bool blProxy)
{
await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler);
await Task.Run(async () => await SubscriptionHandler.UpdateProcess(_config, subId, blProxy, UpdateTaskHandler));
}
#endregion Subscription
@ -489,7 +465,7 @@ public class MainWindowViewModel : MyReactiveObject
var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null);
if (ret == true)
{
Locator.Current.GetService<StatusBarViewModel>()?.InboundDisplayStatus();
AppEvents.InboundDisplayRequested.Publish();
await Reload();
}
}
@ -500,7 +476,7 @@ public class MainWindowViewModel : MyReactiveObject
if (ret == true)
{
await ConfigHandler.InitBuiltinRouting(_config);
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
AppEvents.RoutingsMenuRefreshRequested.Publish();
await Reload();
}
}
@ -523,16 +499,10 @@ public class MainWindowViewModel : MyReactiveObject
}
}
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await MyAppExitAsync(false);
}
private async Task ClearServerStatistics()
{
await StatisticsManager.Instance.ClearAllServerStatistics();
RefreshServers();
await RefreshServers();
}
private async Task OpenTheFileLocation()
@ -544,7 +514,7 @@ public class MainWindowViewModel : MyReactiveObject
}
else if (Utils.IsLinux())
{
ProcUtils.ProcessStart("nautilus", path);
ProcUtils.ProcessStart("xdg-open", path);
}
else if (Utils.IsOSX())
{
@ -568,15 +538,33 @@ public class MainWindowViewModel : MyReactiveObject
BlReloadEnabled = false;
var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId);
if (msgs.Count > 0)
{
foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
BlReloadEnabled = true;
return;
}
await Task.Run(async () =>
{
await LoadCore();
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
Locator.Current.GetService<StatusBarViewModel>()?.TestServerAvailability();
AppEvents.TestServerRequested.Publish();
_updateView?.Invoke(EViewAction.DispatcherReload, null);
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI));
BlReloadEnabled = true;
if (_hasNextReloadJob)
@ -586,17 +574,11 @@ public class MainWindowViewModel : MyReactiveObject
}
}
public void ReloadResult()
private void ReloadResult(bool showClashUI)
{
// BlReloadEnabled = true;
//Locator.Current.GetService<StatusBarViewModel>()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false);
ShowClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (ShowClashUI)
{
Locator.Current.GetService<ClashProxiesViewModel>()?.ProxiesReload();
}
else
{ TabMainSelectedIndex = 0; }
ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
}
private async Task LoadCore()
@ -605,21 +587,6 @@ public class MainWindowViewModel : MyReactiveObject
await CoreManager.Instance.LoadCore(node);
}
public async Task CloseCore()
{
await ConfigHandler.SaveConfig(_config);
await CoreManager.Instance.CoreStop();
}
private async Task AutoHideStartup()
{
if (_config.UiItem.AutoHideStartup)
{
ShowHideWindow(false);
}
await Task.CompletedTask;
}
#endregion core job
#region Presets
@ -628,10 +595,10 @@ public class MainWindowViewModel : MyReactiveObject
{
await ConfigHandler.ApplyRegionalPreset(_config, type);
await ConfigHandler.InitRouting(_config);
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
AppEvents.RoutingsMenuRefreshRequested.Publish();
await ConfigHandler.SaveConfig(_config);
await new UpdateService().UpdateGeoFileAll(_config, UpdateHandler);
await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler);
await Reload();
}

View file

@ -1,4 +1,6 @@
using System.Collections.Concurrent;
using System.Reactive.Linq;
using System.Text;
using System.Text.RegularExpressions;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
@ -7,10 +9,10 @@ namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject
{
private ConcurrentQueue<string> _queueMsg = new();
private int _numMaxMsg = 500;
private bool _lastMsgFilterNotAvailable;
private bool _blLockShow = false;
private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked
public int NumMaxMsg { get; } = 500;
[Reactive]
public string MsgFilter { get; set; }
@ -32,48 +34,52 @@ public class MsgViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.AutoRefresh,
y => y == true)
.Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; });
.Subscribe(c => _config.MsgUIItem.AutoRefresh = AutoRefresh);
MessageBus.Current.Listen<string>(EMsgCommand.SendMsgView.ToString()).Subscribe(OnNext);
}
private async void OnNext(string x)
{
await AppendQueueMsg(x);
AppEvents.SendMsgViewRequested
.AsObservable()
//.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(content => _ = AppendQueueMsg(content));
}
private async Task AppendQueueMsg(string msg)
{
//if (msg == Global.CommandClearMsg)
//{
// ClearMsg();
// return;
//}
if (AutoRefresh == false)
{
return;
}
_ = EnqueueQueueMsg(msg);
if (_blLockShow)
{
return;
}
EnqueueQueueMsg(msg);
if (!_config.UiItem.ShowInTaskbar)
{
return;
}
_blLockShow = true;
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
{
return;
}
await Task.Delay(500);
var txt = string.Join("", _queueMsg.ToArray());
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt);
try
{
await Task.Delay(500).ConfigureAwait(false);
_blLockShow = false;
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line))
{
sb.Append(line);
}
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString());
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
}
}
private async Task EnqueueQueueMsg(string msg)
private void EnqueueQueueMsg(string msg)
{
//filter msg
if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable)
@ -92,26 +98,17 @@ public class MsgViewModel : MyReactiveObject
}
}
//Enqueue
if (_queueMsg.Count > _numMaxMsg)
{
for (int k = 0; k < _queueMsg.Count - _numMaxMsg; k++)
{
_queueMsg.TryDequeue(out _);
}
}
_queueMsg.Enqueue(msg);
if (!msg.EndsWith(Environment.NewLine))
{
_queueMsg.Enqueue(Environment.NewLine);
}
await Task.CompletedTask;
}
public void ClearMsg()
{
_queueMsg.Clear();
}
//public void ClearMsg()
//{
// _queueMsg.Clear();
//}
private void DoMsgFilter()
{

View file

@ -0,0 +1,352 @@
using System.Reactive.Linq;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject
{
#region private prop
private string _serverFilter = string.Empty;
private Dictionary<string, bool> _dicHeaderSort = new();
private string _subIndexId = string.Empty;
// ConfigType filter state: default include-mode with all types selected
private List<EConfigType> _filterConfigTypes = new();
private bool _filterExclude = false;
#endregion private prop
#region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
public ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive]
public SubItem SelectedSub { get; set; }
[Reactive]
public string ServerFilter { get; set; }
// Include/Exclude filter for ConfigType
public List<EConfigType> FilterConfigTypes
{
get => _filterConfigTypes;
set => this.RaiseAndSetIfChanged(ref _filterConfigTypes, value);
}
[Reactive]
public bool FilterExclude
{
get => _filterExclude;
set => this.RaiseAndSetIfChanged(ref _filterExclude, value);
}
#endregion ObservableCollection
#region Init
public ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
{
_config = AppManager.Instance.Config;
_updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue(
x => x.SelectedSub,
y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id)
.Subscribe(async c => await SubSelectedChangedAsync(c));
this.WhenAnyValue(
x => x.ServerFilter,
y => y != null && _serverFilter != y)
.Subscribe(async c => await ServerFilterChanged(c));
// React to ConfigType filter changes
this.WhenAnyValue(x => x.FilterExclude)
.Skip(1)
.Subscribe(async _ => await RefreshServersBiz());
this.WhenAnyValue(x => x.FilterConfigTypes)
.Skip(1)
.Subscribe(async _ => await RefreshServersBiz());
#endregion WhenAnyValue && ReactiveCommand
_ = Init();
}
private async Task Init()
{
SelectedProfile = new();
SelectedSub = new();
// Default: include mode with all ConfigTypes selected
try
{
FilterExclude = false;
FilterConfigTypes = Enum.GetValues(typeof(EConfigType)).Cast<EConfigType>().ToList();
}
catch
{
FilterConfigTypes = new();
}
await RefreshSubscriptions();
await RefreshServers();
}
#endregion Init
#region Actions
public bool CanOk()
{
return SelectedProfile != null && !SelectedProfile.IndexId.IsNullOrEmpty();
}
public bool SelectFinish()
{
if (!CanOk())
{
return false;
}
_updateView?.Invoke(EViewAction.CloseWindow, null);
return true;
}
#endregion Actions
#region Servers && Groups
private async Task SubSelectedChangedAsync(bool c)
{
if (!c)
{
return;
}
_subIndexId = SelectedSub?.Id;
await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null);
}
private async Task ServerFilterChanged(bool c)
{
if (!c)
{
return;
}
_serverFilter = ServerFilter;
if (_serverFilter.IsNullOrEmpty())
{
await RefreshServers();
}
}
public async Task RefreshServers()
{
await RefreshServersBiz();
}
private async Task RefreshServersBiz()
{
var lstModel = await GetProfileItemsEx(_subIndexId, _serverFilter);
ProfileItems.Clear();
ProfileItems.AddRange(lstModel);
if (lstModel.Count > 0)
{
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
if (selected != null)
{
SelectedProfile = selected;
}
else
{
SelectedProfile = lstModel.First();
}
}
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
public async Task RefreshSubscriptions()
{
SubItems.Clear();
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
foreach (var item in await AppManager.Instance.SubItems())
{
SubItems.Add(item);
}
if (_subIndexId != null && SubItems.FirstOrDefault(t => t.Id == _subIndexId) != null)
{
SelectedSub = SubItems.FirstOrDefault(t => t.Id == _subIndexId);
}
else
{
SelectedSub = SubItems.First();
}
}
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{
var lstModel = await AppManager.Instance.ProfileItems(_subIndexId, filter);
lstModel = (from t in lstModel
select new ProfileItemModel
{
IndexId = t.IndexId,
ConfigType = t.ConfigType,
Remarks = t.Remarks,
Address = t.Address,
Port = t.Port,
Security = t.Security,
Network = t.Network,
StreamSecurity = t.StreamSecurity,
Subid = t.Subid,
SubRemarks = t.SubRemarks,
IsActive = t.IndexId == _config.IndexId,
}).OrderBy(t => t.Sort).ToList();
// Apply ConfigType filter (include or exclude)
if (FilterConfigTypes != null && FilterConfigTypes.Count > 0)
{
if (FilterExclude)
{
lstModel = lstModel.Where(t => !FilterConfigTypes.Contains(t.ConfigType)).ToList();
}
else
{
lstModel = lstModel.Where(t => FilterConfigTypes.Contains(t.ConfigType)).ToList();
}
}
return lstModel;
}
public async Task<ProfileItem?> GetProfileItem()
{
if (string.IsNullOrEmpty(SelectedProfile?.IndexId))
{
return null;
}
var indexId = SelectedProfile.IndexId;
var item = await AppManager.Instance.GetProfileItem(indexId);
if (item is null)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return null;
}
return item;
}
public async Task<List<ProfileItem>?> GetProfileItems()
{
if (SelectedProfiles == null || SelectedProfiles.Count == 0)
{
return null;
}
var lst = new List<ProfileItem>();
foreach (var sp in SelectedProfiles)
{
if (string.IsNullOrEmpty(sp?.IndexId))
{
continue;
}
var item = await AppManager.Instance.GetProfileItem(sp.IndexId);
if (item != null)
{
lst.Add(item);
}
}
if (lst.Count == 0)
{
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return null;
}
return lst;
}
public void SortServer(string colName)
{
if (colName.IsNullOrEmpty())
{
return;
}
var prop = typeof(ProfileItemModel).GetProperty(colName);
if (prop == null)
{
return;
}
_dicHeaderSort.TryAdd(colName, true);
var asc = _dicHeaderSort[colName];
var comparer = Comparer<object?>.Create((a, b) =>
{
if (ReferenceEquals(a, b))
{
return 0;
}
if (a is null)
{
return -1;
}
if (b is null)
{
return 1;
}
if (a.GetType() == b.GetType() && a is IComparable ca)
{
return ca.CompareTo(b);
}
return string.Compare(a.ToString(), b.ToString(), StringComparison.OrdinalIgnoreCase);
});
object? KeySelector(ProfileItemModel x)
{
return prop.GetValue(x);
}
IEnumerable<ProfileItemModel> sorted = asc
? ProfileItems.OrderBy(KeySelector, comparer)
: ProfileItems.OrderByDescending(KeySelector, comparer);
var list = sorted.ToList();
ProfileItems.Clear();
ProfileItems.AddRange(list);
_dicHeaderSort[colName] = !asc;
return;
}
#endregion Servers && Groups
#region Public API
// External setter for ConfigType filter
public void SetConfigTypeFilter(IEnumerable<EConfigType> types, bool exclude = false)
{
FilterConfigTypes = types?.Distinct().ToList() ?? new List<EConfigType>();
FilterExclude = exclude;
}
#endregion Public API
}

View file

@ -1,11 +1,11 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
@ -22,13 +22,9 @@ public class ProfilesViewModel : MyReactiveObject
#region ObservableCollection
private IObservableCollection<ProfileItemModel> _profileItems = new ObservableCollectionExtended<ProfileItemModel>();
public IObservableCollection<ProfileItemModel> ProfileItems => _profileItems;
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
private IObservableCollection<SubItem> _subItems = new ObservableCollectionExtended<SubItem>();
public IObservableCollection<SubItem> SubItems => _subItems;
private IObservableCollection<ComboItem> _servers = new ObservableCollectionExtended<ComboItem>();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
public ProfileItemModel SelectedProfile { get; set; }
@ -41,15 +37,9 @@ public class ProfilesViewModel : MyReactiveObject
[Reactive]
public SubItem SelectedMoveToGroup { get; set; }
[Reactive]
public ComboItem SelectedServer { get; set; }
[Reactive]
public string ServerFilter { get; set; }
[Reactive]
public bool BlServers { get; set; }
#endregion ObservableCollection
#region Menu
@ -62,11 +52,13 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> CopyServerCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; }
public ReactiveCommand<Unit, Unit> ShareServerCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayRandomCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayRoundRobinCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerXrayLeastLoadCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultMultipleServerSingBoxLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRandomCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayRoundRobinCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayLeastLoadCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerXrayFallbackCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxLeastPingCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupMultipleServerSingBoxFallbackCmd { get; }
//servers move
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@ -118,15 +110,10 @@ public class ProfilesViewModel : MyReactiveObject
y => y != null && !y.Remarks.IsNullOrEmpty())
.Subscribe(async c => await MoveToGroup(c));
this.WhenAnyValue(
x => x.SelectedServer,
y => y != null && !y.Text.IsNullOrEmpty())
.Subscribe(async c => await ServerSelectedChanged(c));
this.WhenAnyValue(
x => x.ServerFilter,
y => y != null && _serverFilter != y)
.Subscribe(c => ServerFilterChanged(c));
.Subscribe(async c => await ServerFilterChanged(c));
//servers delete
EditServerCmd = ReactiveCommand.CreateFromTask(async () =>
@ -153,25 +140,33 @@ public class ProfilesViewModel : MyReactiveObject
{
await ShareServerAsync();
}, canEditRemove);
SetDefaultMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.Random);
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random);
}, canEditRemove);
SetDefaultMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin);
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin);
}, canEditRemove);
SetDefaultMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing);
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing);
}, canEditRemove);
SetDefaultMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SetDefaultMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad);
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad);
}, canEditRemove);
SetDefaultMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SetDefaultMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing);
await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback);
}, canEditRemove);
GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing);
}, canEditRemove);
GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback);
}, canEditRemove);
//servers move
@ -247,10 +242,29 @@ public class ProfilesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand
if (_updateView != null)
{
MessageBus.Current.Listen<string>(EMsgCommand.RefreshProfiles.ToString()).Subscribe(OnNext);
}
#region AppEvents
AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz());
AppEvents.SubscriptionsRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshSubscriptions());
AppEvents.DispatcherStatisticsRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result));
AppEvents.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async indexId => await SetDefaultServer(indexId));
#endregion AppEvents
_ = Init();
}
@ -260,27 +274,21 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = new();
SelectedSub = new();
SelectedMoveToGroup = new();
SelectedServer = new();
await RefreshSubscriptions();
RefreshServers();
//await RefreshServers();
}
#endregion Init
#region Actions
private async void OnNext(string x)
{
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
private void Reload()
{
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
AppEvents.ReloadRequested.Publish();
}
public void SetSpeedTestResult(SpeedTestResult result)
public async Task SetSpeedTestResult(SpeedTestResult result)
{
if (result.IndexId.IsNullOrEmpty())
{
@ -288,7 +296,7 @@ public class ProfilesViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(result.Delay);
return;
}
var item = _profileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId);
if (item == null)
{
return;
@ -304,31 +312,26 @@ public class ProfilesViewModel : MyReactiveObject
{
item.SpeedVal = result.Speed ?? string.Empty;
}
//_profileItems.Replace(item, JsonUtils.DeepCopy(item));
}
public void UpdateStatistics(ServerSpeedItem update)
public async Task UpdateStatistics(ServerSpeedItem update)
{
if (!_config.GuiItem.EnableStatistics
|| (update.ProxyUp + update.ProxyDown) <= 0
|| DateTime.Now.Second % 3 != 0)
{
return;
}
try
{
var item = _profileItems.FirstOrDefault(it => it.IndexId == update.IndexId);
var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId);
if (item != null)
{
item.TodayDown = Utils.HumanFy(update.TodayDown);
item.TodayUp = Utils.HumanFy(update.TodayUp);
item.TotalDown = Utils.HumanFy(update.TotalDown);
item.TotalUp = Utils.HumanFy(update.TotalUp);
//if (SelectedProfile?.IndexId == item.IndexId)
//{
// var temp = JsonUtils.DeepCopy(item);
// _profileItems.Replace(item, temp);
// SelectedProfile = temp;
//}
//else
//{
// _profileItems.Replace(item, JsonUtils.DeepCopy(item));
//}
}
}
catch
@ -336,11 +339,6 @@ public class ProfilesViewModel : MyReactiveObject
}
}
public async Task AutofitColumnWidthAsync()
{
await _updateView?.Invoke(EViewAction.AdjustMainLvColWidth, null);
}
#endregion Actions
#region Servers && Groups
@ -353,12 +351,12 @@ public class ProfilesViewModel : MyReactiveObject
}
_config.SubIndexId = SelectedSub?.Id;
RefreshServers();
await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null);
}
private void ServerFilterChanged(bool c)
private async Task ServerFilterChanged(bool c)
{
if (!c)
{
@ -367,22 +365,24 @@ public class ProfilesViewModel : MyReactiveObject
_serverFilter = ServerFilter;
if (_serverFilter.IsNullOrEmpty())
{
RefreshServers();
await RefreshServers();
}
}
public void RefreshServers()
public async Task RefreshServers()
{
MessageBus.Current.SendMessage("", EMsgCommand.RefreshProfiles.ToString());
AppEvents.ProfilesRefreshRequested.Publish();
await Task.Delay(200);
}
public async Task RefreshServersBiz()
private async Task RefreshServersBiz()
{
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
_profileItems.Clear();
_profileItems.AddRange(lstModel);
ProfileItems.Clear();
ProfileItems.AddRange(lstModel);
if (lstModel.Count > 0)
{
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
@ -395,25 +395,27 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = lstModel.First();
}
}
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
public async Task RefreshSubscriptions()
private async Task RefreshSubscriptions()
{
_subItems.Clear();
SubItems.Clear();
_subItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
SubItems.Add(new SubItem { Remarks = ResUI.AllGroupServers });
foreach (var item in await AppManager.Instance.SubItems())
{
_subItems.Add(item);
SubItems.Add(item);
}
if (_config.SubIndexId != null && _subItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null)
if (_config.SubIndexId != null && SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId) != null)
{
SelectedSub = _subItems.FirstOrDefault(t => t.Id == _config.SubIndexId);
SelectedSub = SubItems.FirstOrDefault(t => t.Id == _config.SubIndexId);
}
else
{
SelectedSub = _subItems.First();
SelectedSub = SubItems.First();
}
}
@ -508,13 +510,17 @@ public class ProfilesViewModel : MyReactiveObject
{
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item);
}
else if (eConfigType.IsGroupType())
{
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item);
}
else
{
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item);
}
if (ret == true)
{
RefreshServers();
await RefreshServers();
if (item.IndexId == _config.IndexId)
{
Reload();
@ -537,11 +543,11 @@ public class ProfilesViewModel : MyReactiveObject
await ConfigHandler.RemoveServers(_config, lstSelected);
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
if (lstSelected.Count == _profileItems.Count)
if (lstSelected.Count == ProfileItems.Count)
{
_profileItems.Clear();
ProfileItems.Clear();
}
RefreshServers();
await RefreshServers();
if (exists)
{
Reload();
@ -553,7 +559,7 @@ public class ProfilesViewModel : MyReactiveObject
var tuple = await ConfigHandler.DedupServerList(_config, _config.SubIndexId);
if (tuple.Item1 > 0 || tuple.Item2 > 0)
{
RefreshServers();
await RefreshServers();
Reload();
}
NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2));
@ -568,7 +574,7 @@ public class ProfilesViewModel : MyReactiveObject
}
if (await ConfigHandler.CopyServer(_config, lstSelected) == 0)
{
RefreshServers();
await RefreshServers();
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
}
}
@ -582,7 +588,7 @@ public class ProfilesViewModel : MyReactiveObject
await SetDefaultServer(SelectedProfile.IndexId);
}
public async Task SetDefaultServer(string? indexId)
private async Task SetDefaultServer(string? indexId)
{
if (indexId.IsNullOrEmpty())
{
@ -601,24 +607,11 @@ public class ProfilesViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0)
{
RefreshServers();
await RefreshServers();
Reload();
}
}
private async Task ServerSelectedChanged(bool c)
{
if (!c)
{
return;
}
if (SelectedServer == null || SelectedServer.ID.IsNullOrEmpty())
{
return;
}
await SetDefaultServer(SelectedServer.ID);
}
public async Task ShareServerAsync()
{
var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId);
@ -636,7 +629,7 @@ public class ProfilesViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.ShareServer, url);
}
private async Task SetDefaultMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad)
private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad)
{
var lstSelected = await GetProfileItems(true);
if (lstSelected == null)
@ -644,7 +637,7 @@ public class ProfilesViewModel : MyReactiveObject
return;
}
var ret = await ConfigHandler.AddCustomServer4Multiple(_config, lstSelected, coreType, multipleLoad);
var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id);
if (ret.Success != true)
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
@ -652,7 +645,7 @@ public class ProfilesViewModel : MyReactiveObject
}
if (ret?.Data?.ToString() == _config.IndexId)
{
RefreshServers();
await RefreshServers();
Reload();
}
else
@ -675,13 +668,13 @@ public class ProfilesViewModel : MyReactiveObject
return;
}
_dicHeaderSort[colName] = !asc;
RefreshServers();
await RefreshServers();
}
public async Task RemoveInvalidServerResult()
{
var count = await ConfigHandler.RemoveInvalidServerResult(_config, _config.SubIndexId);
RefreshServers();
await RefreshServers();
NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count));
}
@ -702,7 +695,7 @@ public class ProfilesViewModel : MyReactiveObject
await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id);
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
RefreshServers();
await RefreshServers();
SelectedMoveToGroup = null;
SelectedMoveToGroup = new();
}
@ -723,18 +716,18 @@ public class ProfilesViewModel : MyReactiveObject
}
if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0)
{
RefreshServers();
await RefreshServers();
}
}
public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem)
{
var targetIndex = _profileItems.IndexOf(targetItem);
var targetIndex = ProfileItems.IndexOf(targetItem);
if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex)
{
if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0)
{
RefreshServers();
await RefreshServers();
}
}
}
@ -743,7 +736,7 @@ public class ProfilesViewModel : MyReactiveObject
{
if (actionType == ESpeedActionType.Mixedtest)
{
SelectedProfiles = _profileItems;
SelectedProfiles = ProfileItems;
}
var lstSelected = await GetProfileItems(false);
if (lstSelected == null)
@ -751,7 +744,14 @@ public class ProfilesViewModel : MyReactiveObject
return;
}
_speedtestService ??= new SpeedtestService(_config, (SpeedTestResult result) => _updateView?.Invoke(EViewAction.DispatcherSpeedTest, result));
_speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) =>
{
RxApp.MainThreadScheduler.Schedule(result, (scheduler, result) =>
{
_ = SetSpeedTestResult(result);
return Disposable.Empty;
});
});
_speedtestService?.RunLoop(actionType, lstSelected);
}
@ -768,6 +768,18 @@ public class ProfilesViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer);
return;
}
var msgs = await ActionPrecheckManager.Instance.Check(item);
if (msgs.Count > 0)
{
foreach (var msg in msgs)
{
NoticeManager.Instance.SendMessage(msg);
}
NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true));
return;
}
if (blClipboard)
{
var result = await CoreConfigHandler.GenerateClientConfig(item, null);

View file

@ -21,6 +21,9 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
[Reactive]
public string Process { get; set; }
[Reactive]
public string? RuleType { get; set; }
[Reactive]
public bool AutoSort { get; set; }
@ -51,6 +54,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
Domain = Utils.List2String(SelectedSource.Domain, true);
IP = Utils.List2String(SelectedSource.Ip, true);
Process = Utils.List2String(SelectedSource.Process, true);
RuleType = SelectedSource.RuleType?.ToString();
}
private async Task SaveRulesAsync()
@ -73,6 +77,7 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
}
SelectedSource.Protocol = ProtocolItems?.ToList();
SelectedSource.InboundTag = InboundTagItems?.ToList();
SelectedSource.RuleType = RuleType.IsNullOrEmpty() ? null : (ERuleType)Enum.Parse(typeof(ERuleType), RuleType);
var hasRule = SelectedSource.Domain?.Count > 0
|| SelectedSource.Ip?.Count > 0

View file

@ -14,8 +14,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
[Reactive]
public RoutingItem SelectedRouting { get; set; }
private IObservableCollection<RulesItemModel> _rulesItems = new ObservableCollectionExtended<RulesItemModel>();
public IObservableCollection<RulesItemModel> RulesItems => _rulesItems;
public IObservableCollection<RulesItemModel> RulesItems { get; } = new ObservableCollectionExtended<RulesItemModel>();
[Reactive]
public RulesItemModel SelectedSource { get; set; }
@ -101,24 +100,24 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public void RefreshRulesItems()
{
_rulesItems.Clear();
RulesItems.Clear();
foreach (var item in _rules)
{
var it = new RulesItemModel()
{
Id = item.Id,
RuleTypeName = item.RuleType?.ToString(),
OutboundTag = item.OutboundTag,
Port = item.Port,
Network = item.Network,
Protocols = Utils.List2String(item.Protocol),
InboundTags = Utils.List2String(item.InboundTag),
Domains = Utils.List2String(item.Domain),
Ips = Utils.List2String(item.Ip),
Domains = Utils.List2String((item.Domain ?? []).Concat(item.Ip ?? []).ToList()),
Enabled = item.Enabled,
Remarks = item.Remarks,
};
_rulesItems.Add(it);
RulesItems.Add(it);
}
}

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