Compare commits

..

64 Commits

Author SHA1 Message Date
2dust 635a04d74c up 7.24.4 2026-07-30 20:43:20 +08:00
autorepobot 2e1bb23da7 Update build-windows-x86.yml (#9846) 2026-07-28 20:10:12 +08:00
JieXu 6f3bacd5b4 Update build-linux.yml (#9844)
* Update build-linux.yml

* Update build-linux.yml

* Update build-linux.yml
2026-07-28 10:12:11 +08:00
XXcipherX 074ce5de04 Fix missing IPv6 default route for Xray TUN (#9843)
* Fix IPv6 route for Xray TUN

* Update V2rayInboundService.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-28 10:11:20 +08:00
hyjhyj111 02df430172 Use the TUN context snapshot in remaining launch-path reads (#9831)
Follow-up to #9830: audit of all TunModeItem.EnableTun usages found
three more launch-path reads of the live mutable config where the
behavior must agree with the context snapshot that generated the
config:

- CoreConfigClashService (mihomo custom config): the tun section was
  decided from the live config while the mihomo launch elevation uses
  the snapshot; a mid-reload toggle could produce a config containing
  tun launched without sudo, the same failure fixed in #9830. The tun
  state is now passed in as a snapshot.
- CoreManager.LoadCore: the Windows RemoveTunDevice cleanup now checks
  the main/pre context snapshots.
- CoreManager.WaitForProxyPort: preContext.AppConfig is a shared live
  reference; use preContext.IsTunEnabled instead.

Reads that intentionally stay live: StatusBarViewModel (UI state
source), CoreConfigContextBuilder (the snapshot capture point),
GetPreSocksItem (called during snapshot construction, self-consistent),
and AppManager.StatePort2 (transient mid-reload skew only, self-heals
after reload).
2026-07-27 21:13:28 +08:00
2dust 611ce0fd2c Adjust MsgAllowInsecureDeprecated 2026-07-27 21:08:35 +08:00
2dust a846e74e8f Fix
https://github.com/2dust/v2rayN/issues/9834
2026-07-27 14:08:30 +08:00
2dust 92c3df45ae up 7.24.3 2026-07-26 21:25:31 +08:00
hyjhyj111 ee7e21268a Fix TUN core elevation: decide sudo from the config context snapshot (#9830)
CoreManager.RunProcess decided sudo elevation from the live mutable
_config.TunModeItem.EnableTun while the launched config was generated
from the immutable CoreConfigContext snapshot. If the TUN state changed
while a reload was in flight, a core whose config contains a TUN
inbound could launch without elevation and die within the 100ms health
check ("Failed to run core"), with no stderr shown.

The elevation decision now follows context.IsTunEnabled /
preContext.IsTunEnabled, so the generated config and the launch mode
always agree. With legacy TUN protect, the sing-box pre-core hosting
TUN is elevated and the main core no longer runs as root needlessly.
2026-07-26 21:24:11 +08:00
2dust dc216c2b02 Rename TunIp to TunIP 2026-07-26 21:07:36 +08:00
2dust beffa919d3 Use /126 prefixes for TUN IPv6 addresses
https://github.com/2dust/v2rayN/pull/9830
2026-07-26 20:46:32 +08:00
2dust a654826231 Enable legacy TUN protect by default 2026-07-26 20:46:01 +08:00
2dust b01476d147 Fix tun inbound dns setting 2026-07-26 10:29:35 +08:00
2dust cf8dd45abe up 7.24.2 2026-07-24 16:57:53 +08:00
2dust 9bba6f53f8 Update Directory.Packages.props 2026-07-24 16:57:22 +08:00
2dust cd77f1d882 Code clean 2026-07-24 16:57:19 +08:00
mymyme 0427037638 fix: sanitize system proxy exceptions to avoid macOS ExceptionsList b… (#9815)
* fix: sanitize system proxy exceptions to avoid macOS ExceptionsList being dropped

On macOS/Linux the exception list is comma-separated and passed to networksetup. It was only stripped of spaces, so a stray newline or a trailing comma left a malformed/empty entry. macOS configd then silently rejects the whole ExceptionsList (scutil --proxy shows no ExceptionsList), so the bypass never takes effect. Split on ',' with TrimEntries + RemoveEmptyEntries and rejoin; behavior-preserving for well-formed input. Windows path (';') is left unchanged.

Note: not built locally (no .NET SDK on hand); relying on CI.

* Update SysProxyHandler.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-24 14:12:10 +08:00
2dust e749b81ecf Add configurable TUN IPv4/IPv6 addresses
https://github.com/2dust/v2rayN/issues/9810
2026-07-24 10:13:08 +08:00
JieXu fd2c942231 Update package-rhel-loong.sh (#9792)
* Update package-rhel-loong.sh

* Update package-debian-loong.sh

* Update Directory.Packages.props
2026-07-20 09:30:52 +08:00
tt2563 fa42eb670f Update ResUI.zh-Hant.resx (#9783)
Update Traditional Chinese translation
2026-07-19 09:54:45 +08:00
DHR60 0d42996573 Fix ui (#9789) 2026-07-19 09:54:26 +08:00
DHR60 a37772f12b Fakeip (#9786)
* Add fakeip range

* Xray fakedns
2026-07-18 17:47:14 +08:00
DHR60 531e0e9443 Fix (#9787) 2026-07-18 17:45:42 +08:00
2dust 984c77f0c4 up 7.24.1 2026-07-17 09:26:10 +08:00
2dust b5e0be47a5 Code clean 2026-07-17 09:25:13 +08:00
dependabot[bot] 6df0e6de1b Bump actions/setup-dotnet from 5.4.0 to 6.0.0 (#9778)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5.4.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5.4.0...v6.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: 6.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>
2026-07-17 09:13:28 +08:00
autorepobot 2b5328665f Update package-rhel.sh (#9780)
* Update package-rhel.sh

* Update package-rhel-riscv.sh

* Update package-rhel-loong.sh

* Update package-debian.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh

* Update package-debian-riscv.sh

* Update package-rhel-riscv.sh
2026-07-17 09:13:19 +08:00
DHR60 4584ecc5c9 Add HappyEyeballs (#9772)
* Add HappyEyeballs

* Clean up the code

* Fix
2026-07-17 09:12:50 +08:00
DHR60 d9dc2d7cf5 Fix (#9776) 2026-07-17 09:11:28 +08:00
DHR60 36f8565b7a Update cert (#9773) 2026-07-17 08:59:30 +08:00
DHR60 f55d8b2565 Sync xray (#9708)
* Sync xray fragment

* Sync xray freedom domainStrategy

* Add Strategy4ProxyDial
2026-07-16 18:55:06 +08:00
DHR60 09ea4890a7 Fix (#9771) 2026-07-16 17:37:17 +08:00
DHR60 5cc2aaba13 Fix (#9769) 2026-07-16 17:37:04 +08:00
DHR60 b8889bad86 Fix (#9768) 2026-07-16 17:36:51 +08:00
JieXu 5dd5b25869 Update Directory.Packages.props (#9766) 2026-07-16 11:48:57 +08:00
2dust fafdb4a0b6 up 7.24.0 2026-07-16 11:42:28 +08:00
2dust 223642dd67 Fix
https://github.com/2dust/v2rayN/issues/9765
2026-07-16 11:39:47 +08:00
Miheichev Aleksandr Sergeevich 165e0bf9e9 chore(deps): update NuGet dependencies (#9745)
- Downloader 5.9.0 -> 5.9.4
- NLog 6.1.3 -> 6.1.4
- Repobot.SQLite.Unofficial 3.53.3 -> 3.53.3.8
- sqlite-net-e 1.11.0 -> 1.11.285
- YamlDotNet 18.0.0 -> 18.1.0

sqlite-net-e and Repobot.SQLite.Unofficial are bumped together: the newer
sqlite-net-e moves to SQLitePCLRaw.config.e_sqlite3 3.0.3.

SkiaSharp.NativeAssets.Linux intentionally stays at 3.119.4 to match the
managed SkiaSharp resolved through Avalonia.Skia 12.1.0; bumping it alone
to 4.x would compile but break rendering at runtime on Linux. It should
follow Avalonia when Avalonia itself moves to SkiaSharp 4.
ReactiveUI.Avalonia stays at 12.0.3: the numerically higher 14.x line
targets Avalonia 11 / ReactiveUI 19 and would be an effective downgrade
for the Avalonia 12 stack.
2026-07-15 19:12:56 +08:00
DHR60 e615314582 Xray tun route only (#9759) 2026-07-15 19:12:34 +08:00
DHR60 ca9978b625 Fix (#9758) 2026-07-15 18:12:55 +08:00
DHR60 74ab7ad097 Add ProtectCoreTypeList (#9756)
* Add ProtectCoreTypeList

* Add process path restrictions
2026-07-15 17:32:07 +08:00
DHR60 8f5cbad988 Xray precore for custom config (#9754)
* Xray precore for custom config

* Fix

* Custom Pre

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-15 17:23:21 +08:00
2dust 1ab9757498 Revert "fix: 在窗口初始化前设置保存的尺寸,消除启动时偏右下偏移 (#9498)"
This reverts commit 8147b39723.
https://github.com/2dust/v2rayN/issues/9740
2026-07-14 17:27:30 +08:00
DHR60 50ab02e8a9 Desktop DragDropSort (#9734) 2026-07-14 09:57:00 +08:00
2dust b9a1c129c4 Fixed the issue with RefreshIcon's call in the desktop version. 2026-07-14 09:52:01 +08:00
tt2563 996eb51f9b Update Traditional Chinese translation (#9742) 2026-07-14 09:11:48 +08:00
Miheichev Aleksandr Sergeevich b9a58e7137 i18n(ru): translate newly added UI strings (#9733)
Add Russian translations for the 20 resource strings introduced up to
v7.23.4 that were missing from ResUI.ru.resx:

- allowInsecure deprecation warning and insecure-configuration message
- Xray HTTP outbound custom headers (label, tip, validation message)
- Hysteria2 Realm URL (label, format tip, validation message)
- TUN route exclude address (label, tip, invalid-address message)
- Final Fragment option and Root Certificate Provider (labels and tips)
- DNS via Bridge, Gecko Packet Size, Legacy Protect tip,
  Verify Peer Cert By Name, options-conflict message

Translated from the zh-Hans source and cross-checked against the English
resource. Existing Russian terminology conventions are preserved, and key
ordering mirrors the English resource file.
2026-07-14 09:11:14 +08:00
DHR60 9664ee380e ViewModel-First (#9621)
* Interaction

* Remove view action

* ViewModel-first

* MainGirdOrientation Hot Reload

* Fix avalonia preview

* Remove CloseWindowInteraction

* Fix

* Avoid threading issues

* Fix

* ProfilesSelect

* Try fix previewer

* Remove AppEvents.ProfilesRefreshRequested

* Remove AppEvents.SubscriptionsRefreshRequested

* Remove AppEvents.ProxiesReloadRequested

* Remove AppEvents.AdjustMainLvColWidthRequested

* Remove AppEvents.SetDefaultServerRequested

* Remove AppEvents.RoutingsMenuRefreshRequested

* Remove AppEvents.TestServerRequested

* Remove AppEvents.InboundDisplayRequested

* Remove AppEvents.SubscriptionsUpdateRequested

* Remove AppEvents.ShowHideWindowRequested

* Remove AppEvents.ReloadRequested

* Remove AppEvents.AddServerRequested

* Fix
2026-07-14 09:05:05 +08:00
DHR60 6f50206606 Update Avalonia to 12.1 (#9148) 2026-07-13 09:41:28 +08:00
2dust 1de83f96ed up 7.23.4 2026-07-11 17:44:19 +08:00
2dust 994bc1ca6a Code clean 2026-07-11 17:43:32 +08:00
2dust 3ca49dd9db Add option to hide IP info column 2026-07-11 16:05:31 +08:00
DHR60 a0bd8f9934 Remove fallbackTag (#9728) 2026-07-11 15:18:16 +08:00
006lp 88d59488fd feat: support custom headers for Xray HTTP outbounds (#9690)
* feat: support custom headers for Xray HTTP outbounds

* feat: simplify HTTP outbound headers input

* feat: validate JSON syntax in HTTP headers

  Keep HTTP outbound headers stored as raw text, while validating
  header names and values against RFC 9110/9112 syntax. Support string
  arrays for multi-value headers, reject duplicate JSON header keys, and
  clarify the invalid header JSON message.

* feat: refactor HTTP header parsing into utility

* Simplify code

* Simplify code

* Update CoreConfigV2rayServiceTests.cs

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-07-11 14:31:17 +08:00
DHR60 2c70b018ce Revert "Fix" (#9727)
* Revert "Fix"

This reverts commit 45ab7503e3.

* Fix
2026-07-11 14:02:12 +08:00
Kurobac b6ab428dfc Fix Linux tray tooltip identity (#9709)
Keep the tray tooltip text fixed to the application name on Linux so desktop environments do not treat server changes as tray identity changes. Non-Linux platforms continue to show the current server information.
2026-07-11 11:42:00 +08:00
2dust 45ab7503e3 Fix
https://github.com/2dust/v2rayN/issues/9699
2026-07-11 11:20:36 +08:00
2dust 1768b4a7ee Update winget-publish.yml 2026-07-09 20:51:04 +08:00
2dust 3e9f78f831 up 7.23.3 2026-07-09 19:53:43 +08:00
JieXu 8c4bd52389 Update package-rhel.sh (#9710)
* Update package-rhel.sh

* Update package-rhel-riscv.sh

* Update package-rhel-loong.sh

* Update package-debian.sh

* Update package-debian-riscv.sh

* Update package-debian-loong.sh

* Update package-osx.sh
2026-07-09 16:25:39 +08:00
DHR60 fa61fedf11 Add Root Cert Store (#9703) 2026-07-09 16:25:02 +08:00
DHR60 ad12a6c456 Update ca cert (#9700) 2026-07-08 09:48:46 +08:00
2dust 66b48c231f Synchronize GlobalHotKeys library and update HotKeyManager 2026-07-06 16:07:42 +08:00
JieXu 52766d66bf Update build-linux.yml (#9684)
* Update build-linux.yml

* Update package-debian-loong.sh

* Update package-rhel-loong.sh

* Update package-rhel-loong.sh

* Update package-debian-loong.sh
2026-07-06 13:48:05 +08:00
184 changed files with 9815 additions and 3008 deletions
+113
View File
@@ -0,0 +1,113 @@
/*
==============================================================================
THIRD-PARTY CA CERTIFICATE DATA ATTRIBUTION
==============================================================================
This component includes CA certificate data obtained from third-party sources.
1. Common CA Database (CCADB)
------------------------------------------------------------------------------
Website:
https://www.ccadb.org/
Data source:
https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites
License:
Community Data License Agreement – Permissive, Version 2.0 (CDLA-2.0 Permissive)
The names and trademarks of CCADB contributors may not be used to endorse or
promote products derived from this data without prior written permission.
License text:
https://cdla.dev/permissive-2-0/
2. Chromium Root Store
------------------------------------------------------------------------------
Source:
Chromium Project
Data source:
https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT
License:
BSD 3-Clause License
Copyright:
Copyright (c) The Chromium Authors
The Chromium Root Store data is distributed under the BSD 3-Clause License.
License text:
https://opensource.org/licenses/BSD-3-Clause
==============================================================================
*/
using System;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Collections.Generic;
using System.Threading.Tasks;
var client = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
var outputDir = args.Length > 0
? args[0]
: Path.Combine(Environment.CurrentDirectory, "v2rayN", "ServiceLib", "Sample");
Directory.CreateDirectory(outputDir);
Console.WriteLine("=== Mozilla + Chrome Root Store PEM Converter ===\n");
Console.WriteLine($"Output directory: {outputDir}\n");
// Process Mozilla
await ProcessMozillaAsync(outputDir);
// Process Chrome
await ProcessChromeAsync(outputDir);
Console.WriteLine("\nAll done!");
async Task ProcessMozillaAsync(string outputDir)
{
const string certdataUrl = "https://ccadb.my.salesforce-sites.com/mozilla/IncludedRootsPEMTxt?TrustBitsInclude=Websites";
var outputFile = Path.Combine(outputDir, "mozilla_roots_pem");
Console.WriteLine("Downloading Mozilla certdata.txt...");
var content = await client.GetStringAsync(certdataUrl);
var pemMatches = Regex.Matches(content, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
var pems = new List<string>();
foreach (Match m in pemMatches)
{
pems.Add(m.Groups[1].Value.Trim());
}
await File.WriteAllTextAsync(outputFile, string.Join("\n\n", pems));
Console.WriteLine($"Mozilla roots saved to: {outputFile} ({pems.Count} certificates)");
}
async Task ProcessChromeAsync(string outputDir)
{
const string chromeCertsUrl = "https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/root_store.certs?format=TEXT";
var chromeOutput = Path.Combine(outputDir, "chrome_roots_pem");
Console.WriteLine("Downloading Chrome root_store.certs...");
var base64Content = await client.GetStringAsync(chromeCertsUrl);
var decoded = Convert.FromBase64String(base64Content.Replace("\n", ""));
var text = Encoding.UTF8.GetString(decoded);
var pemMatches = Regex.Matches(text, @"(-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----)");
var pems = new List<string>();
foreach (Match m in pemMatches)
{
pems.Add(m.Groups[1].Value.Trim());
}
await File.WriteAllTextAsync(chromeOutput, string.Join("\n\n", pems));
Console.WriteLine($"Chrome roots saved to: {chromeOutput} ({pems.Count} certificates)");
}
+10 -10
View File
@@ -205,7 +205,7 @@ jobs:
deb-loong64: deb-loong64:
name: build and release deb loong64 name: build and release deb loong64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04-arm
env: env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2 QCOW2_URL: https://github.com/xujiegb/debian-loong64-qcow2/releases/download/13.5/debian13-loong64.qcow2
@@ -234,10 +234,10 @@ jobs:
- name: Download QEMU prebuild - name: Download QEMU prebuild
run: | run: |
set -euo pipefail set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz" wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-x64-v3.tar.gz tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install" mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/" rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version "$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware - name: Download loong64 qcow2 and EFI firmware
shell: bash shell: bash
@@ -258,7 +258,7 @@ jobs:
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock" export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-deb-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET" rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto & sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$! VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
@@ -355,7 +355,7 @@ jobs:
rpm-loong64: rpm-loong64:
name: build and release rpm loong64 name: build and release rpm loong64
if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) if: (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-26.04 runs-on: ubuntu-26.04-arm
env: env:
RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }} RELEASE_TAG: ${{ case(inputs.release_tag != '', inputs.release_tag, github.ref_name) }}
QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2 QCOW2_URL: https://github.com/xujiegb/fedora-loong64-qcow2/releases/download/43/fedora43-loong64.qcow2
@@ -384,10 +384,10 @@ jobs:
- name: Download QEMU prebuild - name: Download QEMU prebuild
run: | run: |
set -euo pipefail set -euo pipefail
wget -O qemu-linux-x64-v3.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-x64-v3.tar.gz" wget -O qemu-linux-arm64-v8.2.tar.gz "https://github.com/xujiegb/qemu-linux-prebuild/releases/download/${QEMU_VERSION}/qemu-linux-arm64-v8.2.tar.gz"
tar -xzf qemu-linux-x64-v3.tar.gz tar -xzf qemu-linux-arm64-v8.2.tar.gz
mkdir -p "$HOME/qemu-install" mkdir -p "$HOME/qemu-install"
rsync -a qemu-linux-x64-v3/ "$HOME/qemu-install/" rsync -a qemu-linux-arm64-v8.2/ "$HOME/qemu-install/"
"$HOME/qemu-install/bin/qemu-system-loongarch64" --version "$HOME/qemu-install/bin/qemu-system-loongarch64" --version
- name: Download loong64 qcow2 and EFI firmware - name: Download loong64 qcow2 and EFI firmware
shell: bash shell: bash
@@ -408,7 +408,7 @@ jobs:
export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock" export VIRTIOFSD_SOCKET="$RUNNER_TEMP/virtiofs-rpm-loong64.sock"
rm -f "$VIRTIOFSD_SOCKET" rm -f "$VIRTIOFSD_SOCKET"
sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" -o source="$GITHUB_WORKSPACE" -o cache=auto & sudo virtiofsd --socket-path="$VIRTIOFSD_SOCKET" --shared-dir="$GITHUB_WORKSPACE" --cache=auto &
VIRTIOFSD_PID=$! VIRTIOFSD_PID=$!
trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT trap 'sudo kill "$VIRTIOFSD_PID" 2>/dev/null || true; rm -f "$VIRTIOFSD_SOCKET"' EXIT
+2 -2
View File
@@ -52,7 +52,7 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0 dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET 10.0.1xx - name: Setup .NET 10.0.1xx
uses: actions/setup-dotnet@v5.4.0 uses: actions/setup-dotnet@v6.0.0
with: with:
dotnet-version: 10.0.1xx dotnet-version: 10.0.1xx
@@ -146,7 +146,7 @@ jobs:
$xrayTag = "v$xrayVer" $xrayTag = "v$xrayVer"
$singTag = "v$singVer" $singTag = "v$singVer"
$xrayUrl = "https://github.com/autorepobot/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip" $xrayUrl = "https://github.com/XTLS/Xray-core/releases/download/$xrayTag/Xray-windows-32.zip"
$singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip" $singUrl = "https://github.com/SagerNet/sing-box/releases/download/$singTag/sing-box-$singVer-windows-386.zip"
Write-Host "Bundled Xray version: $xrayVer" Write-Host "Bundled Xray version: $xrayVer"
+6 -1
View File
@@ -69,7 +69,7 @@ jobs:
dotnet --list-sdks 2>$null; $LASTEXITCODE=0 dotnet --list-sdks 2>$null; $LASTEXITCODE=0
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.4.0 uses: actions/setup-dotnet@v6.0.0
with: with:
dotnet-version: '10.0.1xx' dotnet-version: '10.0.1xx'
@@ -83,6 +83,11 @@ jobs:
working-directory: ./v2rayN working-directory: ./v2rayN
run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output run: dotnet publish ./AmazTool/AmazTool.csproj -c Release -r $RID -p:SelfContained=true -p:PublishTrimmed=true $ExtOpt -o $Output
- name: Remove Debug Symbols
shell: bash
run: |
find ${{ matrix.arch }} -type f -name '*.pdb' -delete
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v7.0.1 uses: actions/upload-artifact@v7.0.1
with: with:
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
fetch-depth: '0' fetch-depth: '0'
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v5.4.0 uses: actions/setup-dotnet@v6.0.0
with: with:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
+7 -2
View File
@@ -23,8 +23,13 @@ jobs:
$targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1 $targetRelease = $github | Where-Object -Property prerelease -match 'False' | Select -First 1
$x64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-64\.zip' | Select -ExpandProperty browser_download_url $assets = $targetRelease | Select -ExpandProperty assets -First 1
$arm64InstallerUrl = $targetRelease | Select -ExpandProperty assets -First 1 | Where-Object -Property name -match 'v2rayN-windows-arm64\.zip' | Select -ExpandProperty browser_download_url $x64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-64.zip' } | Select -ExpandProperty browser_download_url
$arm64InstallerUrl = $assets | Where-Object { $_.name -eq 'v2rayN-windows-arm64.zip' } | Select -ExpandProperty browser_download_url
if (-not $x64InstallerUrl -or -not $arm64InstallerUrl) {
throw "Could not find required installers in release assets."
}
$ver = $targetRelease.tag_name $ver = $targetRelease.tag_name
+4 -17
View File
@@ -8,13 +8,13 @@ BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}" XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}" SING_VER="${SING_VER:-}"
MIN_KERNEL="5.10" MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_TFM="net10.0" DOTNET_TFM="net10.0"
DOTNET_LOONGARCH_VERSION="10.0.109" DOTNET_LOONGARCH_VERSION="10.0.110"
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
@@ -117,19 +117,6 @@ install_dependencies() {
export PATH="$HOME/.dotnet:$PATH" export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet" export DOTNET_ROOT="$HOME/.dotnet"
mkdir -p "$HOME/.nuget/NuGet"
cat > "$HOME/.nuget/NuGet/NuGet.Config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
</packageSources>
</configuration>
EOF
dotnet --info >/dev/null 2>&1 && install_ok=1 dotnet --info >/dev/null 2>&1 && install_ok=1
fi fi
@@ -610,7 +597,7 @@ package_binary() {
write_desktop_file "$stage" write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir" write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)" extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
mkdir -p "$workdir/debian" mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF cat > "$workdir/debian/control" <<EOF
+2 -2
View File
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
OUTPUT_DIR="${HOME}/debbuild" OUTPUT_DIR="${HOME}/debbuild"
DOTNET_RISCV_VERSION="10.0.109" DOTNET_RISCV_VERSION="10.0.110"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -595,7 +595,7 @@ package_binary() {
write_desktop_file "$stage" write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir" write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)" extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
mkdir -p "$workdir/debian" mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF cat > "$workdir/debian/control" <<EOF
+1 -1
View File
@@ -609,7 +609,7 @@ package_binary() {
write_desktop_file "$stage" write_desktop_file "$stage"
write_maintainer_scripts "$debian_dir" write_maintainer_scripts "$debian_dir"
extra_depends="libc6 (>= 2.34), fontconfig (>= 2.13.1), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 8.32), bash (>= 5.1), libfreetype6 (>= 2.11)" extra_depends="libc6 (>= 2.39), fontconfig (>= 2.15.0), desktop-file-utils (>= 0.26), xdg-utils (>= 1.1.3), coreutils (>= 9.4), bash (>= 5.2.21), libfreetype6 (>= 2.13)"
mkdir -p "$workdir/debian" mkdir -p "$workdir/debian"
cat > "$workdir/debian/control" <<EOF cat > "$workdir/debian/control" <<EOF
+1 -1
View File
@@ -54,7 +54,7 @@ cat >"$PackagePath/v2rayN.app/Contents/Info.plist" <<-EOF
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.7</string> <string>13.7</string>
</dict> </dict>
</plist> </plist>
EOF EOF
+8 -21
View File
@@ -8,12 +8,12 @@ BUILD_FROM=""
XRAY_VER="${XRAY_VER:-}" XRAY_VER="${XRAY_VER:-}"
SING_VER="${SING_VER:-}" SING_VER="${SING_VER:-}"
MIN_KERNEL="5.10" MIN_KERNEL="6.12"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_LOONGARCH_VERSION="10.0.109" DOTNET_LOONGARCH_VERSION="10.0.110"
DOTNET_LOONGARCH_TAG="v10.0.109-loongarch64" DOTNET_LOONGARCH_TAG="v10.0.110-loongarch64"
DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download" DOTNET_LOONGARCH_BASE="https://github.com/loongson/dotnet/releases/download"
DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz" DOTNET_LOONGARCH_FILE="dotnet-sdk-${DOTNET_LOONGARCH_VERSION}-linux-loongarch64.tar.gz"
DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}" DOTNET_SDK_URL="${DOTNET_LOONGARCH_BASE}/${DOTNET_LOONGARCH_TAG}/${DOTNET_LOONGARCH_FILE}"
@@ -113,19 +113,6 @@ install_dependencies() {
export PATH="$HOME/.dotnet:$PATH" export PATH="$HOME/.dotnet:$PATH"
export DOTNET_ROOT="$HOME/.dotnet" export DOTNET_ROOT="$HOME/.dotnet"
mkdir -p "$HOME/.nuget/NuGet"
cat > "$HOME/.nuget/NuGet/NuGet.Config" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="loongnix" value="https://lnuget.loongnix.cn/v3/index.json" allowInsecureConnections="true" />
</packageSources>
</configuration>
EOF
dotnet --info >/dev/null 2>&1 || install_ok=0 dotnet --info >/dev/null 2>&1 || install_ok=0
fi fi
@@ -523,13 +510,13 @@ ExclusiveArch: loongarch64
Source0: __PKGROOT__.tar.gz Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34 Requires: glibc >= 2.39
Requires: fontconfig >= 2.13.1 Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26 Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3 Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32 Requires: coreutils >= 9.4
Requires: bash >= 5.1 Requires: bash >= 5.2.21
Requires: freetype >= 2.10 Requires: freetype >= 2.13
%description %description
v2rayN Linux for Red Hat Enterprise Linux v2rayN Linux for Red Hat Enterprise Linux
+6 -6
View File
@@ -12,7 +12,7 @@ MIN_KERNEL="5.10"
PKGROOT="v2rayN-publish" PKGROOT="v2rayN-publish"
PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj" PROJECT_HINT="v2rayN.Desktop/v2rayN.Desktop.csproj"
RPM_TOPDIR="${HOME}/rpmbuild" RPM_TOPDIR="${HOME}/rpmbuild"
DOTNET_RISCV_VERSION="10.0.109" DOTNET_RISCV_VERSION="10.0.110"
DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download" DOTNET_RISCV_BASE="https://github.com/xujiegb/dotnet-riscv/releases/download"
DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz" DOTNET_RISCV_FILE="dotnet-sdk-${DOTNET_RISCV_VERSION}-linux-riscv64.tar.gz"
DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}" DOTNET_SDK_URL="${DOTNET_RISCV_BASE}/${DOTNET_RISCV_VERSION}/${DOTNET_RISCV_FILE}"
@@ -509,13 +509,13 @@ ExclusiveArch: riscv64
Source0: __PKGROOT__.tar.gz Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34 Requires: glibc >= 2.39
Requires: fontconfig >= 2.13.1 Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26 Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3 Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32 Requires: coreutils >= 9.4
Requires: bash >= 5.1 Requires: bash >= 5.2.21
Requires: freetype >= 2.10 Requires: freetype >= 2.13
%description %description
v2rayN Linux for Red Hat Enterprise Linux v2rayN Linux for Red Hat Enterprise Linux
+5 -5
View File
@@ -498,13 +498,13 @@ ExclusiveArch: aarch64 x86_64
Source0: __PKGROOT__.tar.gz Source0: __PKGROOT__.tar.gz
Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL Requires: cairo, pango, openssl, mesa-libEGL, mesa-libGL
Requires: glibc >= 2.34 Requires: glibc >= 2.39
Requires: fontconfig >= 2.13.1 Requires: fontconfig >= 2.15.0
Requires: desktop-file-utils >= 0.26 Requires: desktop-file-utils >= 0.26
Requires: xdg-utils >= 1.1.3 Requires: xdg-utils >= 1.1.3
Requires: coreutils >= 8.32 Requires: coreutils >= 9.4
Requires: bash >= 5.1 Requires: bash >= 5.2.21
Requires: freetype >= 2.10 Requires: freetype >= 2.13
%description %description
v2rayN Linux for Red Hat Enterprise Linux v2rayN Linux for Red Hat Enterprise Linux
+1 -1
View File
@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.23.2</Version> <Version>7.24.4</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
+16 -16
View File
@@ -5,34 +5,34 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled> <CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.4.1" /> <PackageVersion Include="Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.13" /> <PackageVersion Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" /> <PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.18" /> <PackageVersion Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
<PackageVersion Include="AwesomeAssertions" Version="9.4.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.11.0" /> <PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" />
<PackageVersion Include="IPNetwork2" Version="4.3.0" /> <PackageVersion Include="IPNetwork2" Version="4.3.0" />
<PackageVersion Include="ReactiveUI.Avalonia" Version="11.4.13" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.0.3" />
<PackageVersion Include="CliWrap" Version="3.10.2" /> <PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="Downloader" Version="5.9.0" /> <PackageVersion Include="Downloader" Version="5.9.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.7.0" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" /> <PackageVersion Include="H.NotifyIcon.Wpf" Version="2.4.1" />
<PackageVersion Include="MaterialDesignThemes" Version="5.3.2" /> <PackageVersion Include="MaterialDesignThemes" Version="5.3.2" />
<PackageVersion Include="QRCoder" Version="1.8.0" /> <PackageVersion Include="QRCoder" Version="1.8.0" />
<PackageVersion Include="ReactiveUI" Version="23.2.28" /> <PackageVersion Include="ReactiveUI" Version="23.2.28" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" /> <PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" />
<PackageVersion Include="Semi.Avalonia" Version="11.3.14" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.2" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.3.7.3" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" />
<PackageVersion Include="NLog" Version="6.1.3" /> <PackageVersion Include="NLog" Version="6.1.4" />
<PackageVersion Include="sqlite-net-e" Version="1.11.0" /> <PackageVersion Include="sqlite-net-e" Version="1.11.285" />
<PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3" /> <PackageVersion Include="Repobot.SQLite.Unofficial" Version="3.53.3.10" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" /> <PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" /> <PackageVersion Include="WebDav.Client" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" /> <PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" /> <PackageVersion Include="xunit.v3" Version="3.2.2" />
<PackageVersion Include="YamlDotNet" Version="18.0.0" /> <PackageVersion Include="YamlDotNet" Version="18.1.0" />
<PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" /> <PackageVersion Include="ZXing.Net.Bindings.SkiaSharp" Version="0.16.22" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
</ItemGroup> </ItemGroup>
@@ -59,7 +59,7 @@ internal static class CoreConfigTestFactory
}, },
WebDavItem = new WebDavItem(), WebDavItem = new WebDavItem(),
CheckUpdateItem = new CheckUpdateItem(), CheckUpdateItem = new CheckUpdateItem(),
Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Length = "100-200", Interval = "10-20" }, Fragment4RayItem = new Fragment4RayItem { Packets = "tlshello", Lengths = ["100-200"], Delays = ["10-20"] },
Inbound = Inbound =
[ [
new InItem new InItem
@@ -84,6 +84,7 @@ internal static class CoreConfigTestFactory
ParallelQuery = false, ParallelQuery = false,
Strategy4Freedom = Global.AsIs, Strategy4Freedom = Global.AsIs,
Strategy4Proxy = Global.AsIs, Strategy4Proxy = Global.AsIs,
Strategy4ProxyDial = Global.AsIs,
}, },
IndexId = string.Empty, IndexId = string.Empty,
SubIndexId = string.Empty, SubIndexId = string.Empty,
@@ -130,6 +131,25 @@ internal static class CoreConfigTestFactory
}; };
} }
public static ProfileItem CreateHttpNode(ECoreType coreType, string indexId = "node-http-1",
string remarks = "demo-http")
{
return new ProfileItem
{
IndexId = indexId,
ConfigType = EConfigType.HTTP,
CoreType = coreType,
Remarks = remarks,
Address = "proxy.example.com",
Port = 8080,
Password = "pass",
Username = "user",
Network = nameof(ETransport.raw),
StreamSecurity = string.Empty,
Subid = string.Empty,
};
}
public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks, public static ProfileItem CreatePolicyGroupNode(ECoreType coreType, string indexId, string remarks,
IEnumerable<string> childIndexIds) IEnumerable<string> childIndexIds)
{ {
@@ -592,4 +592,38 @@ public class CoreConfigSingboxServiceTests
proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478"); proxy.realm.stun_servers.Should().Contain("turn.cloudflare.com:3478");
proxy.server.Should().BeNull(); proxy.server.Should().BeNull();
} }
[Fact]
public void GenerateClientConfigContent_TunSystemStackWithIpv6_ShouldUsePrefixWithPeerAddress()
{
// Regression test for #9820: sing-box fails with "need one more IPv6 address in
// first prefix for system stack" when the TUN inbound uses a /128 IPv6 prefix.
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
config.TunModeItem.EnableTun = true;
config.TunModeItem.Stack = "system";
config.TunModeItem.EnableIPv6Address = true;
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateVmessNode(ECoreType.sing_box);
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.sing_box) with
{
IsTunEnabled = true,
};
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString())!;
var tun = cfg.inbounds.First(i => i.type == "tun");
tun.address.Should().NotBeNullOrEmpty();
foreach (var address in tun.address!)
{
var prefixLength = int.Parse(address[(address.LastIndexOf('/') + 1)..]);
var isIpv6 = address.Contains(':');
prefixLength.Should().BeLessThanOrEqualTo(isIpv6 ? 126 : 30,
$"'{address}' must leave room for the peer address the system stack derives");
}
}
} }
@@ -28,6 +28,39 @@ public class CoreConfigV2rayServiceTests
v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed)); v2rayConfig.inbounds.Should().Contain(i => i.protocol == nameof(EInboundProtocol.mixed));
} }
[Fact]
public void GenerateClientConfigContent_HttpOutbound_ShouldEmitHeadersInSettings()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
var node = CoreConfigTestFactory.CreateHttpNode(ECoreType.Xray);
node.SetProtocolExtra(node.GetProtocolExtra() with
{
HttpHeaders = "{\"User-Agent\":\"v2rayN\",\"Set-Cookie\":[\"a=1\",\"b=2\"]}",
});
var context = CoreConfigTestFactory.CreateContext(config, node, ECoreType.Xray);
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString())!;
var outbound = cfg.outbounds.First(o => o.tag == Global.ProxyTag && o.protocol == "http");
outbound.settings.address?.ToString().Should().Be("proxy.example.com");
outbound.settings.port.Should().Be(8080);
outbound.settings.user.Should().Be("user");
outbound.settings.pass.Should().Be("pass");
outbound.settings.level.Should().Be(1);
outbound.settings.headers.Should().NotBeNull();
var headers = JsonUtils.ParseJson(outbound.settings.headers.ToString());
headers["User-Agent"]!.GetValue<string>().Should().Be("v2rayN");
headers["Set-Cookie"]!.AsArray()
.Select(item => item!.GetValue<string>())
.Should().Equal("a=1", "b=2");
outbound.settings.servers.Should().BeNull();
outbound.settings.vnext.Should().BeNull();
}
[Fact] [Fact]
public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer() public void GenerateClientConfigContent_PolicyGroup_ShouldExpandChildrenAndBuildBalancer()
{ {
@@ -534,7 +567,7 @@ public class CoreConfigV2rayServiceTests
var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom"); var directOutbound = cfg.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag && o.protocol == "freedom");
directOutbound.Should().NotBeNull(); directOutbound.Should().NotBeNull();
directOutbound!.settings.domainStrategy.Should().Be("UseIPv4"); directOutbound!.streamSettings.sockopt!.domainStrategy.Should().Be("UseIPv4");
} }
[Fact] [Fact]
@@ -0,0 +1,44 @@
using AwesomeAssertions;
using ServiceLib.Enums;
using ServiceLib.Manager;
using Xunit;
namespace ServiceLib.Tests.Manager;
public class CoreManagerTests
{
[Theory]
[InlineData(ECoreType.sing_box)]
[InlineData(ECoreType.mihomo)]
[InlineData(ECoreType.Xray)]
public void ShouldRunAsSudo_TunLaunchOnNonWindows_RequiresElevation(ECoreType coreType)
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeTrue();
}
[Fact]
public void ShouldRunAsSudo_NonTunLaunch_ShouldNotElevate()
{
// Regression guard for the macOS TUN failure: the elevation decision must follow
// the context snapshot that generated the config. A launch whose snapshot has TUN
// disabled must never elevate, and a launch whose snapshot has TUN enabled must
// elevate regardless of later changes to the live config.
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.sing_box, isNonWindows: true).Should().BeFalse();
CoreManager.ShouldRunAsSudo(isTunLaunch: false, ECoreType.Xray, isNonWindows: true).Should().BeFalse();
}
[Fact]
public void ShouldRunAsSudo_OnWindows_ShouldNotElevate()
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, ECoreType.sing_box, isNonWindows: false).Should().BeFalse();
}
[Theory]
[InlineData(ECoreType.v2fly)]
[InlineData(ECoreType.hysteria)]
[InlineData(null)]
public void ShouldRunAsSudo_UnsupportedCoreType_ShouldNotElevate(ECoreType? coreType)
{
CoreManager.ShouldRunAsSudo(isTunLaunch: true, coreType, isNonWindows: true).Should().BeFalse();
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace ServiceLib.Base;
public interface ICloseable
{
public event EventHandler? RequestClose;
}
+7
View File
@@ -0,0 +1,7 @@
namespace ServiceLib.Base;
public interface IWindowDialog
{
public Task<bool> ShowDialogAsync<TViewModel>(TViewModel vm)
where TViewModel : class;
}
@@ -3,5 +3,4 @@ namespace ServiceLib.Base;
public class MyReactiveObject : ReactiveObject public class MyReactiveObject : ReactiveObject
{ {
protected static Config? _config; protected static Config? _config;
protected Func<EViewAction, object?, Task<bool>>? _updateView;
} }
+9 -4
View File
@@ -485,12 +485,17 @@ public class Utils
public static string? DomainStrategy4Sbox(string? strategy) public static string? DomainStrategy4Sbox(string? strategy)
{ {
if (strategy is null)
{
return null;
}
return strategy switch return strategy switch
{ {
not null when strategy.StartsWith("UseIPv4") => "prefer_ipv4", _ when strategy.StartsWith("UseIPv6") => "prefer_ipv6",
not null when strategy.StartsWith("UseIPv6") => "prefer_ipv6", _ when strategy.StartsWith("UseIP") => "prefer_ipv4",
not null when strategy.StartsWith("ForceIPv4") => "ipv4_only", _ when strategy.StartsWith("ForceIPv6") => "ipv6_only",
not null when strategy.StartsWith("ForceIPv6") => "ipv6_only", _ when strategy.StartsWith("ForceIP") => "ipv4_only",
_ => null _ => null
}; };
} }
-36
View File
@@ -1,36 +0,0 @@
namespace ServiceLib.Enums;
public enum EViewAction
{
CloseWindow,
ShowYesNo,
SaveFileDialog,
AddBatchRoutingRulesYesNo,
SetClipboardData,
AddServerViaClipboard,
ImportRulesFromClipboard,
ProfilesFocus,
ShareSub,
ShareServer,
ScanScreenTask,
ScanImageTask,
BrowseServer,
ImportRulesFromFile,
InitSettingFont,
PasswordInput,
SubEditWindow,
RoutingRuleSettingWindow,
RoutingRuleDetailsWindow,
AddServerWindow,
AddServer2Window,
AddGroupServerWindow,
DNSSettingWindow,
RoutingSettingWindow,
OptionSettingWindow,
FullConfigTemplateWindow,
GlobalHotkeySettingWindow,
SubSettingWindow,
DispatcherRefreshServersBiz,
DispatcherRefreshIcon,
DispatcherShowMsg,
}
-14
View File
@@ -2,16 +2,9 @@ namespace ServiceLib.Events;
public static class AppEvents 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<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new(); public static readonly EventChannel<bool> HasUpdateNotified = 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<ServerSpeedItem> DispatcherStatisticsRequested = new();
public static readonly EventChannel<string> SendSnackMsgRequested = new(); public static readonly EventChannel<string> SendSnackMsgRequested = new();
@@ -20,12 +13,5 @@ public static class AppEvents
public static readonly EventChannel<Unit> AppExitRequested = new(); public static readonly EventChannel<Unit> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = 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(); public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
} }
+43 -6
View File
@@ -2,8 +2,6 @@ namespace ServiceLib;
public class Global public class Global
{ {
#region const
public const string AppName = "v2rayN"; public const string AppName = "v2rayN";
public const string GithubUrl = "https://github.com"; public const string GithubUrl = "https://github.com";
public const string GithubApiUrl = "https://api.github.com/repos"; public const string GithubApiUrl = "https://api.github.com/repos";
@@ -42,6 +40,11 @@ public class Global
public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh"; public const string KillAsSudoOSXShellFileName = NamespaceSample + "kill_as_sudo_osx_sh";
public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh"; public const string KillAsSudoLinuxShellFileName = NamespaceSample + "kill_as_sudo_linux_sh";
public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter"; public const string SingboxFakeIPFilterFileName = NamespaceSample + "singbox_fakeip_filter";
public const string ChromeRootCertFileName = NamespaceSample + "chrome_roots_pem";
public const string MozillaRootCertFileName = NamespaceSample + "mozilla_roots_pem";
public const string ChromeRootProvider = "chrome";
public const string MozillaRootProvider = "mozilla";
public const string DefaultSecurity = "auto"; public const string DefaultSecurity = "auto";
public const string DefaultNetwork = "raw"; public const string DefaultNetwork = "raw";
@@ -101,9 +104,7 @@ public class Global
public const string SingboxFakeDNSTag = "fake_dns"; public const string SingboxFakeDNSTag = "fake_dns";
public const int Hysteria2DefaultHopInt = 30; public const int Hysteria2DefaultHopInt = 30;
public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset"; public const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset";
public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$"; public const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$";
public static readonly List<string> PolicyGroupDefaultFilterList = public static readonly List<string> PolicyGroupDefaultFilterList =
@@ -552,7 +553,6 @@ public class Global
"http", "http",
"tls", "tls",
"quic", "quic",
"fakedns",
]; ];
public static readonly List<int> TunMtus = public static readonly List<int> TunMtus =
@@ -732,5 +732,42 @@ public class Global
"reply", "reply",
]; ];
#endregion const public static readonly List<string> FakeIPRanges =
[
"198.18.0.0/15",
"11.0.0.0/8",
];
public static readonly List<string> RootCertProviders =
[
"system",
ChromeRootProvider,
MozillaRootProvider,
];
public static readonly IReadOnlyList<string> TunIPv4Address =
[
"172.18.0.1/30",
"172.31.0.1/30",
"172.20.0.1/30",
"172.16.0.1/30",
"192.168.100.1/30",
"10.10.14.1/30",
"10.1.0.1/30",
"10.0.0.1/30",
];
// Prefixes must leave room for a peer address (max /126); the sing-box system
// stack derives a gateway from the first prefix and rejects single-address prefixes.
public static readonly IReadOnlyList<string> TunIPv6Address =
[
"fc00::172:18:0:1/126",
"fc00::172:31:0:1/126",
"fc00::172:20:0:1/126",
"fc00::172:16:0:1/126",
"fc00::192:168:100:1/126",
"fc00::10:10:14:1/126",
"fc00::10:1:0:1/126",
"fc00::10:0:0:1/126",
];
} }
+1 -1
View File
@@ -30,8 +30,8 @@ global using ServiceLib.Handler.Fmt;
global using ServiceLib.Handler.SysProxy; global using ServiceLib.Handler.SysProxy;
global using ServiceLib.Helper; global using ServiceLib.Helper;
global using ServiceLib.Manager; global using ServiceLib.Manager;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Configs; global using ServiceLib.Models.Configs;
global using ServiceLib.Models.CoreConfigs;
global using ServiceLib.Models.Dto; global using ServiceLib.Models.Dto;
global using ServiceLib.Models.Entities; global using ServiceLib.Models.Entities;
global using ServiceLib.Resx; global using ServiceLib.Resx;
@@ -49,6 +49,7 @@ public class CoreConfigContextBuilder
RoutingItem = await ConfigHandler.GetDefaultRouting(config), RoutingItem = await ConfigHandler.GetDefaultRouting(config),
IsWindows = Utils.IsWindows(), IsWindows = Utils.IsWindows(),
IsMacOS = Utils.IsMacOS(), IsMacOS = Utils.IsMacOS(),
ProtectCoreTypeList = config.TunModeItem.EnableTun ? [ECoreType.Xray, ECoreType.sing_box] : []
}; };
var validatorResult = NodeValidatorResult.Empty(); var validatorResult = NodeValidatorResult.Empty();
var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node); var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node);
@@ -193,12 +194,16 @@ public class CoreConfigContextBuilder
if (preSocksItem != null) if (preSocksItem != null)
{ {
var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem); var preSocksResult = await Build(nodeContext.AppConfig, preSocksItem);
var protectCoreTypeList = new HashSet<ECoreType>(nodeContext.ProtectCoreTypeList) { nodeContext.RunCoreType };
return preSocksResult with return preSocksResult with
{ {
Context = preSocksResult.Context with Context = preSocksResult.Context with
{ {
ProtectDomainList = ProtectDomainList =
[.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []], [.. nodeContext.ProtectDomainList ?? [], .. preSocksResult.Context.ProtectDomainList ?? []],
ProtectCoreTypeList = protectCoreTypeList,
}, },
}; };
} }
+26 -5
View File
@@ -92,9 +92,13 @@ public static class ConfigHandler
EnableTun = false, EnableTun = false,
Mtu = 9000, Mtu = 9000,
IcmpRouting = Global.TunIcmpRoutingPolicies.First(), IcmpRouting = Global.TunIcmpRoutingPolicies.First(),
EnableLegacyProtect = false, EnableLegacyProtect = true,
}; };
config.GuiItem ??= new(); config.GuiItem ??= new();
if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider))
{
config.GuiItem.RootCertProvider = Global.RootCertProviders.First();
}
config.MsgUIItem ??= new(); config.MsgUIItem ??= new();
config.UiItem ??= new(); config.UiItem ??= new();
@@ -111,10 +115,12 @@ public static class ConfigHandler
config.ConstItem ??= new ConstItem(); config.ConstItem ??= new ConstItem();
config.SimpleDNSItem ??= InitBuiltinSimpleDNS(); config.SimpleDNSItem ??= InitBuiltinSimpleDNS();
config.SimpleDNSItem.FakeIPRange ??= Global.FakeIPRanges.FirstOrDefault();
config.SimpleDNSItem.GlobalFakeIp ??= true; config.SimpleDNSItem.GlobalFakeIp ??= true;
config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault(); config.SimpleDNSItem.BootstrapDNS ??= Global.DomainPureIPDNSAddress.FirstOrDefault();
config.SimpleDNSItem.ServeStale ??= false; config.SimpleDNSItem.ServeStale ??= false;
config.SimpleDNSItem.ParallelQuery ??= false; config.SimpleDNSItem.ParallelQuery ??= false;
config.SimpleDNSItem.EnableHappyEyeballs ??= false;
config.SpeedTestItem ??= new(); config.SpeedTestItem ??= new();
if (config.SpeedTestItem.SpeedTestTimeout < 10) if (config.SpeedTestItem.SpeedTestTimeout < 10)
@@ -164,10 +170,24 @@ public static class ConfigHandler
config.Fragment4RayItem ??= new() config.Fragment4RayItem ??= new()
{ {
Packets = "tlshello", Packets = "tlshello",
Length = "50-100",
Interval = "10-20",
MaxSplit = "0"
}; };
config.Fragment4RayItem.MaxSplit ??= "0";
config.HappyEyeballs4RayItem ??= new()
{
TryDelayMs = 250,
PrioritizeIPv6 = false,
Interleave = 1,
MaxConcurrentTry = 4,
};
if ((config.Fragment4RayItem.Lengths ?? []).Count == 0)
{
config.Fragment4RayItem.Lengths = [config.Fragment4RayItem.Length ?? "50-100"];
}
if ((config.Fragment4RayItem.Delays ?? []).Count == 0)
{
config.Fragment4RayItem.Delays = [config.Fragment4RayItem.Interval ?? "10-20"];
}
config.GlobalHotkeys ??= []; config.GlobalHotkeys ??= [];
if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty()) if (config.SystemProxyItem.SystemProxyExceptions.IsNullOrEmpty())
@@ -1507,7 +1527,8 @@ public static class ConfigHandler
else if (node.ConfigType == EConfigType.Custom else if (node.ConfigType == EConfigType.Custom
&& node.PreSocksPort is > 0 and <= 65535) && node.PreSocksPort is > 0 and <= 65535)
{ {
var preCoreType = config.TunModeItem.EnableTun ? ECoreType.sing_box : ECoreType.Xray; var customPreCoreType = AppManager.Instance.GetCoreType(null, EConfigType.Custom);
var preCoreType = (enableLegacyProtect && config.TunModeItem.EnableTun) ? ECoreType.sing_box : customPreCoreType;
itemSocks = new ProfileItem() itemSocks = new ProfileItem()
{ {
CoreType = preCoreType, CoreType = preCoreType,
@@ -17,7 +17,7 @@ public static class CoreConfigHandler
{ {
result = node.CoreType switch result = node.CoreType switch
{ {
ECoreType.mihomo => await new CoreConfigClashService(config).GenerateClientCustomConfig(node, fileName), ECoreType.mihomo => await new CoreConfigClashService(config, context.IsTunEnabled).GenerateClientCustomConfig(node, fileName),
_ => await GenerateClientCustomConfig(node, fileName) _ => await GenerateClientCustomConfig(node, fileName)
}; };
} }
@@ -16,7 +16,6 @@ public static class SysProxyHandler
try try
{ {
var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks); var port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks);
var exceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
if (port <= 0) if (port <= 0)
{ {
return false; return false;
@@ -24,17 +23,18 @@ public static class SysProxyHandler
switch (type) switch (type)
{ {
case ESysProxyType.ForcedChange when Utils.IsWindows(): case ESysProxyType.ForcedChange when Utils.IsWindows():
{ var (strProxy, strExceptions) = GetWindowsProxyString(config, port);
GetWindowsProxyString(config, port, out var strProxy, out var strExceptions); ProxySettingWindows.SetProxy(strProxy, strExceptions, 2);
ProxySettingWindows.SetProxy(strProxy, strExceptions, 2); break;
break;
}
case ESysProxyType.ForcedChange when Utils.IsLinux(): case ESysProxyType.ForcedChange when Utils.IsLinux():
var exceptions = SanitizeExceptions(config);
await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions); await ProxySettingLinux.SetProxy(Global.Loopback, port, exceptions);
break; break;
case ESysProxyType.ForcedChange when Utils.IsMacOS(): case ESysProxyType.ForcedChange when Utils.IsMacOS():
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions); var exceptions2 = SanitizeExceptions(config);
await ProxySettingOSX.SetProxy(Global.Loopback, port, exceptions2);
break; break;
case ESysProxyType.ForcedClear when Utils.IsWindows(): case ESysProxyType.ForcedClear when Utils.IsWindows():
@@ -66,15 +66,31 @@ public static class SysProxyHandler
return true; return true;
} }
private static void GetWindowsProxyString(Config config, int port, out string strProxy, out string strExceptions) private static string SanitizeExceptions(Config config)
{ {
strExceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", ""); var exceptions = config.SystemProxyItem.SystemProxyExceptions;
if (exceptions.IsNullOrEmpty())
{
return string.Empty;
}
var items = exceptions
.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Replace(" ", string.Empty))
.Where(item => item.Length > 0);
return string.Join(',', items);
}
private static (string strProxy, string strExceptions) GetWindowsProxyString(Config config, int port)
{
var strExceptions = config.SystemProxyItem.SystemProxyExceptions.Replace(" ", "");
if (config.SystemProxyItem.NotProxyLocalAddress) if (config.SystemProxyItem.NotProxyLocalAddress)
{ {
strExceptions = $"<local>;{strExceptions}"; strExceptions = $"<local>;{strExceptions}";
} }
strProxy = string.Empty; var strProxy = string.Empty;
if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty()) if (config.SystemProxyItem.SystemProxyAdvancedProtocol.IsNullOrEmpty())
{ {
strProxy = $"{Global.Loopback}:{port}"; strProxy = $"{Global.Loopback}:{port}";
@@ -86,6 +102,8 @@ public static class SysProxyHandler
.Replace("{http_port}", port.ToString()) .Replace("{http_port}", port.ToString())
.Replace("{socks_port}", port.ToString()); .Replace("{socks_port}", port.ToString());
} }
return (strProxy, strExceptions);
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
+95 -17
View File
@@ -1,3 +1,4 @@
using System.Security.Authentication;
using Downloader; using Downloader;
namespace ServiceLib.Helper; namespace ServiceLib.Helper;
@@ -24,17 +25,19 @@ public class DownloaderHelper
headers.Add(HttpRequestHeader.Authorization, "Basic " + Utils.Base64Encode(uri.UserInfo)); headers.Add(HttpRequestHeader.Authorization, "Basic " + Utils.Base64Encode(uri.UserInfo));
} }
var requestConfiguration = new RequestConfiguration()
{
Headers = headers,
UserAgent = userAgent,
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration = requestConfiguration,
{ CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
Headers = headers,
UserAgent = userAgent,
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
}
}; };
await using var downloader = new Downloader.DownloadService(downloadOpt); await using var downloader = new Downloader.DownloadService(downloadOpt);
@@ -65,15 +68,17 @@ public class DownloaderHelper
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5); var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration = requestConfiguration,
{ CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
ConnectTimeout= connectTimeout * 1000,
Proxy = webProxy
}
}; };
var lastUpdateTime = DateTime.Now; var lastUpdateTime = DateTime.Now;
@@ -143,15 +148,17 @@ public class DownloaderHelper
} }
var connectTimeout = Math.Clamp(timeout / 5, 2, 5); var connectTimeout = Math.Clamp(timeout / 5, 2, 5);
var requestConfiguration = new RequestConfiguration()
{
ConnectTimeout = connectTimeout * 1000,
Proxy = webProxy
};
var downloadOpt = new DownloadConfiguration() var downloadOpt = new DownloadConfiguration()
{ {
BlockTimeout = timeout * 1000, BlockTimeout = timeout * 1000,
MaxTryAgainOnFailure = 2, MaxTryAgainOnFailure = 2,
RequestConfiguration = RequestConfiguration = requestConfiguration,
{ CustomHttpMessageHandlerFactory = () => GetSocketsHttpHandler(requestConfiguration),
ConnectTimeout= connectTimeout * 1000,
Proxy = webProxy
}
}; };
var progressPercentage = 0; var progressPercentage = 0;
@@ -188,4 +195,75 @@ public class DownloaderHelper
downloadOpt = null; downloadOpt = null;
} }
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/SocketClient.cs#L45
// There is a risk of MITM attacks
// https://github.com/bezzad/Downloader/blob/a75a6e431acd6cbba6293f7afdcf676544a09174/src/Downloader/Extensions/ExceptionHelper.cs#L111
private static SocketsHttpHandler GetSocketsHttpHandler(RequestConfiguration config)
{
SocketsHttpHandler handler = new()
{
AllowAutoRedirect = config.AllowAutoRedirect,
MaxAutomaticRedirections = config.MaximumAutomaticRedirections,
AutomaticDecompression = config.AutomaticDecompression,
PreAuthenticate = config.PreAuthenticate,
UseCookies = config.CookieContainer != null,
UseProxy = config.Proxy != null,
MaxConnectionsPerServer = 1000,
PooledConnectionIdleTimeout = config.KeepAliveTimeout,
PooledConnectionLifetime = Timeout.InfiniteTimeSpan,
EnableMultipleHttp2Connections = true,
ConnectTimeout = TimeSpan.FromMilliseconds(config.ConnectTimeout)
};
// Set up the SslClientAuthenticationOptions for custom certificate validation
if (config.ClientCertificates?.Count > 0)
{
handler.SslOptions.ClientCertificates = config.ClientCertificates;
}
handler.SslOptions.EnabledSslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12;
//handler.SslOptions.RemoteCertificateValidationCallback = ExceptionHelper.CertificateValidationCallBack;
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
{
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
handler.SslOptions.RemoteCertificateValidationCallback = null;
}
// Configure keep-alive
if (config.KeepAlive)
{
handler.KeepAlivePingTimeout = config.KeepAliveTimeout;
handler.KeepAlivePingPolicy = HttpKeepAlivePingPolicy.WithActiveRequests;
}
// Configure credentials
if (config.Credentials != null)
{
handler.Credentials = config.Credentials;
handler.PreAuthenticate = config.PreAuthenticate;
}
// Configure cookies
if (handler.UseCookies && config.CookieContainer != null)
{
handler.CookieContainer = config.CookieContainer;
}
// Configure proxy
if (handler.UseProxy && config.Proxy != null)
{
handler.Proxy = config.Proxy;
}
// Add expect header
if (!string.IsNullOrWhiteSpace(config.Expect))
{
handler.Expect100ContinueTimeout = TimeSpan.FromSeconds(1);
}
return handler;
}
} }
+2 -1
View File
@@ -10,6 +10,7 @@ public sealed class AppManager
private int? _statePort2; private int? _statePort2;
public static AppManager Instance => _instance.Value; public static AppManager Instance => _instance.Value;
public Config Config => _config; public Config Config => _config;
public IWindowDialog WindowDialog { get; set; } = null!;
public int StatePort public int StatePort
{ {
@@ -662,7 +663,7 @@ public sealed class AppManager
return Global.SsSecuritiesInSingbox; return Global.SsSecuritiesInSingbox;
} }
public ECoreType GetCoreType(ProfileItem profileItem, EConfigType eConfigType) public ECoreType GetCoreType(ProfileItem? profileItem, EConfigType eConfigType)
{ {
if (profileItem?.CoreType != null) if (profileItem?.CoreType != null)
{ {
+59 -175
View File
@@ -10,180 +10,17 @@ public class CertPemManager
{ {
private static readonly string _tag = "CertPemManager"; private static readonly string _tag = "CertPemManager";
private static readonly Lazy<CertPemManager> _instance = new(() => new()); private static readonly Lazy<CertPemManager> _instance = new(() => new());
private Config _config;
/// <summary> public async Task Init(Config config)
/// Trusted CA certificate thumbprints (SHA256) to prevent MITM attacks
/// </summary>
private static readonly HashSet<string> TrustedCaThumbprints = new(StringComparer.OrdinalIgnoreCase)
{ {
"EBD41040E4BB3EC742C9E381D31EF2A41A48B6685C96E7CEF3C1DF6CD4331C99", // GlobalSign Root CA if (_config != null)
"6DC47172E01CBCB0BF62580D895FE2B8AC9AD4F873801E0C10B9C837D21EB177", // Entrust.net Premium 2048 Secure Server CA {
"73C176434F1BC6D5ADF45B0E76E727287C8DE57616C1E6E6141A2B2CBC7D8E4C", // Entrust Root Certification Authority return;
"D8E0FEBC1DB2E38D00940F37D27D41344D993E734B99D5656D9778D4D8143624", // Certum Root CA }
"D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4", // Comodo AAA Services root _config = config;
"85A0DD7DD720ADB7FF05F83D542B209DC7FF4528F7D677B18389FEA5E5C49E86", // QuoVadis Root CA 2 await Task.CompletedTask;
"18F1FC7F205DF8ADDDEB7FE007DD57E3AF375A9C4D8D73546BF4F1FED1E18D35", // QuoVadis Root CA 3 }
"C3846BF24B9E93CA64274C0EC67C1ECC5E024FFCACD2D74019350E81FE546AE4", // Go Daddy Class 2 CA
"1465FA205397B876FAA6F0A9958E5590E40FCC7FAA4FB7C2C8677521FB5FB658", // Starfield Class 2 CA
"3E9099B5015E8F486C00BCEA9D111EE721FABA355A89BCF1DF69561E3DC6325C", // DigiCert Assured ID Root CA
"4348A0E9444C78CB265E058D5E8944B4D84F9662BD26DB257F8934A443C70161", // DigiCert Global Root CA
"7431E5F4C3C1CE4690774F0B61E05440883BA9A01ED00BA6ABD7806ED3B118CF", // DigiCert High Assurance EV Root CA
"62DD0BE9B9F50A163EA0F8E75C053B1ECA57EA55C8688F647C6881F2C8357B95", // SwissSign Gold CA - G2
"0C2CD63DF7806FA399EDE809116B575BF87989F06518F9808C860503178BAF66", // COMODO Certification Authority
"1793927A0614549789ADCE2F8F34F7F0B66D0F3AE3A3B84D21EC15DBBA4FADC7", // COMODO ECC Certification Authority
"41C923866AB4CAD6B7AD578081582E020797A6CBDF4FFF78CE8396B38937D7F5", // OISTE WISeKey Global Root GA CA
"E3B6A2DB2ED7CE48842F7AC53241C7B71D54144BFB40C11F3F1D0B42F5EEA12D", // Certigna
"C0A6F4DC63A24BFDCF54EF2A6A082A0A72DE35803E2FF5FF527AE5D87206DFD5", // ePKI Root Certification Authority
"6C61DAC3A2DEF031506BE036D2A6FE401994FBD13DF9C8D466599274C446EC98", // NetLock Arany (Class Gold) Főtanúsítvány
"3C5F81FEA5FAB82C64BFA2EAECAFCDE8E077FC8620A7CAE537163DF36EDBF378", // Microsec e-Szigno Root CA 2009
"CBB522D7B7F127AD6A0113865BDF1CD4102E7D0759AF635A7CF4720DC963C53B", // GlobalSign Root CA - R3
"2530CC8E98321502BAD96F9B1FBA1B099E2D299E0F4548BB914F363BC0D4531F", // Izenpe.com
"45140B3247EB9CC8C5B4F0D7B53091F73292089E6E5A63E2749DD3ACA9198EDA", // Go Daddy Root Certificate Authority - G2
"2CE1CB0BF9D2F9E102993FBE215152C3B2DD0CABDE1C68E5319B839154DBB7F5", // Starfield Root Certificate Authority - G2
"568D6905A2C88708A4B3025190EDCFEDB1974A606A13C6E5290FCB2AE63EDAB5", // Starfield Services Root Certificate Authority - G2
"5C58468D55F58E497E743982D2B50010B6D165374ACF83A7D4A32DB768C4408E", // Certum Trusted Network CA
"BFD88FE1101C41AE3E801BF8BE56350EE9BAD1A6B9BD515EDC5C6D5B8711AC44", // TWCA Root Certification Authority
"513B2CECB810D4CDE5DD85391ADFC6C2DD60D87BB736D2B521484AA47A0EBEF6", // Security Communication RootCA2
"55926084EC963A64B96E2ABE01CE0BA86A64FBFEBCC7AAB5AFC155B37FD76066", // Actalis Authentication Root CA
"9A114025197C5BB95D94E63D55CD43790847B646B23CDF11ADA4A00EFF15FB48", // Buypass Class 2 Root CA
"EDF7EBBCA27A2A384D387B7D4010C666E2EDB4843E4C29B4AE1D5B9332E6B24D", // Buypass Class 3 Root CA
"FD73DAD31C644FF1B43BEF0CCDDA96710B9CD9875ECA7E31707AF3E96D522BBD", // T-TeleSec GlobalRoot Class 3
"49E7A442ACF0EA6287050054B52564B650E4F49E42E348D6AA38E039E957B1C1", // D-TRUST Root Class 3 CA 2 2009
"EEC5496B988CE98625B934092EEC2908BED0B0F316C2D4730C84EAF1F3D34881", // D-TRUST Root Class 3 CA 2 EV 2009
"E23D4A036D7B70E9F595B1422079D2B91EDFBB1FB651A0633EAA8A9DC5F80703", // CA Disig Root R2
"9A6EC012E1A7DA9DBE34194D478AD7C0DB1822FB071DF12981496ED104384113", // ACCVRAIZ1
"59769007F7685D0FCD50872F9F95D5755A5B2B457D81F3692B610A98672F0E1B", // TWCA Global Root CA
"91E2F5788D5810EBA7BA58737DE1548A8ECACD014598BC0B143E041B17052552", // T-TeleSec GlobalRoot Class 2
"F356BEA244B7A91EB35D53CA9AD7864ACE018E2D35D5F8F96DDF68A6F41AA474", // Atos TrustedRoot 2011
"8A866FD1B276B57E578E921C65828A2BED58E9F2F288054134B7F1F4BFC9CC74", // QuoVadis Root CA 1 G3
"8FE4FB0AF93A4D0D67DB0BEBB23E37C71BF325DCBCDD240EA04DAF58B47E1840", // QuoVadis Root CA 2 G3
"88EF81DE202EB018452E43F864725CEA5FBD1FC2D9D205730709C5D8B8690F46", // QuoVadis Root CA 3 G3
"7D05EBB682339F8C9451EE094EEBFEFA7953A114EDB2F44949452FAB7D2FC185", // DigiCert Assured ID Root G2
"7E37CB8B4C47090CAB36551BA6F45DB840680FBA166A952DB100717F43053FC2", // DigiCert Assured ID Root G3
"CB3CCBB76031E5E0138F8DD39A23F9DE47FFC35E43C1144CEA27D46A5AB1CB5F", // DigiCert Global Root G2
"31AD6648F8104138C738F39EA4320133393E3A18CC02296EF97C2AC9EF6731D0", // DigiCert Global Root G3
"552F7BDCF1A7AF9E6CE672017F4F12ABF77240C78E761AC203D1D9D20AC89988", // DigiCert Trusted Root G4
"52F0E1C4E58EC629291B60317F074671B85D7EA80D5B07273463534B32B40234", // COMODO RSA Certification Authority
"E793C9B02FD8AA13E21C31228ACCB08119643B749C898964B1746D46C3D4CBD2", // USERTrust RSA Certification Authority
"4FF460D54B9C86DABFBCFC5712E0400D2BED3FBC4D4FBDAA86E06ADCD2A9AD7A", // USERTrust ECC Certification Authority
"179FBC148A3DD00FD24EA13458CC43BFA7F59C8182D783A513F6EBEC100C8924", // GlobalSign ECC Root CA - R5
"3C4FB0B95AB8B30032F432B86F535FE172C185D0FD39865837CF36187FA6F428", // Staat der Nederlanden Root CA - G3
"5D56499BE4D2E08BCFCAD08A3E38723D50503BDE706948E42F55603019E528AE", // IdenTrust Commercial Root CA 1
"30D0895A9A448A262091635522D1F52010B5867ACAE12C78EF958FD4F4389F2F", // IdenTrust Public Sector Root CA 1
"43DF5774B03E7FEF5FE40D931A7BEDF1BB2E6B42738C4E6D3841103D3AA7F339", // Entrust Root Certification Authority - G2
"02ED0EB28C14DA45165C566791700D6451D7FB56F0B2AB1D3B8EB070E56EDFF5", // Entrust Root Certification Authority - EC1
"5CC3D78E4E1D5E45547A04E6873E64F90CF9536D1CCC2EF800F355C4C5FD70FD", // CFCA EV ROOT
"6B9C08E86EB0F767CFAD65CD98B62149E5494A67F5845E7BD1ED019F27B86BD6", // OISTE WISeKey Global Root GB CA
"A1339D33281A0B56E557D3D32B1CE7F9367EB094BD5FA72A7E5004C8DED7CAFE", // SZAFIR ROOT CA2
"B676F2EDDAE8775CD36CB0F63CD1D4603961F49E6265BA013A2F0307B6D0B804", // Certum Trusted Network CA 2
"A040929A02CE53B4ACF4F2FFC6981CE4496F755E6D45FE0B2A692BCD52523F36", // Hellenic Academic and Research Institutions RootCA 2015
"44B545AA8A25E65A73CA15DC27FC36D24C1CB9953A066539B11582DC487B4833", // Hellenic Academic and Research Institutions ECC RootCA 2015
"96BCEC06264976F37460779ACF28C5A7CFE8A3C0AAE11A8FFCEE05C0BDDF08C6", // ISRG Root X1
"EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA", // AC RAIZ FNMT-RCM
"8ECDE6884F3D87B1125BA31AC3FCB13D7016DE7F57CC904FE1CB97C6AE98196E", // Amazon Root CA 1
"1BA5B2AA8C65401A82960118F80BEC4F62304D83CEC4713A19C39C011EA46DB4", // Amazon Root CA 2
"18CE6CFE7BF14E60B2E347B8DFE868CB31D02EBB3ADA271569F50343B46DB3A4", // Amazon Root CA 3
"E35D28419ED02025CFA69038CD623962458DA5C695FBDEA3C22B0BFB25897092", // Amazon Root CA 4
"A1A86D04121EB87F027C66F53303C28E5739F943FC84B38AD6AF009035DD9457", // D-TRUST Root CA 3 2013
"46EDC3689046D53A453FB3104AB80DCAEC658B2660EA1629DD7E867990648716", // TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1
"BFFF8FD04433487D6A8AA60C1A29767A9FC2BBB05E420F713A13B992891D3893", // GDCA TrustAUTH R5 ROOT
"85666A562EE0BE5CE925C1D8890A6F76A87EC16D4D7D5F29EA7419CF20123B69", // SSL.com Root Certification Authority RSA
"3417BB06CC6007DA1B961C920B8AB4CE3FAD820E4AA30B9ACBC4A74EBDCEBC65", // SSL.com Root Certification Authority ECC
"2E7BF16CC22485A7BBE2AA8696750761B0AE39BE3B2FE9D0CC6D4EF73491425C", // SSL.com EV Root Certification Authority RSA R2
"22A2C1F7BDED704CC1E701B5F408C310880FE956B5DE2A4A44F99C873A25A7C8", // SSL.com EV Root Certification Authority ECC
"2CABEAFE37D06CA22ABA7391C0033D25982952C453647349763A3AB5AD6CCF69", // GlobalSign Root CA - R6
"8560F91C3624DABA9570B5FEA0DBE36FF11A8323BE9486854FB3F34A5571198D", // OISTE WISeKey Global Root GC CA
"9BEA11C976FE014764C1BE56A6F914B5A560317ABD9988393382E5161AA0493C", // UCA Global G2 Root
"D43AF9B35473755C9684FC06D7D8CB70EE5C28E773FB294EB41EE71722924D24", // UCA Extended Validation Root
"D48D3D23EEDB50A459E55197601C27774B9D7B18C94D5A059511A10250B93168", // Certigna Root CA
"40F6AF0346A99AA1CD1D555A4E9CCE62C7F9634603EE406615833DC8C8D00367", // emSign Root CA - G1
"86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B", // emSign ECC Root CA - G3
"125609AA301DA0A249B97A8239CB6A34216F44DCAC9F3954B14292F2E8C8608F", // emSign Root CA - C1
"BC4D809B15189D78DB3E1D8CF4F9726A795DA1643CA5F1358E1DDB0EDC0D7EB3", // emSign ECC Root CA - C3
"5A2FC03F0C83B090BBFA40604B0988446C7636183DF9846E17101A447FB8EFD6", // Hongkong Post Root CA 3
"DB3517D1F6732A2D5AB97C533EC70779EE3270A62FB4AC4238372460E6F01E88", // Entrust Root Certification Authority - G4
"358DF39D764AF9E1B766E9C972DF352EE15CFAC227AF6AD1D70E8E4A6EDCBA02", // Microsoft ECC Root Certificate Authority 2017
"C741F70F4B2A8D88BF2E71C14122EF53EF10EBA0CFA5E64CFA20F418853073E0", // Microsoft RSA Root Certificate Authority 2017
"BEB00B30839B9BC32C32E4447905950641F26421B15ED089198B518AE2EA1B99", // e-Szigno Root CA 2017
"657CFE2FA73FAA38462571F332A2363A46FCE7020951710702CDFBB6EEDA3305", // certSIGN Root CA G2
"88F438DCF8FFD1FA8F429115FFE5F82AE1E06E0C70C375FAAD717B34A49E7265", // NAVER Global Root Certification Authority
"554153B13D2CF9DDB753BFBE1A4E0AE08D0AA4187058FE60A2B862B2E4B87BCB", // AC RAIZ FNMT-RCM SERVIDORES SEGUROS
"319AF0A7729E6F89269C131EA6A3A16FCD86389FDCAB3C47A4A675C161A3F974", // GlobalSign Secure Mail Root R45
"5CBF6FB81FD417EA4128CD6F8172A3C9402094F74AB2ED3A06B4405D04F30B19", // GlobalSign Secure Mail Root E45
"4FA3126D8D3A11D1C4855A4F807CBAD6CF919D3A5A88B03BEA2C6372D93C40C9", // GlobalSign Root R46
"CBB9C44D84B8043E1050EA31A69F514955D7BFD2E2C6B49301019AD61D9F5058", // GlobalSign Root E46
"FB8FEC759169B9106B1E511644C618C51304373F6C0643088D8BEFFD1B997599", // ANF Secure Server Root CA
"6B328085625318AA50D173C98D8BDA09D57E27413D114CF787A0F5D06C030CF6", // Certum EC-384 CA
"FE7696573855773E37A95E7AD4D9CC96C30157C15D31765BA9B15704E1AE78FD", // Certum Trusted Root CA
"2E44102AB58CB85419451C8E19D9ACF3662CAFBC614B6A53960A30F7D0E2EB41", // TunTrust Root CA
"D95D0E8EDA79525BF9BEB11B14D2100D3294985F0C62D9FABD9CD999ECCB7B1D", // HARICA TLS RSA Root CA 2021
"3F99CC474ACFCE4DFED58794665E478D1547739F2E780F1BB4CA9B133097D401", // HARICA TLS ECC Root CA 2021
"1BE7ABE30686B16348AFD1C61B6866A0EA7F4821E67D5E8AF937CF8011BC750D", // HARICA Client RSA Root CA 2021
"8DD4B5373CB0DE36769C12339280D82746B3AA6CD426E797A31BABE4279CF00B", // HARICA Client ECC Root CA 2021
"57DE0583EFD2B26E0361DA99DA9DF4648DEF7EE8441C3B728AFA9BCDE0F9B26A", // Autoridad de Certificacion Firmaprofesional CIF A62634068
"30FBBA2C32238E2A98547AF97931E550428B9B3F1C8EEB6633DCFA86C5B27DD3", // vTrus ECC Root CA
"8A71DE6559336F426C26E53880D00D88A18DA4C6A91F0DCB6194E206C5C96387", // vTrus Root CA
"69729B8E15A86EFC177A57AFB7171DFC64ADD28C2FCA8CF1507E34453CCB1470", // ISRG Root X2
"F015CE3CC239BFEF064BE9F1D2C417E1A0264A0A94BE1F0C8D121864EB6949CC", // HiPKI Root CA - G1
"B085D70B964F191A73E4AF0D54AE7A0E07AAFDAF9B71DD0862138AB7325A24A2", // GlobalSign ECC Root CA - R4
"D947432ABDE7B7FA90FC2E6B59101B1280E0E1C7E4E40FA3C6887FFF57A7F4CF", // GTS Root R1
"8D25CD97229DBF70356BDA4EB3CC734031E24CF00FAFCFD32DC76EB5841C7EA8", // GTS Root R2
"34D8A73EE208D9BCDB0D956520934B4E40E69482596E8B6F73C8426B010A6F48", // GTS Root R3
"349DFA4058C5E263123B398AE795573C4E1313C83FE68F93556CD5E8031B3C7D", // GTS Root R4
"242B69742FCB1E5B2ABF98898B94572187544E5B4D9911786573621F6A74B82C", // Telia Root CA v2
"E59AAA816009C22BFF5B25BAD37DF306F049797C1F81D85AB089E657BD8F0044", // D-TRUST BR Root CA 1 2020
"08170D1AA36453901A2F959245E347DB0C8D37ABAABC56B81AA100DC958970DB", // D-TRUST EV Root CA 1 2020
"018E13F0772532CF809BD1B17281867283FC48C6E13BE9C69812854A490C1B05", // DigiCert TLS ECC P384 Root G5
"371A00DC0533B3721A7EEB40E8419E70799D2B0A0F2C1D80693165F7CEC4AD75", // DigiCert TLS RSA4096 Root G5
"E8E8176536A60CC2C4E10187C3BEFCA20EF263497018F566D5BEA0F94D0C111B", // DigiCert SMIME ECC P384 Root G5
"90370D3EFA88BF58C30105BA25104A358460A7FA52DFC2011DF233A0F417912A", // DigiCert SMIME RSA4096 Root G5
"77B82CD8644C4305F7ACC5CB156B45675004033D51C60C6202A8E0C33467D3A0", // Certainly Root R1
"B4585F22E4AC756A4E8612A1361C5D9D031A93FD84FEBB778FA3068B0FC42DC2", // Certainly Root E1
"82BD5D851ACF7F6E1BA7BFCBC53030D0E7BC3C21DF772D858CAB41D199BDF595", // DIGITALSIGN GLOBAL ROOT RSA CA
"261D7114AE5F8FF2D8C7209A9DE4289E6AFC9D717023D85450909199F1857CFE", // DIGITALSIGN GLOBAL ROOT ECDSA CA
"E74FBDA55BD564C473A36B441AA799C8A68E077440E8288B9FA1E50E4BBACA11", // Security Communication ECC RootCA1
"F3896F88FE7C0A882766A7FA6AD2749FB57A7F3E98FB769C1FA7B09C2C44D5AE", // BJCA Global Root CA1
"574DF6931E278039667B720AFDC1600FC27EB66DD3092979FB73856487212882", // BJCA Global Root CA2
"48E1CF9E43B688A51044160F46D773B8277FE45BEAAD0E4DF90D1974382FEA99", // LAWtrust Root CA2 (4096)
"22D9599234D60F1D4BC7C7E96F43FA555B07301FD475175089DAFB8C25E477B3", // Sectigo Public Email Protection Root E46
"D5917A7791EB7CF20A2E57EB98284A67B28A57E89182DA53D546678C9FDE2B4F", // Sectigo Public Email Protection Root R46
"C90F26F0FB1B4018B22227519B5CA2B53E2CA5B3BE5CF18EFE1BEF47380C5383", // Sectigo Public Server Authentication Root E46
"7BB647A62AEEAC88BF257AA522D01FFEA395E0AB45C73F93F65654EC38F25A06", // Sectigo Public Server Authentication Root R46
"8FAF7D2E2CB4709BB8E0B33666BF75A5DD45B5DE480F8EA8D4BFE6BEBC17F2ED", // SSL.com TLS RSA Root CA 2022
"C32FFD9F46F936D16C3673990959434B9AD60AAFBB9E7CF33654F144CC1BA143", // SSL.com TLS ECC Root CA 2022
"AD7DD58D03AEDB22A30B5084394920CE12230C2D8017AD9B81AB04079BDD026B", // SSL.com Client ECC Root CA 2022
"1D4CA4A2AB21D0093659804FC0EB2175A617279B56A2475245C9517AFEB59153", // SSL.com Client RSA Root CA 2022
"E38655F4B0190C84D3B3893D840A687E190A256D98052F159E6D4A39F589A6EB", // Atos TrustedRoot Root CA ECC G2 2020
"78833A783BB2986C254B9370D3C20E5EBA8FA7840CBF63FE17297A0B0119685E", // Atos TrustedRoot Root CA RSA G2 2020
"B2FAE53E14CCD7AB9212064701AE279C1D8988FACB775FA8A008914E663988A8", // Atos TrustedRoot Root CA ECC TLS 2021
"81A9088EA59FB364C548A6F85559099B6F0405EFBF18E5324EC9F457BA00112F", // Atos TrustedRoot Root CA RSA TLS 2021
"E0D3226AEB1163C2E48FF9BE3B50B4C6431BE7BB1EACC5C36B5D5EC509039A08", // TrustAsia Global Root CA G3
"BE4B56CB5056C0136A526DF444508DAA36A0B54F42E4AC38F72AF470E479654C", // TrustAsia Global Root CA G4
"D92C171F5CF890BA428019292927FE22F3207FD2B54449CB6F675AF4922146E2", // D-Trust SBR Root CA 1 2022
"DBA84DD7EF622D485463A90137EA4D574DF8550928F6AFA03B4D8B1141E636CC", // D-Trust SBR Root CA 2 2022
"3AE6DF7E0D637A65A8C81612EC6F9A142F85A16834C10280D88E707028518755", // Telekom Security SMIME ECC Root 2021
"578AF4DED0853F4E5998DB4AEAF9CBEA8D945F60B620A38D1A3C13B2BC7BA8E1", // Telekom Security TLS ECC Root 2020
"78A656344F947E9CC0F734D9053D32F6742086B6B9CD2CAE4FAE1A2E4EFDE048", // Telekom Security SMIME RSA Root 2023
"EFC65CADBB59ADB6EFE84DA22311B35624B71B3B1EA0DA8B6655174EC8978646", // Telekom Security TLS RSA Root 2023
"3F63BB2814BE174EC8B6439CF08D6D56F0B7C405883A5648A334424D6B3EC558", // TWCA CYBER Root CA
"3A0072D49FFC04E996C59AEB75991D3C340F3615D6FD4DCE90AC0B3D88EAD4F4", // TWCA Global Root CA G2
"3F034BB5704D44B2D08545A02057DE93EBF3905FCE721ACBC730C06DDAEE904E", // SecureSign Root CA12
"4B009C1034494F9AB56BBA3BA1D62731FC4D20D8955ADCEC10A925607261E338", // SecureSign Root CA14
"E778F0F095FE843729CD1A0082179E5314A9C291442805E1FB1D8FB6B8886C3A", // SecureSign Root CA15
"0552E6F83FDF65E8FA9670E666DF28A4E21340B510CBE52566F97C4FB94B2BD1", // D-TRUST BR Root CA 2 2023
"436472C1009A325C54F1A5BBB5468A7BAEECCBE05DE5F099CB70D3FE41E13C16", // TrustAsia SMIME ECC Root CA
"C7796BEB62C101BB143D262A7C96A0C6168183223EF50D699632D86E03B8CC9B", // TrustAsia SMIME RSA Root CA
"C0076B9EF0531FB1A656D67C4EBE97CD5DBAA41EF44598ACC2489878C92D8711", // TrustAsia TLS ECC Root CA
"06C08D7DAFD876971EB1124FE67F847EC0C7A158D3EA53CBE940E2EA9791F4C3", // TrustAsia TLS RSA Root CA
"8E8221B2E7D4007836A1672F0DCC299C33BC07D316F132FA1A206D587150F1CE", // D-TRUST EV Root CA 2 2023
"9A12C392BFE57891A0C545309D4D9FD567E480CB613D6342278B195C79A7931F", // SwissSign RSA SMIME Root CA 2022 - 1
"193144F431E0FDDB740717D4DE926A571133884B4360D30E272913CBE660CE41", // SwissSign RSA TLS Root CA 2022 - 1
"D9A32485A8CCA85539CEF12FFFFF711378A17851D73DA2732AB4302D763BD62B", // OISTE Client Root ECC G1
"D02A0F994A868C66395F2E7A880DF509BD0C29C96DE16015A0FD501EDA4F96A9", // OISTE Client Root RSA G1
"EEC997C0C30F216F7E3B8B307D2BAE42412D753FC8219DAFD1520B2572850F49", // OISTE Server Root ECC G1
"9AE36232A5189FFDDB353DFD26520C015395D22777DAC59DB57B98C089A651E6", // OISTE Server Root RSA G1
"B49141502D00663D740F2E7EC340C52800962666121A36D09CF7DD2B90384FB4", // e-Szigno TLS Root CA 2023
};
public static CertPemManager Instance => _instance.Value; public static CertPemManager Instance => _instance.Value;
@@ -292,7 +129,7 @@ public class CertPemManager
/// <summary> /// <summary>
/// Validate server certificate with CA pinning /// Validate server certificate with CA pinning
/// </summary> /// </summary>
private static bool ValidateServerCertificate( private bool ValidateServerCertificate(
object _, object _,
X509Certificate? certificate, X509Certificate? certificate,
X509Chain? chain, X509Chain? chain,
@@ -327,9 +164,10 @@ public class CertPemManager
} }
var rootCert = certChain.ChainElements[^1].Certificate; var rootCert = certChain.ChainElements[^1].Certificate;
var rootThumbprint = rootCert.GetCertHashString(HashAlgorithmName.SHA256);
if (!TrustedCaThumbprints.Contains(rootThumbprint)) var trustedCerts = BuildTrustedCertificateCollection();
if (!trustedCerts.Contains(rootCert))
{ {
return false; return false;
} }
@@ -442,4 +280,50 @@ public class CertPemManager
return string.Empty; return string.Empty;
} }
} }
private static readonly Lazy<X509Certificate2Collection> _chromeRootCerts = new(() =>
{
var pemText = EmbedUtils.GetEmbedText(Global.ChromeRootCertFileName);
var collection = new X509Certificate2Collection();
collection.ImportFromPem(pemText);
return collection;
});
private static readonly Lazy<X509Certificate2Collection> _mozillaRootCerts = new(() =>
{
var pemText = EmbedUtils.GetEmbedText(Global.MozillaRootCertFileName);
var collection = new X509Certificate2Collection();
collection.ImportFromPem(pemText);
return collection;
});
private X509Certificate2Collection BuildTrustedCertificateCollection()
{
if (_config.GuiItem.RootCertProvider == Global.ChromeRootProvider)
{
return _chromeRootCerts.Value;
}
return _mozillaRootCerts.Value;
}
private bool IsSystemRootCertProvider()
{
return _config.GuiItem.RootCertProvider != Global.ChromeRootProvider && _config.GuiItem.RootCertProvider != Global.MozillaRootProvider;
}
public X509ChainPolicy? BuildCertificateChainPolicy()
{
if (IsSystemRootCertProvider())
{
return null;
}
var trustedCerts = BuildTrustedCertificateCollection();
var chainPolicy = new X509ChainPolicy
{
TrustMode = X509ChainTrustMode.CustomRootTrust,
RevocationMode = X509RevocationMode.NoCheck,
};
chainPolicy.CustomTrustStore.AddRange(trustedCerts);
return chainPolicy;
}
} }
+12 -1
View File
@@ -34,7 +34,18 @@ public class CoreAdminManager
StringBuilder sb = new(); StringBuilder sb = new();
sb.AppendLine("#!/bin/bash"); sb.AppendLine("#!/bin/bash");
var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}"; var cmdLine = $"{fileName.AppendQuotes()} {string.Format(coreInfo.Arguments, Utils.GetBinConfigPath(configPath).AppendQuotes())}";
sb.AppendLine($"exec sudo -S -- {cmdLine}");
// Passing environment variables to the sudo command, here it only xray or sing-box.
if (coreInfo.Environment.Count > 0)
{
var envArgs = string.Join(" ", coreInfo.Environment.Where(kv => kv.Value.IsNotEmpty()).Select(kv => $"{kv.Key}={kv.Value.AppendQuotes()}"));
sb.AppendLine($"exec sudo -S -- env {envArgs} {cmdLine}");
}
else
{
sb.AppendLine($"exec sudo -S -- {cmdLine}");
}
var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true); var shFilePath = await FileUtils.CreateLinuxShellFile("run_as_sudo.sh", sb.ToString(), true);
var procService = new ProcessService( var procService = new ProcessService(
+21 -8
View File
@@ -8,8 +8,10 @@ public class CoreManager
private static readonly Lazy<CoreManager> _instance = new(() => new()); private static readonly Lazy<CoreManager> _instance = new(() => new());
public static CoreManager Instance => _instance.Value; public static CoreManager Instance => _instance.Value;
private Config _config; private Config _config;
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private WindowsJobService? _processJob; private WindowsJobService? _processJob;
private ProcessService? _processService; private ProcessService? _processService;
private ProcessService? _processPreService; private ProcessService? _processPreService;
private bool _linuxSudo = false; private bool _linuxSudo = false;
@@ -83,7 +85,7 @@ public class CoreManager
await CoreStop(); await CoreStop();
await Task.Delay(100); await Task.Delay(100);
if (Utils.IsWindows() && _config.TunModeItem.EnableTun) if (Utils.IsWindows() && (mainContext?.IsTunEnabled == true || preContext?.IsTunEnabled == true))
{ {
await Task.Delay(100); await Task.Delay(100);
await WindowsUtils.RemoveTunDevice(); await WindowsUtils.RemoveTunDevice();
@@ -181,7 +183,7 @@ public class CoreManager
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(coreType);
var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog; var displayLog = node.ConfigType != EConfigType.Custom || node.DisplayLog;
var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true); var proc = await RunProcess(coreInfo, Global.CoreConfigFileName, displayLog, true, context.IsTunEnabled);
if (proc is null) if (proc is null)
{ {
return; return;
@@ -199,7 +201,7 @@ public class CoreManager
if (result.Success) if (result.Success)
{ {
var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType); var coreInfo = CoreInfoManager.Instance.GetCoreInfo(preCoreType);
var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true); var proc = await RunProcess(coreInfo, Global.CorePreConfigFileName, true, true, preContext.IsTunEnabled);
if (proc is null) if (proc is null)
{ {
return; return;
@@ -220,7 +222,7 @@ public class CoreManager
{ {
return; return;
} }
if (!preContext.AppConfig.TunModeItem.EnableTun) if (!preContext.IsTunEnabled)
{ {
return; return;
} }
@@ -287,7 +289,20 @@ public class CoreManager
#region Process #region Process
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo) /// <summary>
/// Decides whether a core launch must be elevated on non-Windows platforms.
/// The TUN state comes from the immutable <see cref="CoreConfigContext" /> snapshot that
/// generated the config, never from the live mutable config: the generated config and the
/// launch mode must always agree, even if TUN is toggled while a reload is in flight.
/// </summary>
public static bool ShouldRunAsSudo(bool isTunLaunch, ECoreType? coreType, bool isNonWindows)
{
return isTunLaunch
&& coreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray
&& isNonWindows;
}
private async Task<ProcessService?> RunProcess(CoreInfo? coreInfo, string configPath, bool displayLog, bool mayNeedSudo, bool isTunLaunch = false)
{ {
var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg); var fileName = CoreInfoManager.Instance.GetCoreExecFile(coreInfo, out var msg);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
@@ -299,9 +314,7 @@ public class CoreManager
try try
{ {
if (mayNeedSudo if (mayNeedSudo
&& _config.TunModeItem.EnableTun && ShouldRunAsSudo(isTunLaunch, coreInfo.CoreType, Utils.IsNonWindows()))
&& (coreInfo.CoreType is ECoreType.sing_box or ECoreType.mihomo or ECoreType.Xray)
&& Utils.IsNonWindows())
{ {
_linuxSudo = true; _linuxSudo = true;
await CoreAdminManager.Instance.Init(_config, _updateFunc); await CoreAdminManager.Instance.Init(_config, _updateFunc);
@@ -34,6 +34,7 @@ public class Config
public List<KeyEventItem> GlobalHotkeys { get; set; } public List<KeyEventItem> GlobalHotkeys { get; set; }
public List<CoreTypeItem> CoreTypeItem { get; set; } public List<CoreTypeItem> CoreTypeItem { get; set; }
public SimpleDNSItem SimpleDNSItem { get; set; } public SimpleDNSItem SimpleDNSItem { get; set; }
public HappyEyeballs4RayItem HappyEyeballs4RayItem { get; set; }
#endregion other entities #endregion other entities
} }
@@ -74,6 +74,7 @@ public class GUIItem
public int TrayMenuServersLimit { get; set; } = 20; public int TrayMenuServersLimit { get; set; } = 20;
public bool EnableHWA { get; set; } = false; public bool EnableHWA { get; set; } = false;
public bool EnableLog { get; set; } = true; public bool EnableLog { get; set; } = true;
public string? RootCertProvider { get; set; }
} }
[Serializable] [Serializable]
@@ -102,6 +103,7 @@ public class UIItem
public bool MacOSShowInDock { get; set; } public bool MacOSShowInDock { get; set; }
public List<ColumnItem> MainColumnItem { get; set; } public List<ColumnItem> MainColumnItem { get; set; }
public List<WindowSizeItem> WindowSizeItem { get; set; } public List<WindowSizeItem> WindowSizeItem { get; set; }
public bool HideColumnIpInfo { get; set; }
} }
[Serializable] [Serializable]
@@ -145,8 +147,10 @@ public class TunModeItem
public int Mtu { get; set; } public int Mtu { get; set; }
public bool EnableIPv6Address { get; set; } public bool EnableIPv6Address { get; set; }
public string IcmpRouting { get; set; } public string IcmpRouting { get; set; }
public bool EnableLegacyProtect { get; set; } public bool EnableLegacyProtect { get; set; } = true;
public List<string>? RouteExcludeAddress { get; set; } public List<string>? RouteExcludeAddress { get; set; }
public string IPv4Address { get; set; }
public string IPv6Address { get; set; }
} }
[Serializable] [Serializable]
@@ -247,9 +251,15 @@ public class CheckUpdateItem
public class Fragment4RayItem public class Fragment4RayItem
{ {
public string? Packets { get; set; } public string? Packets { get; set; }
public string? Length { get; set; } public List<string>? Lengths { get; set; }
public string? Interval { get; set; } public List<string>? Delays { get; set; }
public string? MaxSplit { get; set; } public string? MaxSplit { get; set; }
// For migration from old version, remove those properties in the future
public string? Length { get; set; }
public string? Interval { get; set; }
// migration end
} }
[Serializable] [Serializable]
@@ -267,14 +277,26 @@ public class SimpleDNSItem
public bool? AddCommonHosts { get; set; } public bool? AddCommonHosts { get; set; }
public bool? FakeIP { get; set; } public bool? FakeIP { get; set; }
public bool? GlobalFakeIp { get; set; } public bool? GlobalFakeIp { get; set; }
public string? FakeIPRange { get; set; }
public bool? BlockBindingQuery { get; set; } public bool? BlockBindingQuery { get; set; }
public string? DirectDNS { get; set; } public string? DirectDNS { get; set; }
public string? RemoteDNS { get; set; } public string? RemoteDNS { get; set; }
public string? BootstrapDNS { get; set; } public string? BootstrapDNS { get; set; }
public string? Strategy4Freedom { get; set; } public string? Strategy4Freedom { get; set; }
public string? Strategy4Proxy { get; set; } public string? Strategy4Proxy { get; set; }
public string? Strategy4ProxyDial { get; set; }
public bool? ServeStale { get; set; } public bool? ServeStale { get; set; }
public bool? ParallelQuery { get; set; } public bool? ParallelQuery { get; set; }
public string? Hosts { get; set; } public string? Hosts { get; set; }
public string? DirectExpectedIPs { get; set; } public string? DirectExpectedIPs { get; set; }
public bool? EnableHappyEyeballs { get; set; }
}
[Serializable]
public class HappyEyeballs4RayItem
{
public int? TryDelayMs { get; set; }
public bool? PrioritizeIPv6 { get; set; }
public int? Interleave { get; set; }
public int? MaxConcurrentTry { get; set; }
} }
@@ -17,6 +17,8 @@ public record CoreConfigContext
// TUN Compatibility // TUN Compatibility
public bool IsTunEnabled { get; init; } = false; public bool IsTunEnabled { get; init; } = false;
public HashSet<string> ProtectDomainList { get; init; } = []; public HashSet<string> ProtectDomainList { get; init; } = [];
// Typically, it is the core of the outbound chain
public HashSet<ECoreType> ProtectCoreTypeList { get; init; } = [];
public bool IsWindows { get; init; } public bool IsWindows { get; init; }
public bool IsMacOS { get; init; } public bool IsMacOS { get; init; }
@@ -4,6 +4,7 @@ public class V2rayConfig
{ {
public Log4Ray log { get; set; } public Log4Ray log { get; set; }
public object dns { get; set; } public object dns { get; set; }
public FakeDns4Ray? fakedns { get; set; }
public List<Inbounds4Ray> inbounds { get; set; } public List<Inbounds4Ray> inbounds { get; set; }
public List<Outbounds4Ray> outbounds { get; set; } public List<Outbounds4Ray> outbounds { get; set; }
public Routing4Ray routing { get; set; } public Routing4Ray routing { get; set; }
@@ -43,6 +44,12 @@ public class Log4Ray
public string? loglevel { get; set; } public string? loglevel { get; set; }
} }
public class FakeDns4Ray
{
public string? ipPool { get; set; }
public long? poolSize { get; set; }
}
public class Inbounds4Ray public class Inbounds4Ray
{ {
public string tag { get; set; } public string tag { get; set; }
@@ -136,8 +143,6 @@ public class Outboundsettings4Ray
public Response4Ray? response { get; set; } public Response4Ray? response { get; set; }
public string? domainStrategy { get; set; }
public int? userLevel { get; set; } public int? userLevel { get; set; }
public string? secretKey { get; set; } public string? secretKey { get; set; }
@@ -146,6 +151,16 @@ public class Outboundsettings4Ray
public int? port { get; set; } public int? port { get; set; }
public string? user { get; set; }
public string? pass { get; set; }
public int? level { get; set; }
public string? email { get; set; }
public object? headers { get; set; }
public List<WireguardPeer4Ray>? peers { get; set; } public List<WireguardPeer4Ray>? peers { get; set; }
public bool? noKernelTun { get; set; } public bool? noKernelTun { get; set; }
@@ -276,7 +291,6 @@ public class BalancersItem4Ray
public List<string>? selector { get; set; } public List<string>? selector { get; set; }
public BalancersStrategy4Ray? strategy { get; set; } public BalancersStrategy4Ray? strategy { get; set; }
public string? tag { get; set; } public string? tag { get; set; }
public string? fallbackTag { get; set; }
} }
public class BalancersStrategy4Ray public class BalancersStrategy4Ray
@@ -507,6 +521,8 @@ public class MaskSettings4Ray
public string? length { get; set; } public string? length { get; set; }
public string? delay { get; set; } public string? delay { get; set; }
public List<string>? lengths { get; set; }
public List<string>? delays { get; set; }
public int? maxSplit { get; set; } public int? maxSplit { get; set; }
// noise // noise
@@ -538,15 +554,20 @@ public class AccountsItem4Ray
public class Sockopt4Ray public class Sockopt4Ray
{ {
public string? domainStrategy { get; set; }
public string? dialerProxy { get; set; } public string? dialerProxy { get; set; }
[JsonPropertyName("interface")] [JsonPropertyName("interface")]
public string? Interface { get; set; } public string? Interface { get; set; }
public HappyEyeballs4Ray? happyEyeballs { get; set; }
} }
public class FragmentItem4Ray public class HappyEyeballs4Ray
{ {
public string? packets { get; set; } public int? tryDelayMs { get; set; }
public string? length { get; set; } public bool? prioritizeIPv6 { get; set; }
public string? interval { get; set; } public int? interleave { get; set; }
public int? maxConcurrentTry { get; set; }
} }
@@ -5,6 +5,9 @@ public record ProtocolExtraItem
public bool? Uot { get; init; } public bool? Uot { get; init; }
public string? CongestionControl { get; init; } public string? CongestionControl { get; init; }
// http outbound
public string? HttpHeaders { get; init; }
// vmess // vmess
public string? AlterId { get; init; } public string? AlterId { get; init; }
public string? VmessSecurity { get; init; } public string? VmessSecurity { get; init; }
+100 -1
View File
@@ -321,6 +321,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Please enter valid HTTP request headers JSON. 的本地化字符串。
/// </summary>
public static string InvalidHttpOutboundHeaders {
get {
return ResourceManager.GetString("InvalidHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Invalid Realm URL. 的本地化字符串。 /// 查找类似 Invalid Realm URL. 的本地化字符串。
/// </summary> /// </summary>
@@ -3051,6 +3060,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Enable Happy Eyeballs 的本地化字符串。
/// </summary>
public static string TbEnableHappyEyeballs {
get {
return ResourceManager.GetString("TbEnableHappyEyeballs", resourceCulture);
}
}
/// <summary>
/// 查找类似 Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path. 的本地化字符串。
/// </summary>
public static string TbEnableHappyEyeballsTip {
get {
return ResourceManager.GetString("TbEnableHappyEyeballsTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 DNS via Bridge 的本地化字符串。 /// 查找类似 DNS via Bridge 的本地化字符串。
/// </summary> /// </summary>
@@ -3079,7 +3106,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 Applies globally by default, with built-in FakeIP filtering (sing-box only). 的本地化字符串。 /// 查找类似 Applies globally by default, and built-in FakeIP filtering is only built into sing-box. 的本地化字符串。
/// </summary> /// </summary>
public static string TbFakeIPTips { public static string TbFakeIPTips {
get { get {
@@ -3240,6 +3267,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 HTTP headers 的本地化字符串。
/// </summary>
public static string TbHttpOutboundHeaders {
get {
return ResourceManager.GetString("TbHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Realm URL 的本地化字符串。 /// 查找类似 Realm URL 的本地化字符串。
/// </summary> /// </summary>
@@ -3312,6 +3348,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Ipv4 Address 的本地化字符串。
/// </summary>
public static string TbIpv4Address {
get {
return ResourceManager.GetString("TbIpv4Address", resourceCulture);
}
}
/// <summary>
/// 查找类似 Ipv6 Address 的本地化字符串。
/// </summary>
public static string TbIpv6Address {
get {
return ResourceManager.GetString("TbIpv6Address", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Most Stable 的本地化字符串。 /// 查找类似 Most Stable 的本地化字符串。
/// </summary> /// </summary>
@@ -3510,6 +3564,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Proxy Dial Resolution Strategy 的本地化字符串。
/// </summary>
public static string TbProxyDialResolveStrategy {
get {
return ResourceManager.GetString("TbProxyDialResolveStrategy", resourceCulture);
}
}
/// <summary>
/// 查找类似 Not recommended; may cause routing loops. 的本地化字符串。
/// </summary>
public static string TbProxyDialResolveStrategyTip {
get {
return ResourceManager.GetString("TbProxyDialResolveStrategyTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Public Key 的本地化字符串。 /// 查找类似 Public Key 的本地化字符串。
/// </summary> /// </summary>
@@ -3609,6 +3681,24 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Root Certificate Provider 的本地化字符串。
/// </summary>
public static string TbRootCertificateProvider {
get {
return ResourceManager.GetString("TbRootCertificateProvider", resourceCulture);
}
}
/// <summary>
/// 查找类似 Only applies to the v2rayN GUI&apos;s downloads and network requests. Does not affect the core&apos;s certificate validation. 的本地化字符串。
/// </summary>
public static string TbRootCertificateProviderTip {
get {
return ResourceManager.GetString("TbRootCertificateProviderTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Round Robin 的本地化字符串。 /// 查找类似 Round Robin 的本地化字符串。
/// </summary> /// </summary>
@@ -4968,6 +5058,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom HTTP outbound request headers as a JSON object with string or string array values. 的本地化字符串。
/// </summary>
public static string TipHttpOutboundHeaders {
get {
return ResourceManager.GetString("TipHttpOutboundHeaders", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 *Default value raw 的本地化字符串。 /// 查找类似 *Default value raw 的本地化字符串。
/// </summary> /// </summary>
+1 -1
View File
@@ -1495,7 +1495,7 @@
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value> <value>Please Add At Least One Configuration</value>
+1 -1
View File
@@ -1492,7 +1492,7 @@
<value>Choisir une config.</value> <value>Choisir une config.</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Actif globalement par défaut, avec filtre FakeIP intégré ; ne fonctionne que dans sing-box</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Veuillez ajouter au moins une configuration</value> <value>Veuillez ajouter au moins une configuration</value>
+1 -1
View File
@@ -1495,7 +1495,7 @@
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value> <value>Please Add At Least One Configuration</value>
+1 -1
View File
@@ -1495,7 +1495,7 @@
<value>Pilih profil</value> <value>Pilih profil</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Berlaku secara global secara default, dengan filter FakeIP bawaan (hanya sing-box).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Silakan tambahkan setidaknya satu konfigurasi.</value> <value>Silakan tambahkan setidaknya satu konfigurasi.</value>
+35 -2
View File
@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>Congestion control</value> <value>Congestion control</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP headers</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Custom HTTP outbound request headers as a JSON object with string or string array values.</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Previous proxy remarks</value> <value>Previous proxy remarks</value>
</data> </data>
@@ -1495,7 +1501,7 @@
<value>Select Profile</value> <value>Select Profile</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>Applies globally by default, with built-in FakeIP filtering (sing-box only).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Please Add At Least One Configuration</value> <value>Please Add At Least One Configuration</value>
@@ -1774,7 +1780,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<value>New Update</value> <value>New Update</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Warning: Xray will disable allowInsecure (skip certificate verification) in August 2026. Please switch to pinnedPeerCertSha256 (fixed certificate fingerprint) as soon as possible. allowInsecure will not be usable after its expiration.</value> <value>The current node uses an unencrypted connection, meaning your communications could be directly monitored by network intermediaries controlled by authoritarian governments. For security reasons, nodes of this type cannot connect via Xray-core versions 26.2.6 or higher. If this is a self-built node, please enable TLS or other secure encryption, or pin the certificate using pinSHA256. If this is an airport/provider node, please contact your service provider for a technical upgrade. If the provider refuses to cooperate, it is recommended to switch to a service provider that prioritizes user security. For more information, please visit https://github.com/2dust/v2rayN/discussions/9460.</value>
</data> </data>
<data name="TbRouteExcludeAddress" xml:space="preserve"> <data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Route Exclude Address</value> <value>Route Exclude Address</value>
@@ -1806,6 +1812,9 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Invalid Realm URL.</value> <value>Invalid Realm URL.</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Please enter valid HTTP request headers JSON.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value> <value>Format: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data> </data>
@@ -1815,4 +1824,28 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbLegacyProtectTip" xml:space="preserve"> <data name="TbLegacyProtectTip" xml:space="preserve">
<value>If enabled, use sing-box TUN; otherwise, use xray TUN.</value> <value>If enabled, use sing-box TUN; otherwise, use xray TUN.</value>
</data> </data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>Root Certificate Provider</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>Only applies to the v2rayN GUI's downloads and network requests. Does not affect the core's certificate validation.</value>
</data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>Proxy Dial Resolution Strategy</value>
</data>
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
<value>Not recommended; may cause routing loops.</value>
</data>
<data name="TbEnableHappyEyeballs" xml:space="preserve">
<value>Enable Happy Eyeballs</value>
</data>
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
<value>Requires the UseIP Strategy. When enabled, it attempts IPv4 and IPv6 connections simultaneously and automatically selects the faster available path.</value>
</data>
<data name="TbIpv6Address" xml:space="preserve">
<value>Ipv6 Address</value>
</data>
<data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 Address</value>
</data>
</root> </root>
+61 -1
View File
@@ -1044,6 +1044,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>Управление перегрузками</value> <value>Управление перегрузками</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP-заголовки</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>Пользовательские заголовки исходящего HTTP-запроса. Введите JSON-объект, значения которого — строка или массив строк.</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>Псевдоним предыдущего прокси</value> <value>Псевдоним предыдущего прокси</value>
</data> </data>
@@ -1495,7 +1501,7 @@
<value>Выбрать профиль</value> <value>Выбрать профиль</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>По умолчанию применяется глобально, со встроенной фильтрацией FakeIP (только sing-box).</value> <value>Applies globally by default, and built-in FakeIP filtering is only built into sing-box.</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>Добавьте хотя бы одну конфигурацию</value> <value>Добавьте хотя бы одну конфигурацию</value>
@@ -1614,6 +1620,9 @@
<data name="TbEchConfigList" xml:space="preserve"> <data name="TbEchConfigList" xml:space="preserve">
<value>EchConfigList</value> <value>EchConfigList</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Проверять сертификат узла по имени</value>
</data>
<data name="TbFullCertTips" xml:space="preserve"> <data name="TbFullCertTips" xml:space="preserve">
<value>Полный сертификат (цепочка) в формате PEM</value> <value>Полный сертификат (цепочка) в формате PEM</value>
</data> </data>
@@ -1770,4 +1779,55 @@
<data name="menuNewUpdate" xml:space="preserve"> <data name="menuNewUpdate" xml:space="preserve">
<value>Доступно обновление</value> <value>Доступно обновление</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>Предупреждение: 1 августа 2026 г. Xray отключит пропуск проверки сертификата (allowInsecure). Как можно скорее перейдите на привязанный отпечаток сертификата (pinnedPeerCertSha256). После этой даты allowInsecure использовать будет нельзя.</value>
</data>
<data name="TbRouteExcludeAddress" xml:space="preserve">
<value>Адреса, исключаемые из маршрутизации</value>
</data>
<data name="TbRouteExcludeAddressTip" xml:space="preserve">
<value>Разделяйте запятыми (,).</value>
</data>
<data name="MsgTunRouteExcludeInvalidAddress" xml:space="preserve">
<value>Недопустимый адрес в списке исключений маршрутизации TUN: {0}</value>
</data>
<data name="MsgOptionsConflict" xml:space="preserve">
<value>Конфликт между {0} и {1}</value>
</data>
<data name="TbEnableFinalFragment" xml:space="preserve">
<value>Включить финальную фрагментацию (Final Fragment)</value>
</data>
<data name="TbEnableFinalFragmentTip" xml:space="preserve">
<value>Разбивать конец пакетов на более мелкие фрагменты при отправке. Это может влиять на пропускную способность и задержку.</value>
</data>
<data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS через Bridge</value>
</data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>Обнаружена небезопасная конфигурация: AllowInsecure включён, но сертификат не предоставлен. Это может привести к атаке «человек посередине» (MITM).</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Некорректный Realm URL.</value>
</data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>Введите корректный JSON заголовков HTTP-запроса.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>Формат: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Размер пакета Gecko (мин/макс)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve">
<value>Если включено, используется sing-box TUN; иначе — xray TUN.</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>Поставщик корневых сертификатов</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>Применяется только к загрузкам и сетевым запросам графического интерфейса v2rayN. Не влияет на проверку сертификатов ядром.</value>
</data>
</root> </root>
+39 -2
View File
@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>拥塞控制算法</value> <value>拥塞控制算法</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP 请求头</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>自定义 HTTP 出站请求头,请输入值为字符串或字符串数组的 JSON 对象。</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>前置代理配置别名</value> <value>前置代理配置别名</value>
</data> </data>
@@ -1492,7 +1498,7 @@
<value>选择配置</value> <value>选择配置</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>默认全局生效,内置 FakeIP 过滤,仅在 sing-box 中生效</value> <value>默认全局生效,仅在 sing-box 中内置 FakeIP 过滤。</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>请至少添加一个配置</value> <value>请至少添加一个配置</value>
@@ -1768,7 +1774,11 @@
<value>有更新</value> <value>有更新</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告:Xray 将在 2026.8.1 禁用跳过证书验证 allowInsecure ,请尽快改用证书固定指纹 pinnedPeerCertSha256。到期后无法使用 </value> <value>当前节点使用未加密连接,您的通信可能会被威权政府掌控的网络中间设施直接查看
为了安全,此类节点无法通过 26.2.6+ 版本的Xray核心连接
如果是自建节点请启用 TLS 等安全加密,或固定证书 pinSHA256
如果机场节点请联系服务商完成技术升级,如服务商拒绝配合,建议更换更重视用户安全的服务商
更多的信息,请访问 https://github.com/2dust/v2rayN/discussions/9460</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve"> <data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value> <value>Verify Peer Cert By Name</value>
@@ -1803,6 +1813,9 @@
<data name="InvalidHy2RealmUrl" xml:space="preserve"> <data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>Realm URL 不正确。</value> <value>Realm URL 不正确。</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>请填写合法的HTTP请求头JSON。</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve"> <data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>格式:realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value> <value>格式:realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data> </data>
@@ -1812,4 +1825,28 @@
<data name="TbLegacyProtectTip" xml:space="preserve"> <data name="TbLegacyProtectTip" xml:space="preserve">
<value>启用则使用 sing-box TUN ,否则使用 xray TUN</value> <value>启用则使用 sing-box TUN ,否则使用 xray TUN</value>
</data> </data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>根证书提供者</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>仅用于 v2rayN 界面程序的下载及网络请求,不影响核心的证书验证。</value>
</data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>连接代理解析策略</value>
</data>
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
<value>不建议开启,特殊情况可能回环。</value>
</data>
<data name="TbEnableHappyEyeballs" xml:space="preserve">
<value>启用 Happy Eyeballs</value>
</data>
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
<value>需配合 UseIP 策略使用。启用后将同时尝试 IPv4 和 IPv6 连接,并自动选择更快可用的路径。</value>
</data>
<data name="TbIpv6Address" xml:space="preserve">
<value>Ipv6 地址</value>
</data>
<data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 地址</value>
</data>
</root> </root>
+54 -2
View File
@@ -1041,6 +1041,12 @@
<data name="TbHeaderType8" xml:space="preserve"> <data name="TbHeaderType8" xml:space="preserve">
<value>擁塞控制算法</value> <value>擁塞控制算法</value>
</data> </data>
<data name="TbHttpOutboundHeaders" xml:space="preserve">
<value>HTTP 請求頭</value>
</data>
<data name="TipHttpOutboundHeaders" xml:space="preserve">
<value>自訂 HTTP 出站請求頭,請輸入值為字串或字串陣列的 JSON 物件。</value>
</data>
<data name="LvPrevProfile" xml:space="preserve"> <data name="LvPrevProfile" xml:space="preserve">
<value>前置代理節點別名</value> <value>前置代理節點別名</value>
</data> </data>
@@ -1492,7 +1498,7 @@
<value>選擇節點</value> <value>選擇節點</value>
</data> </data>
<data name="TbFakeIPTips" xml:space="preserve"> <data name="TbFakeIPTips" xml:space="preserve">
<value>默認全域生效,內置 FakeIP 過濾,僅在 sing-box 中生效</value> <value>默認全局生效,僅在 sing-box 中內置 FakeIP 過濾。</value>
</data> </data>
<data name="PleaseAddAtLeastOneServer" xml:space="preserve"> <data name="PleaseAddAtLeastOneServer" xml:space="preserve">
<value>請至少添加一個節點</value> <value>請至少添加一個節點</value>
@@ -1768,7 +1774,11 @@
<value>有更新</value> <value>有更新</value>
</data> </data>
<data name="MsgAllowInsecureDeprecated" xml:space="preserve"> <data name="MsgAllowInsecureDeprecated" xml:space="preserve">
<value>警告:Xray 將在 2026.8.1 停用跳過憑證驗證 allowInsecure ,請盡快改用憑證固定指紋 pinnedPeerCertSha256。到期後無法使用 allowInsecure。</value> <value>目前節點使用未加密連接,您的通訊可能會被威權政府掌控的網路中間設施直接查看
為了安全,此類節點無法透過 26.2.6+ 版本的Xray核心連接
如果是自建節點請啟用 TLS 等安全加密,或固定憑證 pinSHA256
若機場節點請聯絡服務商完成技術升級,如服務商拒絕配合,建議更換更重視用戶安全的服務商
更多的信息,請訪問 https://github.com/2dust/v2rayN/discussions/9460</value>
</data> </data>
<data name="TbVerifyPeerCertByName" xml:space="preserve"> <data name="TbVerifyPeerCertByName" xml:space="preserve">
<value>Verify Peer Cert By Name</value> <value>Verify Peer Cert By Name</value>
@@ -1794,7 +1804,49 @@
<data name="TbEnabletDnsViaProxy" xml:space="preserve"> <data name="TbEnabletDnsViaProxy" xml:space="preserve">
<value>DNS 透過 Bridge</value> <value>DNS 透過 Bridge</value>
</data> </data>
<data name="MsgInsecureConfiguration" xml:space="preserve">
<value>偵測到不安全的設定:已啟用 AllowInsecure,但未提供憑證。這可能導致中間人攻擊(MITM)</value>
</data>
<data name="TbHy2RealmUrl" xml:space="preserve">
<value>Realm URL</value>
</data>
<data name="InvalidHy2RealmUrl" xml:space="preserve">
<value>無效的 Realm URL.</value>
</data>
<data name="TbHy2RealmUrlTip" xml:space="preserve">
<value>格式: realm://&lt;token&gt;@&lt;rendezvous-host&gt;[:port]/&lt;realm-name&gt;?stun=&lt;stun-host&gt;[:port]</value>
</data>
<data name="TbGeckoPacketSize" xml:space="preserve">
<value>Gecko 封包大小 (min/max)</value>
</data>
<data name="TbLegacyProtectTip" xml:space="preserve"> <data name="TbLegacyProtectTip" xml:space="preserve">
<value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value> <value>啟用則使用 sing-box TUN ,否則使用 xray TUN</value>
</data> </data>
<data name="InvalidHttpOutboundHeaders" xml:space="preserve">
<value>請填寫合法的HTTP請求頭JSON。</value>
</data>
<data name="TbRootCertificateProvider" xml:space="preserve">
<value>根憑證提供者</value>
</data>
<data name="TbRootCertificateProviderTip" xml:space="preserve">
<value>僅適用於 v2rayN GUI 的下載與網路請求,不影響核心的憑證驗證</value>
</data>
<data name="TbProxyDialResolveStrategy" xml:space="preserve">
<value>連線代理解析策略</value>
</data>
<data name="TbProxyDialResolveStrategyTip" xml:space="preserve">
<value>不建議啟用,特殊情況可能造成連線循環</value>
</data>
<data name="TbEnableHappyEyeballs" xml:space="preserve">
<value>啟用 Happy Eyeballs</value>
</data>
<data name="TbEnableHappyEyeballsTip" xml:space="preserve">
<value>需搭配 UseIP 策略,啟用後會同時嘗試 IPv4 與 IPv6 連線,並自動選擇速度較快的路徑</value>
</data>
<data name="TbIpv6Address" xml:space="preserve">
<value>Ipv6 位址</value>
</data>
<data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 位址</value>
</data>
</root> </root>
+26 -25
View File
@@ -1,27 +1,28 @@
{ {
"tag": "tun", "tag": "tun",
"protocol": "tun", "protocol": "tun",
"settings": { "settings": {
"name": "xray_tun", "name": "xray_tun",
"MTU": 9000, "MTU": 9000,
"gateway": [ "gateway": [
"172.18.0.1/30", "172.18.0.1/30",
"fdfe:dcba:9876::1/126" "fdfe:dcba:9876::1/126"
], ],
"dns": [ "dns": [
"172.18.0.1" "1.1.1.1",
], "8.8.8.8"
"autoSystemRoutingTable": [ ],
"0.0.0.0/0", "autoSystemRoutingTable": [
"::/0" "0.0.0.0/0",
], "::/0"
"autoOutboundsInterface": "auto" ],
}, "autoOutboundsInterface": "auto"
"sniffing": { },
"enabled": true, "sniffing": {
"destOverride": [ "enabled": true,
"http", "destOverride": [
"tls" "http",
] "tls"
} ]
}
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2
View File
@@ -23,6 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Sample\chrome_roots_pem" />
<EmbeddedResource Include="Sample\clash_mixin_yaml" /> <EmbeddedResource Include="Sample\clash_mixin_yaml" />
<EmbeddedResource Include="Sample\clash_tun_yaml" /> <EmbeddedResource Include="Sample\clash_tun_yaml" />
<EmbeddedResource Include="Sample\custom_routing_black" /> <EmbeddedResource Include="Sample\custom_routing_black" />
@@ -32,6 +33,7 @@
<EmbeddedResource Include="Sample\dns_v2ray_normal" /> <EmbeddedResource Include="Sample\dns_v2ray_normal" />
<EmbeddedResource Include="Sample\kill_as_sudo_linux_sh" /> <EmbeddedResource Include="Sample\kill_as_sudo_linux_sh" />
<EmbeddedResource Include="Sample\kill_as_sudo_osx_sh" /> <EmbeddedResource Include="Sample\kill_as_sudo_osx_sh" />
<EmbeddedResource Include="Sample\mozilla_roots_pem" />
<EmbeddedResource Include="Sample\pac" /> <EmbeddedResource Include="Sample\pac" />
<EmbeddedResource Include="Sample\proxy_set_linux_sh" /> <EmbeddedResource Include="Sample\proxy_set_linux_sh" />
<EmbeddedResource Include="Sample\proxy_set_osx_sh" /> <EmbeddedResource Include="Sample\proxy_set_osx_sh" />
@@ -1,9 +1,11 @@
namespace ServiceLib.Services.CoreConfig; namespace ServiceLib.Services.CoreConfig;
/// <summary> /// <summary>
/// Core configuration file processing class /// Core configuration file processing class.
/// The TUN state is taken as a snapshot so the generated config always agrees
/// with the launch elevation decision (see CoreManager.ShouldRunAsSudo).
/// </summary> /// </summary>
public class CoreConfigClashService(Config config) public class CoreConfigClashService(Config config, bool isTunEnabled)
{ {
private static readonly string _tag = "CoreConfigClashService"; private static readonly string _tag = "CoreConfigClashService";
@@ -102,7 +104,7 @@ public class CoreConfigClashService(Config config)
} }
//enable tun mode //enable tun mode
if (config.TunModeItem.EnableTun) if (isTunEnabled)
{ {
var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml); var tun = EmbedUtils.GetEmbedText(Global.ClashTunYaml);
if (tun.IsNotEmpty()) if (tun.IsNotEmpty())
@@ -171,7 +173,7 @@ public class CoreConfigClashService(Config config)
} }
foreach (var item in mixinContent) foreach (var item in mixinContent)
{ {
if (!config.TunModeItem.EnableTun && item.Key == "tun") if (!isTunEnabled && item.Key == "tun")
{ {
continue; continue;
} }
@@ -137,12 +137,12 @@ public partial class CoreConfigSingboxService
// fake ip // fake ip
if (simpleDnsItem.FakeIP == true) if (simpleDnsItem.FakeIP == true)
{ {
var fakeipRange = simpleDnsItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : simpleDnsItem.FakeIPRange;
var fakeip = new Server4Sbox var fakeip = new Server4Sbox
{ {
tag = Global.SingboxFakeDNSTag, tag = Global.SingboxFakeDNSTag,
type = "fakeip", type = "fakeip",
inet4_range = "198.18.0.0/15", inet4_range = fakeipRange,
inet6_range = "fc00::/18",
}; };
_coreConfig.dns.servers.Add(fakeip); _coreConfig.dns.servers.Add(fakeip);
} }
@@ -171,7 +171,7 @@ public partial class CoreConfigSingboxService
_coreConfig.dns.rules.Add(new() _coreConfig.dns.rules.Add(new()
{ {
server = Global.SingboxDirectDNSTag, server = Global.SingboxDirectDNSTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom), strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial),
domain = context.ProtectDomainList.ToList(), domain = context.ProtectDomainList.ToList(),
}); });
} }
@@ -266,7 +266,7 @@ public partial class CoreConfigSingboxService
}); });
} }
if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp == true) if (simpleDnsItem.FakeIP == true && simpleDnsItem.GlobalFakeIp != false)
{ {
var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName)); var fakeipFilterRule = JsonUtils.Deserialize<Rule4Sbox>(EmbedUtils.GetEmbedText(Global.SingboxFakeIPFilterFileName));
fakeipFilterRule.invert = true; fakeipFilterRule.invert = true;
@@ -67,9 +67,13 @@ public partial class CoreConfigSingboxService
tunInbound.auto_route = _config.TunModeItem.AutoRoute; tunInbound.auto_route = _config.TunModeItem.AutoRoute;
tunInbound.strict_route = _config.TunModeItem.StrictRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute;
tunInbound.stack = _config.TunModeItem.Stack; tunInbound.stack = _config.TunModeItem.Stack;
if (_config.TunModeItem.EnableIPv6Address == false)
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.address = [address];
if (_config.TunModeItem.EnableIPv6Address == true)
{ {
tunInbound.address = ["172.18.0.1/30"]; var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.address.Add(address6);
} }
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress; tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;
@@ -417,11 +417,15 @@ public partial class CoreConfigSingboxService
var tls = new Tls4Sbox() var tls = new Tls4Sbox()
{ {
enabled = true, enabled = true,
record_fragment = _config.CoreBasicItem.EnableFragment ? true : null,
server_name = serverName, server_name = serverName,
insecure = _node.GetAllowInsecure(), insecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(), alpn = _node.GetAlpn(),
}; };
if (_config.CoreBasicItem.EnableFragment == true)
{
tls.fragment = true;
tls.record_fragment = true;
}
if (_node.Fingerprint.IsNotEmpty()) if (_node.Fingerprint.IsNotEmpty())
{ {
tls.utls = new Utls4Sbox() tls.utls = new Utls4Sbox()
@@ -10,18 +10,27 @@ public partial class CoreConfigSingboxService
var simpleDnsItem = context.SimpleDnsItem; var simpleDnsItem = context.SimpleDnsItem;
var defaultDomainResolverTag = Global.SingboxDirectDNSTag; var defaultDomainResolverTag = Global.SingboxDirectDNSTag;
var directDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom); var dialDnsStrategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4ProxyDial);
var rawDNSItem = context.RawDnsItem; var rawDNSItem = context.RawDnsItem;
if (rawDNSItem is { Enabled: true }) if (rawDNSItem is { Enabled: true })
{ {
defaultDomainResolverTag = Global.SingboxLocalDNSTag; defaultDomainResolverTag = Global.SingboxLocalDNSTag;
directDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom; dialDnsStrategy = rawDNSItem.DomainStrategy4Freedom.IsNullOrEmpty() ? null : rawDNSItem.DomainStrategy4Freedom;
}
else if (!simpleDnsItem.Strategy4Freedom.IsNullOrEmpty())
{
var directOutbound = _coreConfig.outbounds.FirstOrDefault(o => o.tag == Global.DirectTag);
directOutbound?.domain_resolver = new()
{
server = defaultDomainResolverTag,
strategy = Utils.DomainStrategy4Sbox(simpleDnsItem.Strategy4Freedom),
};
} }
_coreConfig.route.default_domain_resolver = new() _coreConfig.route.default_domain_resolver = new()
{ {
server = defaultDomainResolverTag, server = defaultDomainResolverTag,
strategy = directDnsStrategy strategy = dialDnsStrategy
}; };
if (context.IsTunEnabled) if (context.IsTunEnabled)
@@ -34,19 +43,22 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.AddRange(tunRules); _coreConfig.route.rules.AddRange(tunRules);
} }
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe(); var lstDirectExe = BuildRoutingDirectExe();
_coreConfig.route.rules.Add(new() if (lstDirectExe.Count > 0)
{ {
port = [53], _coreConfig.route.rules.Add(new()
action = "hijack-dns", {
process_name = lstDnsExe port = [53],
}); action = "hijack-dns",
process_path = lstDirectExe,
});
_coreConfig.route.rules.Add(new() _coreConfig.route.rules.Add(new()
{ {
outbound = Global.DirectTag, outbound = Global.DirectTag,
process_name = lstDirectExe process_path = lstDirectExe,
}); });
}
// ICMP Routing // ICMP Routing
var icmpRouting = _config.TunModeItem.IcmpRouting ?? ""; var icmpRouting = _config.TunModeItem.IcmpRouting ?? "";
@@ -256,34 +268,38 @@ public partial class CoreConfigSingboxService
} }
} }
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe() private List<string> BuildRoutingDirectExe()
{ {
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); var allCoreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult) foreach (var coreConfig in allCoreInfo)
{ {
if (!context.ProtectCoreTypeList.Contains(coreConfig.CoreType))
{
continue;
}
if (coreConfig.CoreType == ECoreType.v2rayN) if (coreConfig.CoreType == ECoreType.v2rayN)
{ {
continue; continue;
} }
if (coreConfig.CoreExes == null)
{
continue;
}
foreach (var baseExeName in coreConfig.CoreExes) foreach (var baseExeName in coreConfig.CoreExes)
{ {
if (coreConfig.CoreType != ECoreType.sing_box) //directExeSet.Add(Utils.GetExeName(baseExeName));
var exePath = CoreInfoManager.Instance.GetCoreExecFile(coreConfig, out _);
if (!exePath.IsNullOrEmpty())
{ {
dnsExeSet.Add(Utils.GetExeName(baseExeName)); directExeSet.Add(exePath);
} }
directExeSet.Add(Utils.GetExeName(baseExeName));
} }
} }
var lstDnsExe = new List<string>(dnsExeSet); return directExeSet.ToList();
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
} }
private void GenRoutingUserRule(RulesItem? item) private void GenRoutingUserRule(RulesItem? item)
@@ -96,18 +96,19 @@ public partial class CoreConfigV2rayService
var balancer = new BalancersItem4Ray var balancer = new BalancersItem4Ray
{ {
selector = [selector], selector = [selector],
strategy = strategyType == "leastLoad" ? new() strategy = new()
{ {
type = strategyType, type = strategyType,
settings = new() settings = strategyType == "leastLoad"
{ ? new()
expected = 1, {
tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null, expected = 1,
maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null, tolerance = multipleLoad == EMultipleLoad.Fallback ? 0.2 : null,
}, maxRTT = multipleLoad == EMultipleLoad.Fallback ? "5000ms" : null,
} : null, }
: null,
},
tag = balancerTag, tag = balancerTag,
fallbackTag = _coreConfig.outbounds?.FirstOrDefault(o => o.tag.StartsWith(selector))?.tag,
}; };
_coreConfig.routing.balancers ??= []; _coreConfig.routing.balancers ??= [];
_coreConfig.routing.balancers.Add(balancer); _coreConfig.routing.balancers.Add(balancer);
@@ -28,7 +28,7 @@ public partial class CoreConfigV2rayService
_coreConfig.routing.rules.Add(new RulesItem4Ray _coreConfig.routing.rules.Add(new RulesItem4Ray
{ {
type = "field", type = "field",
inboundTag = new List<string> { Global.DnsTag }, inboundTag = [Global.DnsTag],
outboundTag = Global.ProxyTag, outboundTag = Global.ProxyTag,
}); });
return; return;
@@ -43,11 +43,7 @@ public partial class CoreConfigV2rayService
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null) if (outbound != null)
{ {
outbound.settings = new() FillSockoptDomainStrategy(outbound, strategy4Freedom);
{
domainStrategy = strategy4Freedom,
userLevel = 0
};
} }
} }
@@ -64,6 +60,22 @@ public partial class CoreConfigV2rayService
.ForEach(outbound => outbound.targetStrategy = strategy4Proxy); .ForEach(outbound => outbound.targetStrategy = strategy4Proxy);
} }
var strategy4DialProxy = simpleDnsItem?.Strategy4ProxyDial ?? Global.AsIs;
//Outbound DialProxy domainStrategy
if (strategy4DialProxy.IsNotEmpty() && strategy4DialProxy != Global.AsIs)
{
var xraySupportConfigTypeNames = Global.XraySupportConfigType
.Select(x => x == EConfigType.Hysteria2 ? "hysteria" : Global.ProtocolTypes[x])
.ToHashSet();
_coreConfig.outbounds
.Where(t => xraySupportConfigTypeNames.Contains(t.protocol))
.ToList()
.ForEach(outbound =>
{
FillSockoptDomainStrategy(outbound, strategy4DialProxy);
});
}
FillDnsServers(dnsItem); FillDnsServers(dnsItem);
FillDnsHosts(dnsItem); FillDnsHosts(dnsItem);
@@ -108,6 +120,33 @@ public partial class CoreConfigV2rayService
} }
} }
private void GenFakeDns()
{
var fakeipRange = _config.SimpleDNSItem.FakeIPRange.IsNullOrEmpty() ? Global.FakeIPRanges.First() : _config.SimpleDNSItem.FakeIPRange;
var poolSize = 65535L;
try
{
var fakeipNetwork = IPNetwork2.Parse(fakeipRange);
var totalIPs = fakeipNetwork.Total;
// see https://github.com/XTLS/Xray-core/blob/6e3322d219140a025285ded1114fe17a5edb74d8/app/dns/fakedns/fake.go#L88
// if math.Log2(float64(lruSize)) >= float64(rooms) { return errors.New("LRU size is bigger than subnet size").AtError() }
totalIPs -= 1;
if (totalIPs > 0)
{
poolSize = (totalIPs >= long.MaxValue) ? long.MaxValue : (long)totalIPs;
}
}
catch
{
// Ignore
}
_coreConfig.fakedns = new()
{
ipPool = fakeipRange,
poolSize = poolSize,
};
}
private void FillDnsServers(Dns4Ray dnsItem) private void FillDnsServers(Dns4Ray dnsItem)
{ {
var simpleDNSItem = context.SimpleDnsItem; var simpleDNSItem = context.SimpleDnsItem;
@@ -234,6 +273,23 @@ public partial class CoreConfigV2rayService
var directDnsTagIndex = 1; var directDnsTagIndex = 1;
if (simpleDNSItem.FakeIP == true)
{
var fakeIPMatchDomain = new HashSet<string>(proxyDomainList);
fakeIPMatchDomain.UnionWith(proxyGeositeList);
if (simpleDNSItem.GlobalFakeIp != false)
{
fakeIPMatchDomain.UnionWith(directDomainList);
fakeIPMatchDomain.UnionWith(directGeositeList);
fakeIPMatchDomain.UnionWith(expectedDomainList);
}
if (fakeIPMatchDomain.Count > 0)
{
GenFakeDns();
AddDnsServers(["fakedns"], fakeIPMatchDomain.ToList());
}
}
AddDnsServers(remoteDNSAddress, proxyDomainList); AddDnsServers(remoteDNSAddress, proxyDomainList);
AddDnsServers(directDNSAddress, directDomainList, true); AddDnsServers(directDNSAddress, directDomainList, true);
AddDnsServers(remoteDNSAddress, proxyGeositeList); AddDnsServers(remoteDNSAddress, proxyGeositeList);
@@ -384,11 +440,9 @@ public partial class CoreConfigV2rayService
var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag }); var outbound = _coreConfig.outbounds.FirstOrDefault(t => t is { protocol: "freedom", tag: Global.DirectTag });
if (outbound != null) if (outbound != null)
{ {
outbound.settings = new() outbound.streamSettings ??= new();
{ outbound.streamSettings.sockopt ??= new();
domainStrategy = domainStrategy4Freedom, outbound.streamSettings.sockopt.domainStrategy = domainStrategy4Freedom;
userLevel = 0,
};
} }
} }
@@ -471,4 +525,28 @@ public partial class CoreConfigV2rayService
}; };
servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer)); servers.AsArray().Add(JsonUtils.SerializeToNode(dnsServer));
} }
private void FillSockoptDomainStrategy(Outbounds4Ray outbound, string? domainStrategy, bool skipHappyEyeballs = false)
{
if (domainStrategy.IsNullOrEmpty())
{
return;
}
outbound.streamSettings ??= new();
outbound.streamSettings.sockopt ??= new();
var sockopt = outbound.streamSettings.sockopt;
sockopt.domainStrategy = domainStrategy;
if (skipHappyEyeballs || _config.SimpleDNSItem.EnableHappyEyeballs != true)
{
return;
}
var happyEyeballsItem = _config.HappyEyeballs4RayItem;
sockopt.happyEyeballs ??= new();
var happyEyeballs = sockopt.happyEyeballs;
happyEyeballs.tryDelayMs = happyEyeballsItem.TryDelayMs;
happyEyeballs.prioritizeIPv6 = happyEyeballsItem.PrioritizeIPv6;
happyEyeballs.interleave = happyEyeballsItem.Interleave;
happyEyeballs.maxConcurrentTry = happyEyeballsItem.MaxConcurrentTry;
}
} }
@@ -63,17 +63,24 @@ public partial class CoreConfigV2rayService
new Inbounds4Ray(); new Inbounds4Ray();
tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun"; tunInbound.settings.name = context.IsMacOS ? $"utun{new Random().Next(99)}" : "xray_tun";
tunInbound.settings.MTU = _config.TunModeItem.Mtu; tunInbound.settings.MTU = _config.TunModeItem.Mtu;
if (!_config.TunModeItem.EnableIPv6Address)
var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.settings.gateway = [address];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
if (_config.TunModeItem.EnableIPv6Address == true)
{ {
tunInbound.settings.gateway = ["172.18.0.1/30"]; var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"]; tunInbound.settings.gateway.Add(address6);
tunInbound.settings.autoSystemRoutingTable.Add("::/0");
} }
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty()) if (!bindInterface.IsNullOrEmpty())
{ {
tunInbound.settings.autoOutboundsInterface = bindInterface; tunInbound.settings.autoOutboundsInterface = bindInterface;
} }
tunInbound.sniffing = inbound.sniffing; tunInbound.sniffing = inbound.sniffing;
tunInbound.sniffing.routeOnly = true;
if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (_config.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{ {
@@ -151,6 +158,16 @@ public partial class CoreConfigV2rayService
inbound.sniffing.destOverride = inItem.DestOverride; inbound.sniffing.destOverride = inItem.DestOverride;
inbound.sniffing.routeOnly = inItem.RouteOnly; inbound.sniffing.routeOnly = inItem.RouteOnly;
if (_config.SimpleDNSItem.FakeIP == true)
{
// Ensure destOverride contains "fakedns" if FakeIP is enabled
inbound.sniffing.destOverride ??= [];
if (!inbound.sniffing.destOverride.Contains("fakedns"))
{
inbound.sniffing.destOverride.Add("fakedns");
}
}
return inbound; return inbound;
} }
} }
@@ -136,7 +136,6 @@ public partial class CoreConfigV2rayService
break; break;
} }
case EConfigType.SOCKS: case EConfigType.SOCKS:
case EConfigType.HTTP:
{ {
ServersItem4Ray serversItem; ServersItem4Ray serversItem;
if (outbound.settings.servers.Count <= 0) if (outbound.settings.servers.Count <= 0)
@@ -171,6 +170,31 @@ public partial class CoreConfigV2rayService
outbound.settings.vnext = null; outbound.settings.vnext = null;
break; break;
} }
case EConfigType.HTTP:
{
outbound.settings.address = _node.Address;
outbound.settings.port = _node.Port;
if (protocolExtra.HttpHeaders.IsNotEmpty())
{
outbound.settings.headers = JsonUtils.ParseJson(protocolExtra.HttpHeaders);
}
if (_node.Username.IsNotEmpty()
&& _node.Password.IsNotEmpty())
{
outbound.settings.user = _node.Username;
outbound.settings.pass = _node.Password;
outbound.settings.level = 1;
outbound.settings.email = Global.UserEMail;
}
FillOutboundMux(outbound);
outbound.settings.vnext = null;
outbound.settings.servers = null;
break;
}
case EConfigType.VLESS: case EConfigType.VLESS:
{ {
VnextItem4Ray vnextItem; VnextItem4Ray vnextItem;
@@ -841,21 +865,10 @@ public partial class CoreConfigV2rayService
&& (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true)) && (n.streamSettings?.sockopt?.dialerProxy?.IsNullOrEmpty() ?? true))
.ToList(); .ToList();
var (fragmentMask, noiseMask) = BuildFragmentsMasks(); var fragmentMask = BuildFragmentsMasks();
foreach (var outbound in actOutboundWithTlsList) foreach (var outbound in actOutboundWithTlsList)
{ {
//var packets = configPackets;
//if (outbound.streamSettings.security == Global.StreamSecurityReality
// && packets == "tlshello")
//{
// packets = "1-3";
//}
//else if (outbound.streamSettings.security == Global.StreamSecurity
// && packets != "tlshello")
//{
// packets = "tlshello";
//}
var finalMaskJsonObj = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings?.finalmask)) as JsonObject ?? new JsonObject(); var finalMaskJsonObj = JsonUtils.ParseJson(JsonUtils.Serialize(outbound.streamSettings?.finalmask)) as JsonObject ?? new JsonObject();
// tcp fragment // tcp fragment
var tcpFinalmaskList = finalMaskJsonObj["tcp"] as JsonArray ?? []; var tcpFinalmaskList = finalMaskJsonObj["tcp"] as JsonArray ?? [];
@@ -864,13 +877,6 @@ public partial class CoreConfigV2rayService
tcpFinalmaskList.Add(JsonUtils.SerializeToNode(fragmentMask)); tcpFinalmaskList.Add(JsonUtils.SerializeToNode(fragmentMask));
finalMaskJsonObj["tcp"] = tcpFinalmaskList; finalMaskJsonObj["tcp"] = tcpFinalmaskList;
} }
// udp noise
var udpFinalmaskList = finalMaskJsonObj["udp"] as JsonArray ?? [];
if (udpFinalmaskList.Count == 0)
{
udpFinalmaskList.Add(JsonUtils.SerializeToNode(noiseMask));
finalMaskJsonObj["udp"] = udpFinalmaskList;
}
// write back // write back
outbound.streamSettings.finalmask = finalMaskJsonObj; outbound.streamSettings.finalmask = finalMaskJsonObj;
} }
@@ -878,7 +884,7 @@ public partial class CoreConfigV2rayService
private void ApplyFinalFragment() private void ApplyFinalFragment()
{ {
var (fragmentMask, noiseMask) = BuildFragmentsMasks(); var fragmentMask = BuildFragmentsMasks();
var actOutboundList = _coreConfig.outbounds.Where(n => n.tag.StartsWith(Global.ProxyTag)).ToList(); var actOutboundList = _coreConfig.outbounds.Where(n => n.tag.StartsWith(Global.ProxyTag)).ToList();
var fragmentFreedom = new Outbounds4Ray() var fragmentFreedom = new Outbounds4Ray()
@@ -890,9 +896,8 @@ public partial class CoreConfigV2rayService
finalmask = new Finalmask4Ray finalmask = new Finalmask4Ray
{ {
tcp = [fragmentMask], tcp = [fragmentMask],
udp = [noiseMask], },
} },
}
}; };
foreach (var outbound in actOutboundList) foreach (var outbound in actOutboundList)
@@ -910,13 +915,22 @@ public partial class CoreConfigV2rayService
} }
} }
private (Mask4Ray tcpFragment, Mask4Ray udpNoise) BuildFragmentsMasks() private Mask4Ray BuildFragmentsMasks()
{ {
var configPackets = _config.Fragment4RayItem?.Packets.NullIfEmpty() ?? "tlshello"; var configPackets = _config.Fragment4RayItem?.Packets.NullIfEmpty() ?? "tlshello";
var configLength = _config.Fragment4RayItem?.Length.NullIfEmpty() ?? "50-100"; var configLengths = _config.Fragment4RayItem?.Lengths ?? [];
var configDelay = _config.Fragment4RayItem?.Interval.NullIfEmpty() ?? "10-20"; var configDelays = _config.Fragment4RayItem?.Delays ?? [];
var configMaxSplit = _config.Fragment4RayItem?.MaxSplit.NullIfEmpty() ?? "0"; var configMaxSplit = _config.Fragment4RayItem?.MaxSplit.NullIfEmpty() ?? "0";
if (configLengths.Count == 0)
{
configLengths = ["50-100"];
}
if (configDelays.Count == 0)
{
configDelays = ["10-20"];
}
var maxSplit = 0; var maxSplit = 0;
var parts = configMaxSplit.Split('-'); var parts = configMaxSplit.Split('-');
if (parts.Length > 0 && int.TryParse(parts[0], out var ms)) if (parts.Length > 0 && int.TryParse(parts[0], out var ms))
@@ -930,21 +944,15 @@ public partial class CoreConfigV2rayService
settings = new MaskSettings4Ray settings = new MaskSettings4Ray
{ {
packets = configPackets, packets = configPackets,
length = configLength, lengths = configLengths,
delay = configDelay, delays = configDelays,
maxSplit = maxSplit, maxSplit = maxSplit,
} // For legacy xray compatibility, remove this in the future
}; length = configLengths.FirstOrDefault(),
var noiseMask = new Mask4Ray delay = configDelays.FirstOrDefault(),
{ },
type = "noise",
settings = new MaskSettings4Ray
{
length = "10-20",
delay = "10-16",
}
}; };
return (fragmentMask, noiseMask); return fragmentMask;
} }
} }
@@ -13,18 +13,21 @@ public partial class CoreConfigV2rayService
{ {
_coreConfig.routing.rules.AddRange(tunRules); _coreConfig.routing.rules.AddRange(tunRules);
} }
var (lstDnsExe, lstDirectExe) = BuildRoutingDirectExe(); var lstDirectExe = BuildRoutingDirectExe();
_coreConfig.routing.rules.Add(new() if (lstDirectExe.Count > 0)
{ {
port = "53", _coreConfig.routing.rules.Add(new()
process = lstDnsExe, {
outboundTag = Global.DnsOutboundTag, port = "53",
}); process = lstDirectExe,
_coreConfig.routing.rules.Add(new() outboundTag = Global.DnsOutboundTag,
{ });
process = lstDirectExe, _coreConfig.routing.rules.Add(new()
outboundTag = Global.DirectTag, {
}); process = lstDirectExe,
outboundTag = Global.DirectTag,
});
}
_coreConfig.routing.rules.Add(new() _coreConfig.routing.rules.Add(new()
{ {
inboundTag = ["tun"], inboundTag = ["tun"],
@@ -232,36 +235,44 @@ public partial class CoreConfigV2rayService
return finalRule; return finalRule;
} }
private static (List<string> lstDnsExe, List<string> lstDirectExe) BuildRoutingDirectExe() private List<string> BuildRoutingDirectExe()
{ {
var dnsExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var directExeSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var coreInfoResult = CoreInfoManager.Instance.GetCoreInfo(); var allCoreInfo = CoreInfoManager.Instance.GetCoreInfo();
foreach (var coreConfig in coreInfoResult) foreach (var coreConfig in allCoreInfo)
{ {
if (!context.ProtectCoreTypeList.Contains(coreConfig.CoreType))
{
continue;
}
if (coreConfig.CoreType == ECoreType.v2rayN) if (coreConfig.CoreType == ECoreType.v2rayN)
{ {
continue; continue;
} }
if (coreConfig.CoreExes == null)
{
continue;
}
if (coreConfig.CoreType == ECoreType.Xray)
{
directExeSet.Add("xray/");
continue;
}
foreach (var baseExeName in coreConfig.CoreExes) foreach (var baseExeName in coreConfig.CoreExes)
{ {
if (coreConfig.CoreType != ECoreType.Xray) //directExeSet.Add(Utils.GetExeName(baseExeName));
var exePath = CoreInfoManager.Instance.GetCoreExecFile(coreConfig, out _);
if (!exePath.IsNullOrEmpty())
{ {
dnsExeSet.Add(Utils.GetExeName(baseExeName)); directExeSet.Add(exePath);
} }
directExeSet.Add(Utils.GetExeName(baseExeName));
} }
} }
//directExeSet.Add("xray/");
directExeSet.Add("xray/");
directExeSet.Add("self/"); directExeSet.Add("self/");
var lstDnsExe = new List<string>(dnsExeSet); return directExeSet.ToList();
var lstDirectExe = new List<string>(directExeSet);
return (lstDnsExe, lstDirectExe);
} }
} }
+13 -1
View File
@@ -80,7 +80,13 @@ public class DownloadService
AllowAutoRedirect = false, AllowAutoRedirect = false,
Proxy = await GetWebProxy(blProxy) Proxy = await GetWebProxy(blProxy)
}; };
var client = new HttpClient(webRequestHandler); var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
{
webRequestHandler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
webRequestHandler.SslOptions.RemoteCertificateValidationCallback = null;
}
using var client = new HttpClient(webRequestHandler);
var response = await client.GetAsync(url); var response = await client.GetAsync(url);
if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null) if (response.StatusCode == HttpStatusCode.Redirect && response.Headers.Location is not null)
@@ -163,6 +169,12 @@ public class DownloadService
UseProxy = webProxy != null, UseProxy = webProxy != null,
ConnectTimeout = TimeSpan.FromSeconds(connectTimeout) ConnectTimeout = TimeSpan.FromSeconds(connectTimeout)
}; };
var certificateChainPolicy = CertPemManager.Instance.BuildCertificateChainPolicy();
if (certificateChainPolicy != null)
{
handler.SslOptions.CertificateChainPolicy = certificateChainPolicy;
handler.SslOptions.RemoteCertificateValidationCallback = null;
}
using var client = new HttpClient(handler) using var client = new HttpClient(handler)
{ {
@@ -425,7 +425,7 @@ public class SpeedtestService(Config config, Func<SpeedTestResult, Task> updateF
ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime); ProfileExManager.Instance.SetTestDelay(it.IndexId, responseTime);
await UpdateFunc(it.IndexId, responseTime.ToString()); await UpdateFunc(it.IndexId, responseTime.ToString());
if (responseTime > 0) if (!_config.UiItem.HideColumnIpInfo && responseTime > 0)
{ {
var ipInfo = await ConnectionHandler.GetIPInfo(webProxy); var ipInfo = await ConnectionHandler.GetIPInfo(webProxy);
var ipStr = ipInfo?.ToString() ?? Global.None; var ipStr = ipInfo?.ToString() ?? Global.None;
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddGroupServerViewModel : MyReactiveObject public class AddGroupServerViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -29,7 +31,7 @@ public class AddGroupServerViewModel : MyReactiveObject
public IObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>(); public IObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; } public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; } public ReactiveCommand<Unit, Unit> RemoveCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@@ -39,15 +41,18 @@ public class AddGroupServerViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddGroupServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddGroupServerViewModel(ProfileItem profileItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedChild, x => x.SelectedChild,
SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty()); selectedChild => selectedChild != null && !selectedChild.Remarks.IsNullOrEmpty());
AddCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddChildAsync();
});
RemoveCmd = ReactiveCommand.CreateFromTask(async () => RemoveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await ChildRemoveAsync(); await ChildRemoveAsync();
@@ -103,6 +108,20 @@ public class AddGroupServerViewModel : MyReactiveObject
ChildItemsObs.AddRange(childItemList); ChildItemsObs.AddRange(childItemList);
} }
public async Task AddChildAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
profileSelectViewModel.MultiSelect = true;
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return;
}
var profiles = await profileSelectViewModel.GetProfileItems() ?? [];
ChildItemsObs.AddRange(profiles);
}
public async Task ChildRemoveAsync() public async Task ChildRemoveAsync()
{ {
if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty())
@@ -230,7 +249,7 @@ public class AddGroupServerViewModel : MyReactiveObject
if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -1,7 +1,11 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddServer2ViewModel : MyReactiveObject public class AddServer2ViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public Interaction<Unit, string?> BrowseConfigFileInteraction { get; } = new();
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -13,15 +17,18 @@ public class AddServer2ViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveServerCmd { get; } public ReactiveCommand<Unit, Unit> SaveServerCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }
public AddServer2ViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddServer2ViewModel(ProfileItem profileItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
_updateView?.Invoke(EViewAction.BrowseServer, null); var fileName = await BrowseConfigFileInteraction.Handle(Unit.Default);
await Task.CompletedTask; if (fileName.IsNullOrEmpty())
{
return;
}
await BrowseServer(fileName);
}); });
EditServerCmd = ReactiveCommand.CreateFromTask(async () => EditServerCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -55,7 +62,7 @@ public class AddServer2ViewModel : MyReactiveObject
if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddServerViewModel : MyReactiveObject public class AddServerViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public ProfileItem SelectedSource { get; set; }
@@ -80,6 +82,9 @@ public class AddServerViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool NaiveQuic { get; set; } public bool NaiveQuic { get; set; }
[Reactive]
public string HttpHeadersJson { get; set; }
[Reactive] [Reactive]
public string Hy2RealmUrl { get; set; } public string Hy2RealmUrl { get; set; }
@@ -238,10 +243,9 @@ public class AddServerViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; } public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView) public AddServerViewModel(ProfileItem profileItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
FetchCertCmd = ReactiveCommand.CreateFromTask(async () => FetchCertCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -255,7 +259,6 @@ public class AddServerViewModel : MyReactiveObject
{ {
await SaveServerAsync(); await SaveServerAsync();
}); });
this.WhenAnyValue(x => x.Cert) this.WhenAnyValue(x => x.Cert)
.Subscribe(_ => UpdateCertTip()); .Subscribe(_ => UpdateCertTip());
@@ -311,6 +314,7 @@ public class AddServerViewModel : MyReactiveObject
CongestionControl = protocolExtra.CongestionControl ?? string.Empty; CongestionControl = protocolExtra.CongestionControl ?? string.Empty;
InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null; InsecureConcurrency = protocolExtra.InsecureConcurrency > 0 ? protocolExtra.InsecureConcurrency : null;
NaiveQuic = protocolExtra.NaiveQuic ?? false; NaiveQuic = protocolExtra.NaiveQuic ?? false;
HttpHeadersJson = protocolExtra.HttpHeaders ?? string.Empty;
Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty; Hy2RealmUrl = protocolExtra.Hy2RealmUrl ?? string.Empty;
GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt(); GeckoMinPacketSize = protocolExtra.GeckoMinPacketSize.ToInt();
GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt(); GeckoMaxPacketSize = protocolExtra.GeckoMaxPacketSize.ToInt();
@@ -379,6 +383,11 @@ public class AddServerViewModel : MyReactiveObject
return; return;
} }
} }
if (HttpHeadersJson.IsNotEmpty() && JsonUtils.ParseJson(HttpHeadersJson) == null)
{
NoticeManager.Instance.Enqueue(ResUI.InvalidHttpOutboundHeaders);
return;
}
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse; SelectedSource.AllowInsecure = AllowInsecure ? Global.StringTrue : Global.StringFalse;
SelectedSource.MuxEnabled = MuxEnabled; SelectedSource.MuxEnabled = MuxEnabled;
@@ -416,6 +425,7 @@ public class AddServerViewModel : MyReactiveObject
VmessSecurity = VmessSecurity.NullIfEmpty(), VmessSecurity = VmessSecurity.NullIfEmpty(),
VlessEncryption = VlessEncryption.NullIfEmpty(), VlessEncryption = VlessEncryption.NullIfEmpty(),
SsMethod = SsMethod.NullIfEmpty(), SsMethod = SsMethod.NullIfEmpty(),
HttpHeaders = SelectedSource.ConfigType == EConfigType.HTTP ? HttpHeadersJson.NullIfEmpty() : null,
WgPublicKey = WgPublicKey.NullIfEmpty(), WgPublicKey = WgPublicKey.NullIfEmpty(),
WgPresharedKey = WgPresharedKey.NullIfEmpty(), WgPresharedKey = WgPresharedKey.NullIfEmpty(),
WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(), WgInterfaceAddress = WgInterfaceAddress.NullIfEmpty(),
@@ -434,7 +444,7 @@ public class AddServerViewModel : MyReactiveObject
if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) if (await ConfigHandler.AddServer(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -13,12 +13,11 @@ public class BackupAndRestoreViewModel : MyReactiveObject
public WebDavItem SelectedSource { get; set; } public WebDavItem SelectedSource { get; set; }
[Reactive] [Reactive]
public string OperationMsg { get; set; } public string OperationMsg { get; set; } = string.Empty;
public BackupAndRestoreViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public BackupAndRestoreViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -7,15 +7,16 @@ public class CheckUpdateViewModel : MyReactiveObject
private List<CheckUpdateModel> _lstUpdated = []; private List<CheckUpdateModel> _lstUpdated = [];
private static readonly string _tag = "CheckUpdateViewModel"; private static readonly string _tag = "CheckUpdateViewModel";
public EventChannel<Unit> ReloadRequested { get; } = new();
public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>(); public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>();
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; } public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; } public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; }
public CheckUpdateViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public CheckUpdateViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate); CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate);
CheckUpdateCmd.ThrownExceptions.Subscribe(ex => CheckUpdateCmd.ThrownExceptions.Subscribe(ex =>
@@ -299,7 +300,7 @@ public class CheckUpdateViewModel : MyReactiveObject
{ {
if (blReload) if (blReload)
{ {
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
} }
else else
{ {
@@ -16,10 +16,9 @@ public class ClashConnectionsViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public bool AutoRefresh { get; set; }
public ClashConnectionsViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ClashConnectionsViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
@@ -33,10 +33,9 @@ public class ClashProxiesViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public bool AutoRefresh { get; set; }
public ClashProxiesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ClashProxiesViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () => ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -86,15 +85,6 @@ public class ClashProxiesViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand #endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ProxiesReloadRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await ProxiesReload());
#endregion AppEvents
_ = Init(); _ = Init();
} }
@@ -1,31 +1,36 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class DNSSettingViewModel : MyReactiveObject public class DNSSettingViewModel : MyReactiveObject, ICloseable
{ {
[Reactive] public bool? UseSystemHosts { get; set; } public event EventHandler? RequestClose;
[Reactive] public bool? AddCommonHosts { get; set; }
[Reactive] public bool? FakeIP { get; set; } [Reactive] public bool UseSystemHosts { get; set; }
[Reactive] public bool? BlockBindingQuery { get; set; } [Reactive] public bool AddCommonHosts { get; set; }
[Reactive] public string? DirectDNS { get; set; } [Reactive] public bool FakeIP { get; set; }
[Reactive] public string? RemoteDNS { get; set; } [Reactive] public string FakeIPRange { get; set; }
[Reactive] public string? BootstrapDNS { get; set; } [Reactive] public bool BlockBindingQuery { get; set; }
[Reactive] public string? Strategy4Freedom { get; set; } [Reactive] public string DirectDNS { get; set; }
[Reactive] public string? Strategy4Proxy { get; set; } [Reactive] public string RemoteDNS { get; set; }
[Reactive] public string? Hosts { get; set; } [Reactive] public string BootstrapDNS { get; set; }
[Reactive] public string? DirectExpectedIPs { get; set; } [Reactive] public string Strategy4Freedom { get; set; }
[Reactive] public bool? ParallelQuery { get; set; } [Reactive] public string Strategy4Proxy { get; set; }
[Reactive] public bool? ServeStale { get; set; } [Reactive] public string Strategy4ProxyDial { get; set; }
[Reactive] public string Hosts { get; set; }
[Reactive] public string DirectExpectedIPs { get; set; }
[Reactive] public bool ParallelQuery { get; set; }
[Reactive] public bool ServeStale { get; set; }
[Reactive] public bool EnableHappyEyeballs { get; set; }
[Reactive] public bool UseSystemHostsCompatible { get; set; } [Reactive] public bool UseSystemHostsCompatible { get; set; }
[Reactive] public string DomainStrategy4FreedomCompatible { get; set; } [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } = string.Empty;
[Reactive] public string DomainDNSAddressCompatible { get; set; } [Reactive] public string DomainDNSAddressCompatible { get; set; } = string.Empty;
[Reactive] public string NormalDNSCompatible { get; set; } [Reactive] public string NormalDNSCompatible { get; set; } = string.Empty;
[Reactive] public string TunDNSCompatible { get; set; } [Reactive] public string TunDNSCompatible { get; set; } = string.Empty;
[Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } = string.Empty;
[Reactive] public string DomainDNSAddress2Compatible { get; set; } [Reactive] public string DomainDNSAddress2Compatible { get; set; } = string.Empty;
[Reactive] public string NormalDNS2Compatible { get; set; } [Reactive] public string NormalDNS2Compatible { get; set; } = string.Empty;
[Reactive] public string TunDNS2Compatible { get; set; } [Reactive] public string TunDNS2Compatible { get; set; } = string.Empty;
[Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public bool SBCustomDNSEnableCompatible { get; set; } [Reactive] public bool SBCustomDNSEnableCompatible { get; set; }
@@ -35,10 +40,9 @@ public class DNSSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; } public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; }
public DNSSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public DNSSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync); SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync);
ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -66,20 +70,22 @@ public class DNSSettingViewModel : MyReactiveObject
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
var item = _config.SimpleDNSItem; var item = _config.SimpleDNSItem;
UseSystemHosts = item.UseSystemHosts; UseSystemHosts = item.UseSystemHosts ?? false;
AddCommonHosts = item.AddCommonHosts; AddCommonHosts = item.AddCommonHosts ?? false;
FakeIP = item.FakeIP; FakeIP = item.FakeIP ?? false;
BlockBindingQuery = item.BlockBindingQuery; FakeIPRange = item.FakeIPRange ?? string.Empty;
DirectDNS = item.DirectDNS; BlockBindingQuery = item.BlockBindingQuery ?? false;
RemoteDNS = item.RemoteDNS; DirectDNS = item.DirectDNS ?? string.Empty;
BootstrapDNS = item.BootstrapDNS; RemoteDNS = item.RemoteDNS ?? string.Empty;
Strategy4Freedom = item.Strategy4Freedom; BootstrapDNS = item.BootstrapDNS ?? string.Empty;
Strategy4Proxy = item.Strategy4Proxy; Strategy4Freedom = item.Strategy4Freedom ?? string.Empty;
Hosts = item.Hosts; Strategy4Proxy = item.Strategy4Proxy ?? string.Empty;
DirectExpectedIPs = item.DirectExpectedIPs; Strategy4ProxyDial = item.Strategy4ProxyDial ?? string.Empty;
ParallelQuery = item.ParallelQuery; Hosts = item.Hosts ?? string.Empty;
ServeStale = item.ServeStale; DirectExpectedIPs = item.DirectExpectedIPs ?? string.Empty;
ParallelQuery = item.ParallelQuery ?? false;
ServeStale = item.ServeStale ?? false;
EnableHappyEyeballs = item.EnableHappyEyeballs ?? false;
var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray); var item1 = await AppManager.Instance.GetDNSItem(ECoreType.Xray);
RayCustomDNSEnableCompatible = item1.Enabled; RayCustomDNSEnableCompatible = item1.Enabled;
UseSystemHostsCompatible = item1.UseSystemHosts; UseSystemHostsCompatible = item1.UseSystemHosts;
@@ -101,17 +107,19 @@ public class DNSSettingViewModel : MyReactiveObject
_config.SimpleDNSItem.UseSystemHosts = UseSystemHosts; _config.SimpleDNSItem.UseSystemHosts = UseSystemHosts;
_config.SimpleDNSItem.AddCommonHosts = AddCommonHosts; _config.SimpleDNSItem.AddCommonHosts = AddCommonHosts;
_config.SimpleDNSItem.FakeIP = FakeIP; _config.SimpleDNSItem.FakeIP = FakeIP;
_config.SimpleDNSItem.FakeIPRange = FakeIPRange;
_config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery; _config.SimpleDNSItem.BlockBindingQuery = BlockBindingQuery;
_config.SimpleDNSItem.DirectDNS = DirectDNS; _config.SimpleDNSItem.DirectDNS = DirectDNS;
_config.SimpleDNSItem.RemoteDNS = RemoteDNS; _config.SimpleDNSItem.RemoteDNS = RemoteDNS;
_config.SimpleDNSItem.BootstrapDNS = BootstrapDNS; _config.SimpleDNSItem.BootstrapDNS = BootstrapDNS;
_config.SimpleDNSItem.Strategy4Freedom = Strategy4Freedom; _config.SimpleDNSItem.Strategy4Freedom = Strategy4Freedom;
_config.SimpleDNSItem.Strategy4Proxy = Strategy4Proxy; _config.SimpleDNSItem.Strategy4Proxy = Strategy4Proxy;
_config.SimpleDNSItem.Strategy4ProxyDial = Strategy4ProxyDial;
_config.SimpleDNSItem.Hosts = Hosts; _config.SimpleDNSItem.Hosts = Hosts;
_config.SimpleDNSItem.DirectExpectedIPs = DirectExpectedIPs; _config.SimpleDNSItem.DirectExpectedIPs = DirectExpectedIPs;
_config.SimpleDNSItem.ParallelQuery = ParallelQuery; _config.SimpleDNSItem.ParallelQuery = ParallelQuery;
_config.SimpleDNSItem.ServeStale = ServeStale; _config.SimpleDNSItem.ServeStale = ServeStale;
_config.SimpleDNSItem.EnableHappyEyeballs = EnableHappyEyeballs;
if (NormalDNSCompatible.IsNotEmpty()) if (NormalDNSCompatible.IsNotEmpty())
{ {
var obj = JsonUtils.ParseJson(NormalDNSCompatible); var obj = JsonUtils.ParseJson(NormalDNSCompatible);
@@ -183,9 +191,6 @@ public class DNSSettingViewModel : MyReactiveObject
await ConfigHandler.SaveDNSItems(_config, item2); await ConfigHandler.SaveDNSItems(_config, item2);
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
if (_updateView != null) RequestClose?.Invoke(this, EventArgs.Empty);
{
await _updateView(EViewAction.CloseWindow, null);
}
} }
} }
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class FullConfigTemplateViewModel : MyReactiveObject public class FullConfigTemplateViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
#region Reactive #region Reactive
[Reactive] [Reactive]
@@ -11,16 +13,16 @@ public class FullConfigTemplateViewModel : MyReactiveObject
public bool EnableFullConfigTemplate4Singbox { get; set; } public bool EnableFullConfigTemplate4Singbox { get; set; }
[Reactive] [Reactive]
public string FullConfigTemplate4Ray { get; set; } public string FullConfigTemplate4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullTunConfigTemplate4Ray { get; set; } public string FullTunConfigTemplate4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullConfigTemplate4Singbox { get; set; } public string FullConfigTemplate4Singbox { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullTunConfigTemplate4Singbox { get; set; } public string FullTunConfigTemplate4Singbox { get; set; } = string.Empty;
[Reactive] [Reactive]
public bool AddProxyOnly4Ray { get; set; } public bool AddProxyOnly4Ray { get; set; }
@@ -29,19 +31,18 @@ public class FullConfigTemplateViewModel : MyReactiveObject
public bool AddProxyOnly4Singbox { get; set; } public bool AddProxyOnly4Singbox { get; set; }
[Reactive] [Reactive]
public string ProxyDetour4Ray { get; set; } public string ProxyDetour4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string ProxyDetour4Singbox { get; set; } public string ProxyDetour4Singbox { get; set; } = string.Empty;
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
#endregion Reactive #endregion Reactive
public FullConfigTemplateViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public FullConfigTemplateViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SaveSettingAsync(); await SaveSettingAsync();
@@ -84,7 +85,7 @@ public class FullConfigTemplateViewModel : MyReactiveObject
} }
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_ = _updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
private async Task<bool> SaveXrayConfigAsync() private async Task<bool> SaveXrayConfigAsync()
@@ -1,15 +1,16 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class GlobalHotkeySettingViewModel : MyReactiveObject public class GlobalHotkeySettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
private readonly List<KeyEventItem> _globalHotkeys; private readonly List<KeyEventItem> _globalHotkeys;
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public GlobalHotkeySettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public GlobalHotkeySettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
_globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys); _globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys);
@@ -51,7 +52,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveConfig(_config) == 0) if (await ConfigHandler.SaveConfig(_config) == 0)
{ {
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -4,6 +4,21 @@ namespace ServiceLib.ViewModels;
public class MainWindowViewModel : MyReactiveObject public class MainWindowViewModel : MyReactiveObject
{ {
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, byte[]?> ScanScreenInteraction { get; } = new();
public Interaction<Unit, string?> BrowseImageFileInteraction { get; } = new();
public Interaction<bool?, Unit> ShowHideWindowInteraction { get; } = new();
public bool DesignMode { get; set; }
public ProfilesViewModel ProfilesViewModel { get; } = new();
public MsgViewModel MsgViewModel { get; } = new();
public ClashProxiesViewModel ClashProxiesViewModel { get; } = new();
public ClashConnectionsViewModel ClashConnectionsViewModel { get; } = new();
public CheckUpdateViewModel CheckUpdateViewModel { get; } = new();
public BackupAndRestoreViewModel BackupAndRestoreViewModel { get; } = new();
public StatusBarViewModel StatusBarViewModel { get; } = StatusBarViewModel.Instance;
#region Menu #region Menu
//servers //servers
@@ -67,15 +82,17 @@ public class MainWindowViewModel : MyReactiveObject
[Reactive] public bool BlNewUpdate { get; set; } [Reactive] public bool BlNewUpdate { get; set; }
[Reactive] public EGirdOrientation MainGirdOrientation { get; set; }
#endregion Menu #endregion Menu
#region Init #region Init
public MainWindowViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public MainWindowViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
BlIsWindows = Utils.IsWindows(); BlIsWindows = Utils.IsWindows();
MainGirdOrientation = _config.UiItem.MainGirdOrientation;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -191,7 +208,8 @@ public class MainWindowViewModel : MyReactiveObject
}); });
GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () => GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) var globalHotkeySettingViewModel = new GlobalHotkeySettingViewModel();
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(globalHotkeySettingViewModel) == true)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
} }
@@ -233,26 +251,11 @@ public class MainWindowViewModel : MyReactiveObject
#region AppEvents #region AppEvents
AppEvents.ReloadRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await Reload());
AppEvents.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
AppEvents.AddServerViaClipboardRequested AppEvents.AddServerViaClipboardRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null)); .Subscribe(async _ => await AddServerViaClipboardAsync(null));
AppEvents.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
AppEvents.HasUpdateNotified AppEvents.HasUpdateNotified
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
@@ -260,6 +263,53 @@ public class MainWindowViewModel : MyReactiveObject
#endregion AppEvents #endregion AppEvents
ProfilesViewModel.RefreshServersRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServers());
var vmReloadRequestedList = new List<IObservable<Unit>>
{
ProfilesViewModel.ReloadRequested.AsObservable(),
StatusBarViewModel.ReloadRequested.AsObservable(),
CheckUpdateViewModel.ReloadRequested.AsObservable(),
};
foreach (var reloadRequested in vmReloadRequestedList)
{
reloadRequested
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await Reload());
}
StatusBarViewModel.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
StatusBarViewModel.AddServerViaClipboardRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null));
StatusBarViewModel.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blShow =>
{
await ShowHideWindowInteraction.Handle(blShow);
});
StatusBarViewModel.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async indexId => await ProfilesViewModel.SetDefaultServer(indexId));
StatusBarViewModel.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
_ = Init(); _ = Init();
} }
@@ -267,18 +317,24 @@ public class MainWindowViewModel : MyReactiveObject
{ {
AppManager.Instance.ShowInTaskbar = true; AppManager.Instance.ShowInTaskbar = true;
if (DesignMode)
{
return;
}
//await ConfigHandler.InitBuiltinRouting(_config); //await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinFullConfigTemplate(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config);
await ProfileExManager.Instance.Init(); await ProfileExManager.Instance.Init();
await CoreManager.Instance.Init(_config, UpdateHandler); await CoreManager.Instance.Init(_config, UpdateHandler);
await CertPemManager.Instance.Init(_config);
TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler); TaskManager.Instance.RegUpdateTask(_config, UpdateTaskHandler);
if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed) if (_config.GuiItem.EnableStatistics || _config.GuiItem.DisplayRealTimeSpeed)
{ {
await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler);
} }
await RefreshServers(); await RefreshServersDispatcherAsync();
await Reload(); await Reload();
} }
@@ -303,7 +359,7 @@ public class MainWindowViewModel : MyReactiveObject
if (success) if (success)
{ {
var indexIdOld = _config.IndexId; var indexIdOld = _config.IndexId;
await RefreshServers(); await RefreshServersDispatcherAsync();
// If indexId changed or subIndexId is empty, directly reload. // If indexId changed or subIndexId is empty, directly reload.
if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty()) if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty())
@@ -322,7 +378,7 @@ public class MainWindowViewModel : MyReactiveObject
if (_config.UiItem.EnableAutoAdjustMainLvColWidth) if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{ {
AppEvents.AdjustMainLvColWidthRequested.Publish(); await ProfilesViewModel.AdjustMainLvColWidth();
} }
} }
} }
@@ -343,14 +399,20 @@ public class MainWindowViewModel : MyReactiveObject
private async Task RefreshServers() private async Task RefreshServers()
{ {
AppEvents.ProfilesRefreshRequested.Publish(); await ProfilesViewModel.RefreshServersBiz();
await StatusBarViewModel.RefreshServersBiz();
await Task.Delay(200); // await Task.Delay(200);
} }
private void RefreshSubscriptions() private async Task RefreshServersDispatcherAsync()
{ {
AppEvents.SubscriptionsRefreshRequested.Publish(); await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
}
private async Task RefreshSubscriptions()
{
await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
} }
#endregion Servers && Groups #endregion Servers && Groups
@@ -369,19 +431,22 @@ public class MainWindowViewModel : MyReactiveObject
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType == EConfigType.Custom)
{ {
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); var addServer2ViewModel = new AddServer2ViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel);
} }
else if (eConfigType.IsGroupType()) else if (eConfigType.IsGroupType())
{ {
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); var addGroupServerViewModel = new AddGroupServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel);
} }
else else
{ {
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); var addServerViewModel = new AddServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel);
} }
if (ret == true) if (ret == true)
{ {
await RefreshServers(); await RefreshServersDispatcherAsync();
if (item.IndexId == _config.IndexId) if (item.IndexId == _config.IndexId)
{ {
await Reload(); await Reload();
@@ -391,16 +456,22 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaClipboardAsync(string? clipboardData) public async Task AddServerViaClipboardAsync(string? clipboardData)
{ {
var stringData = clipboardData;
if (clipboardData == null) if (clipboardData == null)
{ {
await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null); var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default);
return; if (result.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
stringData = result;
} }
var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); var ret = await ConfigHandler.AddBatchServers(_config, stringData, _config.SubIndexId, false);
if (ret > 0) if (ret > 0)
{ {
RefreshSubscriptions(); await RefreshSubscriptions();
await RefreshServers(); await RefreshServersDispatcherAsync();
NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret));
} }
else else
@@ -411,8 +482,8 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaScanAsync() public async Task AddServerViaScanAsync()
{ {
_updateView?.Invoke(EViewAction.ScanScreenTask, null); var result = await ScanScreenInteraction.Handle(Unit.Default);
await Task.CompletedTask; await ScanScreenResult(result);
} }
public async Task ScanScreenResult(byte[]? bytes) public async Task ScanScreenResult(byte[]? bytes)
@@ -423,8 +494,8 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaImageAsync() public async Task AddServerViaImageAsync()
{ {
_updateView?.Invoke(EViewAction.ScanImageTask, null); var imageFileName = await BrowseImageFileInteraction.Handle(Unit.Default);
await Task.CompletedTask; await AddScanResultAsync(imageFileName);
} }
public async Task ScanImageResult(string fileName) public async Task ScanImageResult(string fileName)
@@ -449,8 +520,8 @@ public class MainWindowViewModel : MyReactiveObject
var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false);
if (ret > 0) if (ret > 0)
{ {
RefreshSubscriptions(); await RefreshSubscriptions();
await RefreshServers(); await RefreshServersDispatcherAsync();
NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan);
} }
else else
@@ -466,9 +537,10 @@ public class MainWindowViewModel : MyReactiveObject
private async Task SubSettingAsync() private async Task SubSettingAsync()
{ {
if (await _updateView?.Invoke(EViewAction.SubSettingWindow, null) == true) var subSettingViewModel = new SubSettingViewModel();
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subSettingViewModel) == true)
{ {
RefreshSubscriptions(); await RefreshSubscriptions();
} }
} }
@@ -483,28 +555,38 @@ public class MainWindowViewModel : MyReactiveObject
private async Task OptionSettingAsync() private async Task OptionSettingAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null); var settingViewModel = new OptionSettingViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(settingViewModel);
if (ret == true) if (ret == true)
{ {
AppEvents.InboundDisplayRequested.Publish(); MainGirdOrientation = _config.UiItem.MainGirdOrientation;
RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.InboundDisplayStatus();
});
await Reload(); await Reload();
} }
} }
private async Task RoutingSettingAsync() private async Task RoutingSettingAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.RoutingSettingWindow, null); var routingSettingViewModel = new RoutingSettingViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(routingSettingViewModel);
if (ret == true) if (ret == true)
{ {
await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinRouting(_config);
AppEvents.RoutingsMenuRefreshRequested.Publish(); RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.RefreshRoutingsMenu();
});
await Reload(); await Reload();
} }
} }
private async Task DNSSettingAsync() private async Task DNSSettingAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.DNSSettingWindow, null); var dnsSettingViewModel = new DNSSettingViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(dnsSettingViewModel);
if (ret == true) if (ret == true)
{ {
await Reload(); await Reload();
@@ -513,7 +595,8 @@ public class MainWindowViewModel : MyReactiveObject
private async Task FullConfigTemplateAsync() private async Task FullConfigTemplateAsync()
{ {
var ret = await _updateView?.Invoke(EViewAction.FullConfigTemplateWindow, null); var fullConfigTemplateViewModel = new FullConfigTemplateViewModel();
var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(fullConfigTemplateViewModel);
if (ret == true) if (ret == true)
{ {
await Reload(); await Reload();
@@ -523,7 +606,7 @@ public class MainWindowViewModel : MyReactiveObject
private async Task ClearServerStatistics() private async Task ClearServerStatistics()
{ {
await StatisticsManager.Instance.ClearAllServerStatistics(); await StatisticsManager.Instance.ClearAllServerStatistics();
await RefreshServers(); await RefreshServersDispatcherAsync();
} }
private async Task OpenTheFileLocation() private async Task OpenTheFileLocation()
@@ -560,6 +643,12 @@ public class MainWindowViewModel : MyReactiveObject
return; return;
} }
if (DesignMode)
{
_reloadSemaphore.Release();
return;
}
try try
{ {
SetReloadEnabled(false); SetReloadEnabled(false);
@@ -582,12 +671,22 @@ public class MainWindowViewModel : MyReactiveObject
await SysProxyHandler.UpdateSysProxy(_config, false); await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000); await Task.Delay(1000);
}); });
AppEvents.TestServerRequested.Publish(); RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.TestServerAvailability();
});
var showClashUI = AppManager.Instance.IsRunningCore(ECoreType.sing_box); var showClashUI = AppManager.Instance.IsRunningCore(ECoreType.sing_box);
if (showClashUI) if (showClashUI)
{ {
AppEvents.ProxiesReloadRequested.Publish(); //await Observable.Start(async () =>
//{
// await ClashProxiesViewModel.ProxiesReload();
//}, RxSchedulers.MainThreadScheduler);
RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await ClashProxiesViewModel.ProxiesReload();
});
} }
ReloadResult(showClashUI); ReloadResult(showClashUI);
@@ -632,7 +731,10 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await ConfigHandler.ApplyRegionalPreset(_config, type); await ConfigHandler.ApplyRegionalPreset(_config, type);
await ConfigHandler.InitRouting(_config); await ConfigHandler.InitRouting(_config);
AppEvents.RoutingsMenuRefreshRequested.Publish(); RxSchedulers.MainThreadScheduler.Schedule(async () =>
{
await StatusBarViewModel.RefreshRoutingsMenu();
});
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll(); await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll();
+4 -3
View File
@@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject public class MsgViewModel : MyReactiveObject
{ {
public Interaction<string, Unit> DispatcherShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _queueMsg = new(); private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable; private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked private int _showLock = 0; // 0 = unlocked, 1 = locked
@@ -13,10 +15,9 @@ public class MsgViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public bool AutoRefresh { get; set; }
public MsgViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public MsgViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty; MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty;
AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true; AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true;
@@ -64,7 +65,7 @@ public class MsgViewModel : MyReactiveObject
sb.Append(line); sb.Append(line);
} }
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString()); await DispatcherShowMsgInteraction.Handle(sb.ToString());
} }
finally finally
{ {
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class OptionSettingViewModel : MyReactiveObject public class OptionSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
#region Core #region Core
[Reactive] public int LocalPort { get; set; } [Reactive] public int LocalPort { get; set; }
@@ -27,24 +29,12 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public bool EnableFragment { get; set; } [Reactive] public bool EnableFragment { get; set; }
[Reactive] public bool EnableFinalFragment { get; set; } [Reactive] public bool EnableFinalFragment { get; set; }
[Reactive] public string FragmentPackets { get; set; } [Reactive] public string FragmentPackets { get; set; }
[Reactive] public string FragmentLength { get; set; } [Reactive] public string FragmentLengths { get; set; }
[Reactive] public string FragmentInterval { get; set; } [Reactive] public string FragmentDelays { get; set; }
[Reactive] public string FragmentMaxSplit { get; set; } [Reactive] public string FragmentMaxSplit { get; set; }
#endregion Core #endregion Core
#region Core KCP
//[Reactive] public int Kcpmtu { get; set; }
//[Reactive] public int Kcptti { get; set; }
//[Reactive] public int KcpuplinkCapacity { get; set; }
//[Reactive] public int KcpdownlinkCapacity { get; set; }
//[Reactive] public int KcpreadBufferSize { get; set; }
//[Reactive] public int KcpwriteBufferSize { get; set; }
//[Reactive] public bool Kcpcongestion { get; set; }
#endregion Core KCP
#region UI #region UI
[Reactive] public bool AutoRun { get; set; } [Reactive] public bool AutoRun { get; set; }
@@ -72,6 +62,7 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string SrsFileSourceUrl { get; set; } [Reactive] public string SrsFileSourceUrl { get; set; }
[Reactive] public string RoutingRulesSourceUrl { get; set; } [Reactive] public string RoutingRulesSourceUrl { get; set; }
[Reactive] public string IPAPIUrl { get; set; } [Reactive] public string IPAPIUrl { get; set; }
[Reactive] public string RootCertProvider { get; set; }
#endregion UI #endregion UI
@@ -104,6 +95,8 @@ public class OptionSettingViewModel : MyReactiveObject
[Reactive] public string TunIcmpRouting { get; set; } [Reactive] public string TunIcmpRouting { get; set; }
[Reactive] public bool TunEnableLegacyProtect { get; set; } [Reactive] public bool TunEnableLegacyProtect { get; set; }
[Reactive] public string TunRouteExcludeAddress { get; set; } [Reactive] public string TunRouteExcludeAddress { get; set; }
[Reactive] public string TunIPv4Address { get; set; }
[Reactive] public string TunIPv6Address { get; set; }
#endregion Tun mode #endregion Tun mode
@@ -122,10 +115,9 @@ public class OptionSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public OptionSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public OptionSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
BlIsWindows = Utils.IsWindows(); BlIsWindows = Utils.IsWindows();
BlIsLinux = Utils.IsLinux(); BlIsLinux = Utils.IsLinux();
BlIsIsMacOS = Utils.IsMacOS(); BlIsIsMacOS = Utils.IsMacOS();
@@ -141,8 +133,6 @@ public class OptionSettingViewModel : MyReactiveObject
private async Task Init() private async Task Init()
{ {
await _updateView?.Invoke(EViewAction.InitSettingFont, null);
#region Core #region Core
var inbound = _config.Inbound.First(); var inbound = _config.Inbound.First();
@@ -150,6 +140,7 @@ public class OptionSettingViewModel : MyReactiveObject
SecondLocalPortEnabled = inbound.SecondLocalPortEnabled; SecondLocalPortEnabled = inbound.SecondLocalPortEnabled;
UdpEnabled = inbound.UdpEnabled; UdpEnabled = inbound.UdpEnabled;
SniffingEnabled = inbound.SniffingEnabled; SniffingEnabled = inbound.SniffingEnabled;
DestOverride = inbound.DestOverride ?? [];
RouteOnly = inbound.RouteOnly; RouteOnly = inbound.RouteOnly;
AllowLANConn = inbound.AllowLANConn; AllowLANConn = inbound.AllowLANConn;
NewPort4LAN = inbound.NewPort4LAN; NewPort4LAN = inbound.NewPort4LAN;
@@ -168,24 +159,12 @@ public class OptionSettingViewModel : MyReactiveObject
EnableFragment = _config.CoreBasicItem.EnableFragment; EnableFragment = _config.CoreBasicItem.EnableFragment;
EnableFinalFragment = _config.CoreBasicItem.EnableFinalFragment; EnableFinalFragment = _config.CoreBasicItem.EnableFinalFragment;
FragmentPackets = _config.Fragment4RayItem?.Packets; FragmentPackets = _config.Fragment4RayItem?.Packets;
FragmentLength = _config.Fragment4RayItem?.Length; FragmentLengths = Utils.List2String(_config.Fragment4RayItem?.Lengths);
FragmentInterval = _config.Fragment4RayItem?.Interval; FragmentDelays = Utils.List2String(_config.Fragment4RayItem?.Delays);
FragmentMaxSplit = _config.Fragment4RayItem?.MaxSplit; FragmentMaxSplit = _config.Fragment4RayItem?.MaxSplit;
#endregion Core #endregion Core
#region Core KCP
//Kcpmtu = _config.kcpItem.mtu;
//Kcptti = _config.kcpItem.tti;
//KcpuplinkCapacity = _config.kcpItem.uplinkCapacity;
//KcpdownlinkCapacity = _config.kcpItem.downlinkCapacity;
//KcpreadBufferSize = _config.kcpItem.readBufferSize;
//KcpwriteBufferSize = _config.kcpItem.writeBufferSize;
//Kcpcongestion = _config.kcpItem.congestion;
#endregion Core KCP
#region UI #region UI
AutoRun = _config.GuiItem.AutoRun; AutoRun = _config.GuiItem.AutoRun;
@@ -213,6 +192,7 @@ public class OptionSettingViewModel : MyReactiveObject
SrsFileSourceUrl = _config.ConstItem.SrsSourceUrl; SrsFileSourceUrl = _config.ConstItem.SrsSourceUrl;
RoutingRulesSourceUrl = _config.ConstItem.RouteRulesTemplateSourceUrl; RoutingRulesSourceUrl = _config.ConstItem.RouteRulesTemplateSourceUrl;
IPAPIUrl = _config.SpeedTestItem.IPAPIUrl; IPAPIUrl = _config.SpeedTestItem.IPAPIUrl;
RootCertProvider = _config.GuiItem.RootCertProvider;
#endregion UI #endregion UI
@@ -236,6 +216,8 @@ public class OptionSettingViewModel : MyReactiveObject
TunIcmpRouting = _config.TunModeItem.IcmpRouting; TunIcmpRouting = _config.TunModeItem.IcmpRouting;
TunEnableLegacyProtect = _config.TunModeItem.EnableLegacyProtect; TunEnableLegacyProtect = _config.TunModeItem.EnableLegacyProtect;
TunRouteExcludeAddress = Utils.List2String(_config.TunModeItem.RouteExcludeAddress, true); TunRouteExcludeAddress = Utils.List2String(_config.TunModeItem.RouteExcludeAddress, true);
TunIPv4Address = _config.TunModeItem.IPv4Address;
TunIPv6Address = _config.TunModeItem.IPv6Address;
#endregion Tun mode #endregion Tun mode
@@ -308,35 +290,33 @@ public class OptionSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort); NoticeManager.Instance.Enqueue(ResUI.FillLocalListeningPort);
return; return;
} }
var fragmentLengths = Utils.String2List(FragmentLengths) ?? [];
var fragmentDelays = Utils.String2List(FragmentDelays) ?? [];
if (fragmentLengths.Any(item => !Utils.TryParseRange(item, 0, int.MaxValue, out _, out _))
|| fragmentDelays.Any(item => !Utils.TryParseRange(item, 0, int.MaxValue, out _, out _))
|| (FragmentMaxSplit.IsNotEmpty() && !Utils.TryParseMaxSplit(FragmentMaxSplit, 0, 10000, out _, out _)))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics var needReboot = EnableStatistics != _config.GuiItem.EnableStatistics
|| DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed
|| EnableDragDropSort != _config.UiItem.EnableDragDropSort || EnableDragDropSort != _config.UiItem.EnableDragDropSort
|| EnableHWA != _config.GuiItem.EnableHWA || EnableHWA != _config.GuiItem.EnableHWA
|| CurrentFontFamily != _config.UiItem.CurrentFontFamily || CurrentFontFamily != _config.UiItem.CurrentFontFamily;
|| MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation;
//if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString())
// || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString())
// || Utile.IsNullOrEmpty(KcpuplinkCapacity.ToString()) || !Utile.IsNumeric(KcpuplinkCapacity.ToString())
// || Utile.IsNullOrEmpty(KcpdownlinkCapacity.ToString()) || !Utile.IsNumeric(KcpdownlinkCapacity.ToString())
// || Utile.IsNullOrEmpty(KcpreadBufferSize.ToString()) || !Utile.IsNumeric(KcpreadBufferSize.ToString())
// || Utile.IsNullOrEmpty(KcpwriteBufferSize.ToString()) || !Utile.IsNumeric(KcpwriteBufferSize.ToString()))
//{
// NoticeHandler.Instance.Enqueue(ResUI.FillKcpParameters);
// return;
//}
//Core //Core
_config.Inbound.First().LocalPort = LocalPort; var inbound = _config.Inbound.First();
_config.Inbound.First().SecondLocalPortEnabled = SecondLocalPortEnabled; inbound.LocalPort = LocalPort;
_config.Inbound.First().UdpEnabled = UdpEnabled; inbound.SecondLocalPortEnabled = SecondLocalPortEnabled;
_config.Inbound.First().SniffingEnabled = SniffingEnabled; inbound.UdpEnabled = UdpEnabled;
_config.Inbound.First().DestOverride = DestOverride?.ToList(); inbound.SniffingEnabled = SniffingEnabled;
_config.Inbound.First().RouteOnly = RouteOnly; inbound.DestOverride = DestOverride?.ToList();
_config.Inbound.First().AllowLANConn = AllowLANConn; inbound.RouteOnly = RouteOnly;
_config.Inbound.First().NewPort4LAN = NewPort4LAN; inbound.AllowLANConn = AllowLANConn;
_config.Inbound.First().User = User; inbound.NewPort4LAN = NewPort4LAN;
_config.Inbound.First().Pass = Pass; inbound.User = User;
inbound.Pass = Pass;
if (_config.Inbound.Count > 1) if (_config.Inbound.Count > 1)
{ {
_config.Inbound.RemoveAt(1); _config.Inbound.RemoveAt(1);
@@ -351,30 +331,12 @@ public class OptionSettingViewModel : MyReactiveObject
_config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox; _config.CoreBasicItem.EnableCacheFile4Sbox = EnableCacheFile4Sbox;
_config.HysteriaItem.UpMbps = HyUpMbps ?? 0; _config.HysteriaItem.UpMbps = HyUpMbps ?? 0;
_config.HysteriaItem.DownMbps = HyDownMbps ?? 0; _config.HysteriaItem.DownMbps = HyDownMbps ?? 0;
if (EnableFragment)
{
if (!Utils.TryParseRange(FragmentLength, 0, int.MaxValue, out _, out _))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
if (!Utils.TryParseRange(FragmentInterval, 1, 100, out _, out _))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
if (FragmentMaxSplit.IsNotEmpty()
&& !Utils.TryParseMaxSplit(FragmentMaxSplit, 0, 10000, out _, out _))
{
NoticeManager.Instance.Enqueue(ResUI.FillFragmentParameterError);
return;
}
}
_config.CoreBasicItem.EnableFragment = EnableFragment; _config.CoreBasicItem.EnableFragment = EnableFragment;
_config.CoreBasicItem.EnableFinalFragment = EnableFinalFragment; _config.CoreBasicItem.EnableFinalFragment = EnableFinalFragment;
_config.Fragment4RayItem ??= new();
_config.Fragment4RayItem.Packets = FragmentPackets; _config.Fragment4RayItem.Packets = FragmentPackets;
_config.Fragment4RayItem.Length = FragmentLength; _config.Fragment4RayItem.Lengths = fragmentLengths;
_config.Fragment4RayItem.Interval = FragmentInterval; _config.Fragment4RayItem.Delays = fragmentDelays;
_config.Fragment4RayItem.MaxSplit = FragmentMaxSplit; _config.Fragment4RayItem.MaxSplit = FragmentMaxSplit;
_config.GuiItem.AutoRun = AutoRun; _config.GuiItem.AutoRun = AutoRun;
@@ -402,6 +364,7 @@ public class OptionSettingViewModel : MyReactiveObject
_config.ConstItem.SrsSourceUrl = SrsFileSourceUrl; _config.ConstItem.SrsSourceUrl = SrsFileSourceUrl;
_config.ConstItem.RouteRulesTemplateSourceUrl = RoutingRulesSourceUrl; _config.ConstItem.RouteRulesTemplateSourceUrl = RoutingRulesSourceUrl;
_config.SpeedTestItem.IPAPIUrl = IPAPIUrl; _config.SpeedTestItem.IPAPIUrl = IPAPIUrl;
_config.GuiItem.RootCertProvider = RootCertProvider;
//systemProxy //systemProxy
_config.SystemProxyItem.SystemProxyExceptions = SystemProxyExceptions; _config.SystemProxyItem.SystemProxyExceptions = SystemProxyExceptions;
@@ -419,6 +382,8 @@ public class OptionSettingViewModel : MyReactiveObject
_config.TunModeItem.IcmpRouting = TunIcmpRouting; _config.TunModeItem.IcmpRouting = TunIcmpRouting;
_config.TunModeItem.EnableLegacyProtect = TunEnableLegacyProtect; _config.TunModeItem.EnableLegacyProtect = TunEnableLegacyProtect;
_config.TunModeItem.RouteExcludeAddress = Utils.String2List(TunRouteExcludeAddress); _config.TunModeItem.RouteExcludeAddress = Utils.String2List(TunRouteExcludeAddress);
_config.TunModeItem.IPv4Address = TunIPv4Address;
_config.TunModeItem.IPv6Address = TunIPv6Address;
//coreType //coreType
await SaveCoreType(); await SaveCoreType();
@@ -429,7 +394,7 @@ public class OptionSettingViewModel : MyReactiveObject
AppManager.Instance.Reset(); AppManager.Instance.Reset();
NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -1,7 +1,11 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject public class ProfilesSelectViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new();
#region private prop #region private prop
private string _serverFilter = string.Empty; private string _serverFilter = string.Empty;
@@ -12,6 +16,8 @@ public class ProfilesSelectViewModel : MyReactiveObject
#endregion private prop #endregion private prop
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
#region ObservableCollection #region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>(); public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
@@ -36,18 +42,25 @@ public class ProfilesSelectViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool FilterExclude { get; set; } public bool FilterExclude { get; set; }
[Reactive]
public bool MultiSelect { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
#region Init #region Init
public ProfilesSelectViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ProfilesSelectViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
_subIndexId = _config.SubIndexId ?? string.Empty; _subIndexId = _config.SubIndexId ?? string.Empty;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
SaveCmd = ReactiveCommand.Create(() =>
{
SelectFinish();
});
this.WhenAnyValue( this.WhenAnyValue(
x => x.SelectedSub, x => x.SelectedSub,
y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id) y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id)
@@ -107,7 +120,7 @@ public class ProfilesSelectViewModel : MyReactiveObject
{ {
return false; return false;
} }
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
return true; return true;
} }
@@ -125,7 +138,13 @@ public class ProfilesSelectViewModel : MyReactiveObject
await RefreshServers(); await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null); try
{
await ProfilesFocusInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
}
} }
private async Task ServerFilterChanged(bool c) private async Task ServerFilterChanged(bool c)
@@ -157,8 +176,6 @@ public class ProfilesSelectViewModel : MyReactiveObject
var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId);
SelectedProfile = selected ?? lstModel.First(); SelectedProfile = selected ?? lstModel.First();
} }
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
} }
private async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
@@ -2,6 +2,17 @@ namespace ServiceLib.ViewModels;
public class ProfilesViewModel : MyReactiveObject public class ProfilesViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<ProfileItem, bool> SaveFileDialogInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new();
public Interaction<string, Unit> ShareServerInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshServersBizInteraction { get; } = new();
public Interaction<Unit, Unit> AdjustMainLvColWidthInteraction { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new();
public EventChannel<Unit> RefreshServersRequested { get; } = new();
#region private prop #region private prop
private List<ProfileItem> _lstProfile; private List<ProfileItem> _lstProfile;
@@ -82,10 +93,9 @@ public class ProfilesViewModel : MyReactiveObject
#region Init #region Init
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public ProfilesViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
#region WhenAnyValue && ReactiveCommand #region WhenAnyValue && ReactiveCommand
@@ -236,26 +246,11 @@ public class ProfilesViewModel : MyReactiveObject
#region AppEvents #region AppEvents
AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz());
AppEvents.SubscriptionsRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshSubscriptions());
AppEvents.DispatcherStatisticsRequested AppEvents.DispatcherStatisticsRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result)); .Subscribe(async result => await UpdateStatistics(result));
AppEvents.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async indexId => await SetDefaultServer(indexId));
#endregion AppEvents #endregion AppEvents
_ = Init(); _ = Init();
@@ -277,7 +272,7 @@ public class ProfilesViewModel : MyReactiveObject
private void Reload() private void Reload()
{ {
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
} }
public async Task SetSpeedTestResult(SpeedTestResult result) public async Task SetSpeedTestResult(SpeedTestResult result)
@@ -350,7 +345,13 @@ public class ProfilesViewModel : MyReactiveObject
await RefreshServers(); await RefreshServers();
await _updateView?.Invoke(EViewAction.ProfilesFocus, null); try
{
await ProfilesFocusInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
}
} }
private async Task ServerFilterChanged(bool c) private async Task ServerFilterChanged(bool c)
@@ -368,19 +369,21 @@ public class ProfilesViewModel : MyReactiveObject
public async Task RefreshServers() public async Task RefreshServers()
{ {
AppEvents.ProfilesRefreshRequested.Publish(); RefreshServersRequested.Publish();
await Task.Delay(200); // await Task.Delay(200);
await Task.CompletedTask;
} }
private async Task RefreshServersBiz() public async Task RefreshServersBiz()
{ {
var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter); var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter);
_lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? []; _lstProfile = JsonUtils.Deserialize<List<ProfileItem>>(JsonUtils.Serialize(lstModel)) ?? [];
ProfileItems.Clear(); ProfileItems.Clear();
ProfileItems.AddRange(lstModel); ProfileItems.AddRange(lstModel ?? []);
if (lstModel.Count > 0) if (lstModel?.Count > 0)
{ {
ProfileItemModel? selected = null; ProfileItemModel? selected = null;
if (!_pendingSelectIndexId.IsNullOrEmpty()) if (!_pendingSelectIndexId.IsNullOrEmpty())
@@ -392,10 +395,16 @@ public class ProfilesViewModel : MyReactiveObject
SelectedProfile = selected ?? lstModel.First(); SelectedProfile = selected ?? lstModel.First();
} }
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); try
{
await DispatcherRefreshServersBizInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
}
} }
private async Task RefreshSubscriptions() public async Task RefreshSubscriptions()
{ {
var subItems = await AppManager.Instance.SubItems(); var subItems = await AppManager.Instance.SubItems();
subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers }); subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers });
@@ -408,6 +417,11 @@ public class ProfilesViewModel : MyReactiveObject
: null) ?? subItems.FirstOrDefault(); : null) ?? subItems.FirstOrDefault();
} }
public async Task AdjustMainLvColWidth()
{
await AdjustMainLvColWidthInteraction.Handle(Unit.Default);
}
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
{ {
var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter); var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter);
@@ -491,15 +505,18 @@ public class ProfilesViewModel : MyReactiveObject
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType == EConfigType.Custom)
{ {
ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); var addServer2ViewModel = new AddServer2ViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel);
} }
else if (eConfigType.IsGroupType()) else if (eConfigType.IsGroupType())
{ {
ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); var addGroupServerViewModel = new AddGroupServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel);
} }
else else
{ {
ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); var addServerViewModel = new AddServerViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel);
} }
if (ret == true) if (ret == true)
{ {
@@ -518,7 +535,7 @@ public class ProfilesViewModel : MyReactiveObject
{ {
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -539,7 +556,7 @@ public class ProfilesViewModel : MyReactiveObject
private async Task RemoveDuplicateServer() private async Task RemoveDuplicateServer()
{ {
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -576,7 +593,7 @@ public class ProfilesViewModel : MyReactiveObject
await SetDefaultServer(SelectedProfile.IndexId); await SetDefaultServer(SelectedProfile.IndexId);
} }
private async Task SetDefaultServer(string? indexId) public async Task SetDefaultServer(string? indexId)
{ {
if (indexId.IsNullOrEmpty()) if (indexId.IsNullOrEmpty())
{ {
@@ -614,7 +631,7 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
await _updateView?.Invoke(EViewAction.ShareServer, url); await ShareServerInteraction.Handle(url);
} }
private async Task GenGroupAllServer() private async Task GenGroupAllServer()
@@ -783,13 +800,13 @@ public class ProfilesViewModel : MyReactiveObject
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data); await SetClipboardDataInteraction.Handle((string)result.Data);
NoticeManager.Instance.SendMessage(ResUI.OperationSuccess); NoticeManager.Instance.SendMessage(ResUI.OperationSuccess);
} }
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SaveFileDialog, item); await SaveFileDialogInteraction.Handle(item);
} }
} }
@@ -838,11 +855,11 @@ public class ProfilesViewModel : MyReactiveObject
{ {
if (blEncode) if (blEncode)
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, Utils.Base64Encode(sb.ToString())); await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString()));
} }
else else
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); await SetClipboardDataInteraction.Handle(sb.ToString());
} }
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
} }
@@ -865,7 +882,7 @@ public class ProfilesViewModel : MyReactiveObject
if (!result.IsNullOrEmpty()) if (!result.IsNullOrEmpty())
{ {
await _updateView?.Invoke(EViewAction.SetClipboardData, result); await SetClipboardDataInteraction.Handle(result);
NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully);
} }
else else
@@ -893,7 +910,8 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) var subEditViewModel = new SubEditViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true)
{ {
await RefreshSubscriptions(); await RefreshSubscriptions();
await SubSelectedChangedAsync(true); await SubSelectedChangedAsync(true);
@@ -908,7 +926,7 @@ public class ProfilesViewModel : MyReactiveObject
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingRuleDetailsViewModel : MyReactiveObject public class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public IList<string> ProtocolItems { get; set; } public IList<string> ProtocolItems { get; set; }
public IList<string> InboundTagItems { get; set; } public IList<string> InboundTagItems { get; set; }
@@ -23,13 +25,17 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
[Reactive] [Reactive]
public bool AutoSort { get; set; } public bool AutoSort { get; set; }
public ReactiveCommand<Unit, Unit> SelectProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func<EViewAction, object?, Task<bool>>? updateView) public RoutingRuleDetailsViewModel(RulesItem rulesItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SelectProfileCmd = ReactiveCommand.CreateFromTask(async () =>
{
await SelectProfileAsync();
});
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SaveRulesAsync(); await SaveRulesAsync();
@@ -88,6 +94,24 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
return; return;
} }
//NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); //NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess);
await _updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
await Task.CompletedTask;
}
private async Task SelectProfileAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return;
}
var profileItem = await profileSelectViewModel.GetProfileItem();
if (profileItem != null)
{
SelectedSource.OutboundTag = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
}
} }
} }
@@ -1,7 +1,14 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingRuleSettingViewModel : MyReactiveObject public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, string?> BrowseRulesFileInteraction { get; } = new();
private List<RulesItem> _rules; private List<RulesItem> _rules;
[Reactive] [Reactive]
@@ -27,10 +34,9 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public RoutingRuleSettingViewModel(RoutingItem routingItem, Func<EViewAction, object?, Task<bool>>? updateView) public RoutingRuleSettingViewModel(RoutingItem routingItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
@@ -42,7 +48,8 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
}); });
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () => ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await _updateView?.Invoke(EViewAction.ImportRulesFromFile, null); var fileName = await BrowseRulesFileInteraction.Handle(Unit.Default);
await ImportRulesFromFileAsync(fileName);
}); });
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () => ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
@@ -131,7 +138,8 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.RoutingRuleDetailsWindow, item) == true) var routingRuleDetailsViewModel = new RoutingRuleDetailsViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(routingRuleDetailsViewModel) == true)
{ {
if (blNew) if (blNew)
{ {
@@ -148,7 +156,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -191,7 +199,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; };
await _updateView?.Invoke(EViewAction.SetClipboardData, JsonUtils.Serialize(lst, options)); await SetClipboardDataInteraction.Handle(JsonUtils.Serialize(lst, options));
} }
} }
@@ -234,7 +242,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
if (await ConfigHandler.SaveRoutingItem(_config, item) == 0) if (await ConfigHandler.SaveRoutingItem(_config, item) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
@@ -266,12 +274,18 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public async Task ImportRulesFromClipboardAsync(string? clipboardData) public async Task ImportRulesFromClipboardAsync(string? clipboardData)
{ {
var stringData = clipboardData;
if (clipboardData == null) if (clipboardData == null)
{ {
await _updateView?.Invoke(EViewAction.ImportRulesFromClipboard, null); var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default);
return; if (result.IsNullOrEmpty())
{
NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
return;
}
stringData = result;
} }
var ret = await AddBatchRoutingRulesAsync(SelectedRouting, clipboardData); var ret = await AddBatchRoutingRulesAsync(SelectedRouting, stringData);
if (ret == 0) if (ret == 0)
{ {
RefreshRulesItems(); RefreshRulesItems();
@@ -301,7 +315,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData) private async Task<int> AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData)
{ {
var blReplace = false; var blReplace = false;
if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.AddBatchRoutingRulesYesNo) == false)
{ {
blReplace = true; blReplace = true;
} }
@@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels;
public class RoutingSettingViewModel : MyReactiveObject public class RoutingSettingViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
#region Reactive #region Reactive
public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>(); public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>();
@@ -26,10 +28,9 @@ public class RoutingSettingViewModel : MyReactiveObject
#endregion Reactive #endregion Reactive
public RoutingSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public RoutingSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
@@ -131,7 +132,8 @@ public class RoutingSettingViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.RoutingRuleSettingWindow, item) == true) var routingRuleSettingViewModel = new RoutingRuleSettingViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(routingRuleSettingViewModel) == true)
{ {
await RefreshRoutingItems(); await RefreshRoutingItems();
IsModified = true; IsModified = true;
@@ -145,7 +147,7 @@ public class RoutingSettingViewModel : MyReactiveObject
NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules);
return; return;
} }
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
@@ -2,9 +2,20 @@ namespace ServiceLib.ViewModels;
public class StatusBarViewModel : MyReactiveObject public class StatusBarViewModel : MyReactiveObject
{ {
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new(null)); public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> PasswordInputInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshIconInteraction { get; } = new();
public EventChannel<bool> SubscriptionsUpdateRequested { get; } = new();
public EventChannel<bool?> ShowHideWindowRequested { get; } = new();
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new());
public static StatusBarViewModel Instance => _instance.Value; public static StatusBarViewModel Instance => _instance.Value;
public EventChannel<string> SetDefaultServerRequested { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new();
public EventChannel<Unit> AddServerViaScanRequested { get; } = new();
public EventChannel<Unit> AddServerViaClipboardRequested { get; } = new();
#region ObservableCollection #region ObservableCollection
public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>(); public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>();
@@ -92,12 +103,12 @@ public class StatusBarViewModel : MyReactiveObject
#endregion UI #endregion UI
public StatusBarViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public StatusBarViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
SelectedRouting = new(); SelectedRouting = new();
SelectedServer = new(); SelectedServer = new();
RunningServerToolTipText = "-"; RunningServerToolTipText = GetRunningServerToolTipText("-");
BlSystemProxyPacVisible = Utils.IsWindows(); BlSystemProxyPacVisible = Utils.IsWindows();
BlIsNonWindows = Utils.IsNonWindows(); BlIsNonWindows = Utils.IsNonWindows();
@@ -140,17 +151,17 @@ public class StatusBarViewModel : MyReactiveObject
NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () => NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
AppEvents.ShowHideWindowRequested.Publish(null); ShowHideWindowRequested.Publish(null);
await Task.CompletedTask; await Task.CompletedTask;
}); });
ShowWindowCmd = ReactiveCommand.CreateFromTask(async () => ShowWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
AppEvents.ShowHideWindowRequested.Publish(true); ShowHideWindowRequested.Publish(true);
await Task.CompletedTask; await Task.CompletedTask;
}); });
HideWindowCmd = ReactiveCommand.CreateFromTask(async () => HideWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
AppEvents.ShowHideWindowRequested.Publish(false); ShowHideWindowRequested.Publish(false);
await Task.CompletedTask; await Task.CompletedTask;
}); });
@@ -193,31 +204,11 @@ public class StatusBarViewModel : MyReactiveObject
#region AppEvents #region AppEvents
if (updateView != null)
{
InitUpdateView(updateView);
}
AppEvents.DispatcherStatisticsRequested AppEvents.DispatcherStatisticsRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result)); .Subscribe(async result => await UpdateStatistics(result));
AppEvents.RoutingsMenuRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshRoutingsMenu());
AppEvents.TestServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await TestServerAvailability());
AppEvents.InboundDisplayRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await InboundDisplayStatus());
AppEvents.SysProxyChangeRequested AppEvents.SysProxyChangeRequested
.AsObservable() .AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
@@ -238,18 +229,6 @@ public class StatusBarViewModel : MyReactiveObject
BlRouting = true; BlRouting = true;
} }
public void InitUpdateView(Func<EViewAction, object?, Task<bool>>? updateView)
{
_updateView = updateView;
if (_updateView != null)
{
AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables);
}
}
private async Task CopyProxyCmdToClipboard() private async Task CopyProxyCmdToClipboard()
{ {
var cmd = Utils.IsWindows() ? "set" : "export"; var cmd = Utils.IsWindows() ? "set" : "export";
@@ -264,28 +243,28 @@ public class StatusBarViewModel : MyReactiveObject
sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}"); sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}");
sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}"); sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}");
await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); await SetClipboardDataInteraction.Handle(sb.ToString());
} }
private async Task AddServerViaClipboard() private async Task AddServerViaClipboard()
{ {
AppEvents.AddServerViaClipboardRequested.Publish(); AddServerViaClipboardRequested.Publish();
await Task.Delay(1000); await Task.Delay(1000);
} }
private async Task AddServerViaScan() private async Task AddServerViaScan()
{ {
AppEvents.AddServerViaScanRequested.Publish(); AddServerViaScanRequested.Publish();
await Task.Delay(1000); await Task.Delay(1000);
} }
private async Task UpdateSubscriptionProcess(bool blProxy) private async Task UpdateSubscriptionProcess(bool blProxy)
{ {
AppEvents.SubscriptionsUpdateRequested.Publish(blProxy); SubscriptionsUpdateRequested.Publish(blProxy);
await Task.Delay(1000); await Task.Delay(1000);
} }
private async Task RefreshServersBiz() public async Task RefreshServersBiz()
{ {
await RefreshServersMenu(); await RefreshServersMenu();
@@ -293,22 +272,26 @@ public class StatusBarViewModel : MyReactiveObject
var running = await ConfigHandler.GetDefaultServer(_config); var running = await ConfigHandler.GetDefaultServer(_config);
if (running != null) if (running != null)
{ {
RunningServerDisplay = RunningServerDisplay = running.GetSummary();
RunningServerToolTipText = running.GetSummary(); RunningServerToolTipText = GetRunningServerToolTipText(RunningServerDisplay);
} }
else else
{ {
RunningServerDisplay = RunningServerDisplay = ResUI.CheckServerSettings;
RunningServerToolTipText = ResUI.CheckServerSettings; RunningServerToolTipText = GetRunningServerToolTipText(RunningServerDisplay);
} }
} }
private string GetRunningServerToolTipText(string serverInfo)
{
return Utils.IsLinux() ? Global.AppName : serverInfo;
}
private async Task RefreshServersMenu() private async Task RefreshServersMenu()
{ {
var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, ""); var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, "");
Servers.Clear(); if (lstModel?.Count > _config.GuiItem.TrayMenuServersLimit)
if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit)
{ {
BlServers = false; BlServers = false;
return; return;
@@ -327,6 +310,7 @@ public class StatusBarViewModel : MyReactiveObject
SelectedServer = item; SelectedServer = item;
} }
} }
Servers.Clear();
Servers.AddRange(models); Servers.AddRange(models);
} }
@@ -344,7 +328,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
return; return;
} }
AppEvents.SetDefaultServerRequested.Publish(SelectedServer.ID); SetDefaultServerRequested.Publish(SelectedServer.ID);
} }
public async Task TestServerAvailability() public async Task TestServerAvailability()
@@ -406,11 +390,18 @@ public class StatusBarViewModel : MyReactiveObject
if (blChange) if (blChange)
{ {
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); try
{
await DispatcherRefreshIconInteraction.Handle(Unit.Default);
}
catch (UnhandledInteractionException<Unit, Unit>)
{
// Ignore
}
} }
} }
private async Task RefreshRoutingsMenu() public async Task RefreshRoutingsMenu()
{ {
var routings = await AppManager.Instance.RoutingItems(); var routings = await AppManager.Instance.RoutingItems();
@@ -441,8 +432,8 @@ public class StatusBarViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) if (await ConfigHandler.SetDefaultRouting(_config, item) == 0)
{ {
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); await DispatcherRefreshIconInteraction.Handle(Unit.Default);
} }
} }
@@ -479,8 +470,8 @@ public class StatusBarViewModel : MyReactiveObject
} }
else else
{ {
bool? passwordResult = await _updateView?.Invoke(EViewAction.PasswordInput, null); var password = await PasswordInputInteraction.Handle(Unit.Default);
if (passwordResult == false) if (password.IsNullOrEmpty())
{ {
_config.TunModeItem.EnableTun = false; _config.TunModeItem.EnableTun = false;
return; return;
@@ -489,7 +480,7 @@ public class StatusBarViewModel : MyReactiveObject
} }
await ConfigHandler.SaveConfig(_config); await ConfigHandler.SaveConfig(_config);
AppEvents.ReloadRequested.Publish(); ReloadRequested.Publish();
} }
private bool AllowEnableTun() private bool AllowEnableTun()
@@ -513,7 +504,7 @@ public class StatusBarViewModel : MyReactiveObject
#region UI #region UI
private async Task InboundDisplayStatus() public async Task InboundDisplayStatus()
{ {
StringBuilder sb = new(); StringBuilder sb = new();
sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}");
@@ -1,17 +1,38 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class SubEditViewModel : MyReactiveObject public class SubEditViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose;
[Reactive] [Reactive]
public SubItem SelectedSource { get; set; } public SubItem SelectedSource { get; set; }
public ReactiveCommand<Unit, Unit> SelectPrevProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SelectNextProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public SubEditViewModel(SubItem subItem, Func<EViewAction, object?, Task<bool>>? updateView) public SubEditViewModel(SubItem subItem)
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
SelectPrevProfileCmd = ReactiveCommand.CreateFromTask(async () =>
{
var profileItem = await SelectProfileAsync();
if (profileItem != null)
{
SelectedSource?.PrevProfile = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
}
});
SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () =>
{
var profileItem = await SelectProfileAsync();
if (profileItem != null)
{
SelectedSource?.NextProfile = profileItem.Remarks;
SelectedSource = JsonUtils.DeepCopy(SelectedSource);
}
});
SaveCmd = ReactiveCommand.CreateFromTask(async () => SaveCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await SaveSubAsync(); await SaveSubAsync();
@@ -49,11 +70,24 @@ public class SubEditViewModel : MyReactiveObject
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);
_updateView?.Invoke(EViewAction.CloseWindow, null); RequestClose?.Invoke(this, EventArgs.Empty);
} }
else else
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
} }
} }
private async Task<ProfileItem?> SelectProfileAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return null;
}
var profileItem = await profileSelectViewModel.GetProfileItem();
return profileItem;
}
} }
@@ -2,6 +2,9 @@ namespace ServiceLib.ViewModels;
public class SubSettingViewModel : MyReactiveObject public class SubSettingViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> ShareSubInteraction { get; } = new();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>(); public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive] [Reactive]
@@ -15,10 +18,9 @@ public class SubSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SubShareCmd { get; } public ReactiveCommand<Unit, Unit> SubShareCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }
public SubSettingViewModel(Func<EViewAction, object?, Task<bool>>? updateView) public SubSettingViewModel()
{ {
_config = AppManager.Instance.Config; _config = AppManager.Instance.Config;
_updateView = updateView;
var canEditRemove = this.WhenAnyValue( var canEditRemove = this.WhenAnyValue(
x => x.SelectedSource, x => x.SelectedSource,
@@ -38,7 +40,7 @@ public class SubSettingViewModel : MyReactiveObject
}, canEditRemove); }, canEditRemove);
SubShareCmd = ReactiveCommand.CreateFromTask(async () => SubShareCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await _updateView?.Invoke(EViewAction.ShareSub, SelectedSource?.Url); await ShareSubInteraction.Handle(SelectedSource?.Url);
}, canEditRemove); }, canEditRemove);
_ = Init(); _ = Init();
@@ -72,7 +74,8 @@ public class SubSettingViewModel : MyReactiveObject
return; return;
} }
} }
if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) var subEditViewModel = new SubEditViewModel(item);
if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true)
{ {
await RefreshSubItems(); await RefreshSubItems();
IsModified = true; IsModified = true;
@@ -81,7 +84,7 @@ public class SubSettingViewModel : MyReactiveObject
private async Task DeleteSubAsync() private async Task DeleteSubAsync()
{ {
if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false)
{ {
return; return;
} }
+8
View File
@@ -22,6 +22,14 @@
<StyleInclude Source="Assets/GlobalStyles.axaml" /> <StyleInclude Source="Assets/GlobalStyles.axaml" />
<StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" /> <StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" />
<dialogHost:DialogHostStyles /> <dialogHost:DialogHostStyles />
<Style Selector="dialogHost|DialogHost">
<Setter Property="OverlayBackground" Value="{DynamicResource SemiColorOverlayBackground}" />
<Setter Property="Background" Value="{DynamicResource SemiColorFill0}" />
<Setter Property="dialogHost:DialogHostStyle.BoxShadow" Value="{DynamicResource SemiShadowElevated}" />
<Setter Property="dialogHost:DialogHostStyle.CornerRadius" Value="6" />
</Style>
</Application.Styles> </Application.Styles>
<TrayIcon.Icons> <TrayIcon.Icons>
+6 -1
View File
@@ -15,6 +15,9 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
{ {
var viewLocator = SimpleViewLocator.Instance;
DataTemplates.Add(viewLocator);
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
if (!Design.IsDesignMode) if (!Design.IsDesignMode)
@@ -23,7 +26,9 @@ public partial class App : Application
DataContext = StatusBarViewModel.Instance; DataContext = StatusBarViewModel.Instance;
} }
var mainWindow = new MainWindow(); var mainWindowViewModel = new MainWindowViewModel();
var mainWindow = (MainWindow)viewLocator.Build(mainWindowViewModel);
mainWindow.ViewModel = mainWindowViewModel;
desktop.MainWindow = mainWindow; desktop.MainWindow = mainWindow;
if (OperatingSystem.IsMacOS()) if (OperatingSystem.IsMacOS())
+12 -14
View File
@@ -4,7 +4,6 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
{ {
public WindowBase() public WindowBase()
{ {
Initialized += OnWindowInitialized;
Loaded += OnLoaded; Loaded += OnLoaded;
Loaded += (s, e) => Loaded += (s, e) =>
{ {
@@ -20,7 +19,7 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
throw new NotImplementedException(); throw new NotImplementedException();
} }
private void OnWindowInitialized(object? sender, EventArgs e) protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
{ {
try try
{ {
@@ -30,23 +29,22 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
return; return;
} }
if (sizeItem.Width > 0 && !Width.Equals(sizeItem.Width)) var screen = Screens.ScreenFromWindow(this) ?? Screens.Primary;
{ var scaling = screen.Scaling > 0 ? screen.Scaling : 1.0;
Width = sizeItem.Width; var workingArea = screen.WorkingArea;
}
if (sizeItem.Height > 0 && !Height.Equals(sizeItem.Height)) var width = Math.Min(sizeItem.Width, workingArea.Width / scaling);
{ var height = Math.Min(sizeItem.Height, workingArea.Height / scaling);
Height = sizeItem.Height; var x = workingArea.X + ((workingArea.Width - (width * scaling)) / 2);
} var y = workingArea.Y + ((workingArea.Height - (height * scaling)) / 2);
Width = width;
Height = height;
Position = new PixelPoint((int)x, (int)y);
} }
catch { } catch { }
} }
protected virtual void OnLoaded(object? sender, RoutedEventArgs e)
{
}
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {
base.OnClosed(e); base.OnClosed(e);
@@ -0,0 +1,69 @@
using Avalonia.Controls.Templates;
using v2rayN.Desktop.ViewModels;
using v2rayN.Desktop.Views;
namespace v2rayN.Desktop.Common;
public class SimpleViewLocator : IDataTemplate
{
private static readonly Lazy<SimpleViewLocator> _instance = new(() => new SimpleViewLocator());
private readonly Dictionary<Type, Func<Control?>> _locator = new();
private SimpleViewLocator()
{
RegisterViewFactory<AddGroupServerViewModel, AddGroupServerWindow>();
RegisterViewFactory<AddServer2ViewModel, AddServer2Window>();
RegisterViewFactory<AddServerViewModel, AddServerWindow>();
RegisterViewFactory<BackupAndRestoreViewModel, BackupAndRestoreView>();
RegisterViewFactory<CheckUpdateViewModel, CheckUpdateView>();
RegisterViewFactory<ClashConnectionsViewModel, ClashConnectionsView>();
RegisterViewFactory<ClashProxiesViewModel, ClashProxiesView>();
RegisterViewFactory<DNSSettingViewModel, DNSSettingWindow>();
RegisterViewFactory<FullConfigTemplateViewModel, FullConfigTemplateWindow>();
RegisterViewFactory<GlobalHotkeySettingViewModel, GlobalHotkeySettingWindow>();
RegisterViewFactory<MainWindowViewModel, MainWindow>();
RegisterViewFactory<MsgViewModel, MsgView>();
RegisterViewFactory<OptionSettingViewModel, OptionSettingWindow>();
RegisterViewFactory<ProfilesSelectViewModel, ProfilesSelectWindow>();
RegisterViewFactory<ProfilesViewModel, ProfilesView>();
RegisterViewFactory<RoutingRuleDetailsViewModel, RoutingRuleDetailsWindow>();
RegisterViewFactory<RoutingRuleSettingViewModel, RoutingRuleSettingWindow>();
RegisterViewFactory<RoutingSettingViewModel, RoutingSettingWindow>();
RegisterViewFactory<StatusBarViewModel, StatusBarView>();
RegisterViewFactory<SubEditViewModel, SubEditWindow>();
RegisterViewFactory<SubSettingViewModel, SubSettingWindow>();
RegisterViewFactory<ThemeSettingViewModel, ThemeSettingView>();
}
public static SimpleViewLocator Instance => _instance.Value;
public Control Build(object? data)
{
if (data is null)
{
return new TextBlock { Text = "No VM provided" };
}
_locator.TryGetValue(data.GetType(), out var factory);
return factory?.Invoke() ?? new TextBlock { Text = $"VM Not Registered: {data.GetType()}" };
}
public bool Match(object? data)
{
return data is MyReactiveObject;
}
public void RegisterViewFactory<TViewModel>(Func<Control> factory) where TViewModel : class
{
_locator.Add(typeof(TViewModel), factory);
}
public void RegisterViewFactory<TViewModel, TView>()
where TViewModel : class
where TView : Control, new()
{
_locator.Add(typeof(TViewModel), () => new TView());
}
}
+9 -6
View File
@@ -1,4 +1,5 @@
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using v2rayN.Desktop.Manager;
using v2rayN.Desktop.Views; using v2rayN.Desktop.Views;
namespace v2rayN.Desktop.Common; namespace v2rayN.Desktop.Common;
@@ -7,16 +8,17 @@ internal class UI
{ {
private static readonly string caption = Global.AppName; private static readonly string caption = Global.AppName;
public static async Task<ButtonResult> ShowYesNo(Window owner, string msg) public static async Task<ButtonResult> ShowYesNo(string msg)
{ {
var owner = WindowDialog.TryGetOwnerWindow();
var box = new MessageBoxDialog(caption, msg); var box = new MessageBoxDialog(caption, msg);
var result = await box.ShowDialog<ButtonResult>(owner); var result = await box.ShowDialog<ButtonResult>(owner);
return result == ButtonResult.Yes ? ButtonResult.Yes : ButtonResult.No; return result == ButtonResult.Yes ? ButtonResult.Yes : ButtonResult.No;
} }
public static async Task<string?> OpenFileDialog(Window owner, FilePickerFileType? filter) public static async Task<string?> OpenFileDialog(FilePickerFileType? filter)
{ {
var sp = GetStorageProvider(owner); var sp = GetStorageProvider();
if (sp is null) if (sp is null)
{ {
return null; return null;
@@ -32,9 +34,9 @@ internal class UI
return files.FirstOrDefault()?.TryGetLocalPath(); return files.FirstOrDefault()?.TryGetLocalPath();
} }
public static async Task<string?> SaveFileDialog(Window owner, string filter) public static async Task<string?> SaveFileDialog(string filter)
{ {
var sp = GetStorageProvider(owner); var sp = GetStorageProvider();
if (sp is null) if (sp is null)
{ {
return null; return null;
@@ -48,8 +50,9 @@ internal class UI
return files?.TryGetLocalPath(); return files?.TryGetLocalPath();
} }
private static IStorageProvider? GetStorageProvider(Window owner) private static IStorageProvider? GetStorageProvider()
{ {
var owner = WindowDialog.TryGetOwnerWindow();
var topLevel = TopLevel.GetTopLevel(owner); var topLevel = TopLevel.GetTopLevel(owner);
return topLevel?.StorageProvider; return topLevel?.StorageProvider;
} }
@@ -0,0 +1,99 @@
using ServiceLib.Models.Entities;
using System.Diagnostics;
using v2rayN.Desktop.Manager;
using v2rayN.Desktop.ViewModels;
namespace v2rayN.Desktop.DesignData;
/// <summary>
/// Provides design-time data for Avalonia XAML previewer.
/// Each inner class lazily initializes <see cref="AppManager"/> with a stub config
/// so that ViewModel constructors don't fail during design-time rendering.
/// </summary>
public static class DesignData
{
// ── Parameterless-constructor ViewModels ───────────────────────────────
public static MainWindowViewModel? MainWindow { get; } = SafeCreate(CreateMainWindow);
public static ProfilesViewModel? Profiles { get; } = SafeCreate(() => new ProfilesViewModel());
public static StatusBarViewModel? StatusBar { get; } = SafeCreate(CreateStatusBar);
public static MsgViewModel? Msg { get; } = SafeCreate(() => new MsgViewModel());
public static SubSettingViewModel? SubSetting { get; } = SafeCreate(() => new SubSettingViewModel());
public static RoutingSettingViewModel? RoutingSetting { get; } = SafeCreate(() => new RoutingSettingViewModel());
public static ClashProxiesViewModel? ClashProxies { get; } = SafeCreate(() => new ClashProxiesViewModel());
public static ClashConnectionsViewModel? ClashConnections { get; } = SafeCreate(() => new ClashConnectionsViewModel());
public static CheckUpdateViewModel? CheckUpdate { get; } = SafeCreate(() => new CheckUpdateViewModel());
public static DNSSettingViewModel? DNSSetting { get; } = SafeCreate(() => new DNSSettingViewModel());
public static FullConfigTemplateViewModel? FullConfigTemplate { get; } = SafeCreate(() => new FullConfigTemplateViewModel());
public static GlobalHotkeySettingViewModel? GlobalHotkeySetting { get; } = SafeCreate(() => new GlobalHotkeySettingViewModel());
public static OptionSettingViewModel? OptionSetting { get; } = SafeCreate(() => new OptionSettingViewModel());
public static ProfilesSelectViewModel? ProfilesSelect { get; } = SafeCreate(() => new ProfilesSelectViewModel());
public static BackupAndRestoreViewModel? BackupAndRestore { get; } = SafeCreate(() => new BackupAndRestoreViewModel());
public static ThemeSettingViewModel? ThemeSetting { get; } = SafeCreate(() => new ThemeSettingViewModel());
// ── ViewModels that require constructor parameters ─────────────────────
public static AddGroupServerViewModel? AddGroupServer { get; } = SafeCreate(() => new AddGroupServerViewModel(new ProfileItem { Remarks = "Design Group", ConfigType = EConfigType.PolicyGroup }));
public static AddServer2ViewModel? AddServer2 { get; } = SafeCreate(() => new AddServer2ViewModel(new ProfileItem { Remarks = "Design Custom Server", ConfigType = EConfigType.Custom }));
public static AddServerViewModel? AddServer { get; } = SafeCreate(() => new AddServerViewModel(new ProfileItem { Remarks = "Design VMess Server", ConfigType = EConfigType.VMess, Address = "example.com", Port = 443 }));
public static RoutingRuleSettingViewModel? RoutingRuleSetting { get; } = SafeCreate(() => new RoutingRuleSettingViewModel(new RoutingItem { Remarks = "Design Routing Rule" }));
public static RoutingRuleDetailsViewModel? RoutingRuleDetails { get; } = SafeCreate(() => new RoutingRuleDetailsViewModel(new RulesItem { Domain = ["example.com"], OutboundTag = "direct" }));
public static SubEditViewModel? SubEdit { get; } = SafeCreate(() => new SubEditViewModel(new SubItem { Remarks = "Design Subscription", Url = "https://example.com/sub" }));
// ── Helper factories ───────────────────────────────────────────────────
private static MainWindowViewModel CreateMainWindow()
{
var vm = new MainWindowViewModel { DesignMode = true };
return vm;
}
private static StatusBarViewModel CreateStatusBar()
{
var vm = StatusBarViewModel.Instance;
vm.InboundDisplay = "socks:10808";
vm.InboundLanDisplay = "http:10809";
vm.RunningServerDisplay = "🚀 Design Server (Active)";
vm.RunningInfoDisplay = "v2rayN Design Mode";
vm.SpeedProxyDisplay = "↑ 1.2 MB/s";
vm.SpeedDirectDisplay = "↓ 5.6 MB/s";
vm.RoutingItems.Add(new RoutingItem { Remarks = "Default Routing" });
vm.RoutingItems.Add(new RoutingItem { Remarks = "Global" });
return vm;
}
private static T? SafeCreate<T>(Func<T> factory) where T : class
{
try
{
AppManager.Instance.InitApp();
AppManager.Instance.WindowDialog = new WindowDialog();
return factory();
}
catch (Exception ex)
{
Debug.WriteLine($"[DesignData] Failed to create {typeof(T).Name}: {ex}");
return null;
}
}
}
+1
View File
@@ -3,6 +3,7 @@ global using System.Collections.Generic;
global using System.Globalization; global using System.Globalization;
global using System.IO; global using System.IO;
global using System.Linq; global using System.Linq;
global using System.Reactive;
global using System.Reactive.Disposables.Fluent; global using System.Reactive.Disposables.Fluent;
global using System.Reactive.Linq; global using System.Reactive.Linq;
global using System.Runtime.Versioning; global using System.Runtime.Versioning;
@@ -8,6 +8,7 @@ public sealed class HotkeyManager
private static readonly Lazy<HotkeyManager> _instance = new(() => new()); private static readonly Lazy<HotkeyManager> _instance = new(() => new());
public static HotkeyManager Instance = _instance.Value; public static HotkeyManager Instance = _instance.Value;
private readonly Dictionary<int, EGlobalHotkey> _hotkeyTriggerDic = new(); private readonly Dictionary<int, EGlobalHotkey> _hotkeyTriggerDic = new();
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private GlobalHotKeys.HotKeyManager? _hotKeyManager; private GlobalHotKeys.HotKeyManager? _hotKeyManager;
@@ -74,9 +75,7 @@ public sealed class HotkeyManager
} }
} }
_hotKeyManager?.HotKeyPressed _hotKeyManager.HotKeyPressed += OnNext;
.ObserveOn(AvaloniaScheduler.Instance)
.Subscribe(OnNext);
} }
private void OnNext(HotKey key) private void OnNext(HotKey key)
@@ -0,0 +1,68 @@
using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Manager;
public class WindowDialog : IWindowDialog
{
public async Task<bool> ShowDialogAsync<TViewModel>(TViewModel vm)
where TViewModel : class
{
var owner = TryGetOwnerWindow();
var view = SimpleViewLocator.Instance.Build(vm);
if (view is not WindowBase<TViewModel> window)
{
return false;
}
window.ViewModel = vm;
if (vm is ServiceLib.Base.ICloseable closeable)
{
closeable.RequestClose += (_, _) => Dispatch(() => window.Close(true));
}
var result = await window.ShowDialog<bool>(owner);
return result;
}
public static Window TryGetOwnerWindow()
{
var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime
?? throw new InvalidOperationException("Application lifetime is not of type IClassicDesktopStyleApplicationLifetime.");
var openWindows = desktop.Windows
.Where(w => w.IsVisible)
.ToArray();
if (openWindows.Length == 0)
{
return desktop.MainWindow
?? throw new InvalidOperationException("No open windows and no main window found.");
}
if (openWindows.Length == 1)
{
return openWindows[0];
}
try
{
Window.SortWindowsByZOrder(openWindows);
var activeTopmost = openWindows.Reverse().FirstOrDefault(w => w.IsActive);
return activeTopmost ?? openWindows[^1];
}
catch
{
return desktop.Windows.FirstOrDefault(w => w.IsActive)
?? desktop.MainWindow
?? openWindows[0];
}
}
private static void Dispatch(Action action)
{
Dispatcher.UIThread.InvokeAsync(action);
}
}
+6
View File
@@ -1,4 +1,5 @@
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
using v2rayN.Desktop.Manager;
namespace v2rayN.Desktop; namespace v2rayN.Desktop;
@@ -48,6 +49,8 @@ internal class Program
{ {
return false; return false;
} }
AppManager.Instance.WindowDialog = new WindowDialog();
return true; return true;
} }
@@ -58,6 +61,9 @@ internal class Program
.UsePlatformDetect() .UsePlatformDetect()
//.WithInterFont() //.WithInterFont()
.WithFontByDefault() .WithFontByDefault()
#if DEBUG
.WithDeveloperTools()
#endif
.LogToTrace() .LogToTrace()
.UseReactiveUI(_ => { }); .UseReactiveUI(_ => { });
@@ -3,6 +3,7 @@
xmlns="https://github.com/avaloniaui" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib"
@@ -10,6 +11,7 @@
Width="900" Width="900"
Height="700" Height="700"
x:DataType="vms:AddGroupServerViewModel" x:DataType="vms:AddGroupServerViewModel"
Design.DataContext="{x:Static dd:DesignData.AddGroupServer}"
ShowInTaskbar="False" ShowInTaskbar="False"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -5,11 +5,6 @@ namespace v2rayN.Desktop.Views;
public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel> public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
{ {
public AddGroupServerWindow() public AddGroupServerWindow()
{
InitializeComponent();
}
public AddGroupServerWindow(ProfileItem profileItem)
{ {
InitializeComponent(); InitializeComponent();
@@ -18,8 +13,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
lstChild.SelectionChanged += LstChild_SelectionChanged; lstChild.SelectionChanged += LstChild_SelectionChanged;
tabControl.SelectionChanged += TabControl_SelectionChanged; tabControl.SelectionChanged += TabControl_SelectionChanged;
ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler);
cmbCoreType.ItemsSource = Global.CoreTypes; cmbCoreType.ItemsSource = Global.CoreTypes;
cmbPolicyGroupType.ItemsSource = new List<string> cmbPolicyGroupType.ItemsSource = new List<string>
{ {
@@ -31,6 +24,42 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
}; };
cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList; cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList;
this.WhenActivated(disposables =>
{
this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.WhereNotNull()
.Subscribe(InitializeData)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCmd, v => v.menuAddChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
// Context menu actions that require custom logic (Add, SelectAll)
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
}
private void InitializeData(ProfileItem profileItem)
{
switch (profileItem.ConfigType) switch (profileItem.ConfigType)
{ {
case EConfigType.PolicyGroup: case EConfigType.PolicyGroup:
@@ -46,34 +75,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
} }
break; break;
} }
this.WhenActivated(disposables =>
{
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables);
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables);
});
// Context menu actions that require custom logic (Add, SelectAll)
menuAddChildServer.Click += MenuAddChild_Click;
menuSelectAllChild.Click += (s, e) => lstChild.SelectAll();
// Keyboard shortcuts when focus is within grid
AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel);
lstChild.LoadingRow += LstChild_LoadingRow;
} }
private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e) private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e)
@@ -81,17 +82,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
e.Row.Header = $" {e.Row.Index + 1}"; e.Row.Header = $" {e.Row.Index + 1}";
} }
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
{
switch (action)
{
case EViewAction.CloseWindow:
Close(true);
break;
}
return await Task.FromResult(true);
}
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
{ {
txtRemarks.Focus(); txtRemarks.Focus();
@@ -145,19 +135,6 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
} }
} }
private async void MenuAddChild_Click(object? sender, RoutedEventArgs e)
{
var selectWindow = new ProfilesSelectWindow();
selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
selectWindow.AllowMultiSelect(true);
var result = await selectWindow.ShowDialog<bool?>(this);
if (result == true)
{
var profiles = await selectWindow.ProfileItems;
ViewModel?.ChildItemsObs.AddRange(profiles);
}
}
private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e) private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (ViewModel != null) if (ViewModel != null)

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