Compare commits

...

22 Commits

Author SHA1 Message Date
2dust
5efff37acb up 7.24.5 2026-08-05 19:49:08 +08:00
2dust
e8cdd1cc02 Remove allowInsecure from the Xray Core
https://github.com/2dust/v2rayN/issues/9893
2026-08-05 18:48:39 +08:00
2dust
d381d429e4 Wait briefly after sudo process kills
https://github.com/2dust/v2rayN/issues/9863
Add a short 1-second delay after terminating core processes on Linux, macOS, and in CoreAdminManager so ports and process resources have time to settle before the next step runs.
2026-08-05 17:03:23 +08:00
DHR60
b27ae11d1e Adjust allow insecure fmt (#9888)
* Adjust allow insecure fmt

* Remove `VLESS` insecure fmt
2026-08-05 14:26:16 +08:00
DHR60
eff584597f Add custom outbound support (#9817)
* Add custom outbound support

* Add test

* Add inner fmt support

* Full config to outbounds

* Rename to `Outbound`

* AI optimized

* Fix

* Add bind interface placeholder

---------

Co-authored-by: 2dust <31833384+2dust@users.noreply.github.com>
2026-08-05 14:25:16 +08:00
2dust
ccf18ba0fb Bug fix
https://github.com/2dust/v2rayN/issues/9896
2026-08-05 11:28:46 +08:00
sparklelcm333
15ba2bdf93 fix: 修复回归的窗口启动时偏右下偏移 (#9875) 2026-08-05 10:38:51 +08:00
Tylerrrkd
e1e6c5ddb0 Fix infinite TUN routing loop on traffic to the TUN's own addresses (#9897)
* Restore the silently dropped embedded TUN routing rules

tun_singbox_rules declares "network": "udp" as a bare string, but
Rule4Sbox.network is a List<string>. System.Text.Json therefore throws on
the very first rule, and JsonUtils.Deserialize swallows it and returns
default. GenRouting only checks for null before AddRange, so the whole
embedded rule set is dropped without a trace.

Both rules it carries have thus never reached a generated config: the
NetBIOS/mDNS reject and the multicast reject. The mismatch predates the
sing-box 1.12 migration - the template has used the string form since the
rules file was introduced.

- declare network as an array so the template matches Rule4Sbox

* Drop traffic addressed to the TUN's own addresses

With auto_route the TUN steals the default route, so a packet whose
destination is the TUN interface's own address is handed to sing-box instead
of being looped back by the kernel. Routing then matches ip_is_private and
sends it to `direct`, whose interface is auto-detected as the TUN again, so
the packet is written straight back into the TUN and re-enters routing. The
loop never terminates and pins a CPU core.

Seen in the wild on macOS: a WebRTC client offered the TUN's own
fc00::172:18:0:1 ULA as an ICE candidate and the resulting STUN
connectivity checks sustained ~8k packets/s out of the interface at 800%
CPU, 21 GB written to the TUN over five hours - against 3 GB read, the
asymmetry that gives the loop away.

Nothing legitimate is addressed to those addresses, so reject them before
any outbound rule can match. Use method "drop" rather than the default
ICMP unreachable, whose destination would be the looping address itself.

- reject the TUN inbound's own addresses, taken from the generated inbound
  so the two cannot drift apart
2026-08-05 10:35:37 +08:00
DHR60
d924c6f557 ReactiveUI 24 (#9678)
* ReactiveUI.SourceGenerators

* Primitives

* Try fix
2026-08-01 10:08:43 +08:00
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
125 changed files with 2320 additions and 1036 deletions

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
@@ -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

View File

@@ -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"

View File

@@ -1,7 +1,7 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>7.24.2</Version> <Version>7.24.5</Version>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>

View File

@@ -12,16 +12,16 @@
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" /> <PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="DialogHost.Avalonia" Version="0.12.3" /> <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="12.0.3" /> <PackageVersion Include="ReactiveUI.Avalonia" Version="12.1.0" />
<PackageVersion Include="CliWrap" Version="3.10.2" /> <PackageVersion Include="CliWrap" Version="3.10.2" />
<PackageVersion Include="Downloader" Version="5.9.5" /> <PackageVersion Include="Downloader" Version="5.9.5" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" /> <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="24.0.0" />
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" /> <PackageVersion Include="ReactiveUI.SourceGenerators" Version="3.1.0" />
<PackageVersion Include="ReactiveUI.WPF" Version="23.2.28" /> <PackageVersion Include="ReactiveUI.WPF" Version="24.0.0" />
<PackageVersion Include="Semi.Avalonia" Version="12.1.0" /> <PackageVersion Include="Semi.Avalonia" Version="12.1.0" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" /> <PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="12.0.0" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" /> <PackageVersion Include="Semi.Avalonia.DataGrid" Version="12.1.0" />

View File

@@ -150,6 +150,23 @@ internal static class CoreConfigTestFactory
}; };
} }
public static ProfileItem CreateCustomOutboundNode(ECoreType coreType, string indexId = "node-custom-1",
string remarks = "demo-custom-outbound", string address = "custom_outbound.json")
{
return new ProfileItem
{
IndexId = indexId,
ConfigType = EConfigType.Outbound,
CoreType = coreType,
Remarks = remarks,
Address = address,
Port = 0,
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)
{ {

View File

@@ -592,4 +592,74 @@ 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");
}
}
[Fact]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.sing_box);
CoreConfigTestFactory.BindAppManagerConfig(config);
var customNode = CoreConfigTestFactory.CreateCustomOutboundNode(ECoreType.sing_box, "n-custom", "custom-singbox");
var customJsonContent = """
{
"type": "shadowsocks",
"server": "1.2.3.4",
"server_port": 8388,
"method": "aes-128-gcm",
"password": "custom_password"
}
""";
var context = CoreConfigTestFactory.CreateContext(config, customNode, ECoreType.sing_box);
context.CustomOutboundContent[customNode.IndexId] = customJsonContent;
var result = new CoreConfigSingboxService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<SingboxConfig>(result.Data!.ToString());
cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
proxyOutbound.Should().NotBeNull();
proxyOutbound!.type.Should().Be("shadowsocks");
proxyOutbound.server.Should().Be("1.2.3.4");
proxyOutbound.server_port.Should().Be(8388);
proxyOutbound.method.Should().Be("aes-128-gcm");
proxyOutbound.password.Should().Be("custom_password");
}
} }

View File

@@ -592,4 +592,43 @@ public class CoreConfigV2rayServiceTests
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32"); tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.0/32");
tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31"); tunInbound!.settings.autoSystemRoutingTable.Should().Contain("10.0.0.2/31");
} }
[Fact]
public void GenerateClientConfigContent_CustomOutbound_ShouldReplaceWithUserCustomOutboundJson()
{
var config = CoreConfigTestFactory.CreateConfig(ECoreType.Xray);
CoreConfigTestFactory.BindAppManagerConfig(config);
var customNode = CoreConfigTestFactory.CreateCustomOutboundNode(ECoreType.Xray, "n-custom", "custom-xray");
var customJsonContent = """
{
"protocol": "shadowsocks",
"settings": {
"servers": [
{
"address": "1.2.3.4",
"port": 8388,
"method": "aes-128-gcm",
"password": "custom_password"
}
]
}
}
""";
var context = CoreConfigTestFactory.CreateContext(config, customNode, ECoreType.Xray);
context.CustomOutboundContent[customNode.IndexId] = customJsonContent;
var result = new CoreConfigV2rayService(context).GenerateClientConfigContent();
result.Success.Should().BeTrue($"ret msg: {result.Msg}");
result.Data.Should().NotBeNull();
var cfg = JsonUtils.Deserialize<V2rayConfig>(result.Data!.ToString());
cfg.Should().NotBeNull();
var proxyOutbound = cfg!.outbounds.FirstOrDefault(o => o.tag == Global.ProxyTag);
proxyOutbound.Should().NotBeNull();
proxyOutbound!.protocol.Should().Be("shadowsocks");
proxyOutbound.settings.servers.Should().NotBeNull();
}
} }

View File

@@ -3,9 +3,6 @@ global using System.Diagnostics;
global using System.Net; global using System.Net;
global using System.Net.NetworkInformation; global using System.Net.NetworkInformation;
global using System.Net.Sockets; global using System.Net.Sockets;
global using System.Reactive;
global using System.Reactive.Disposables;
global using System.Reactive.Linq;
global using System.Reflection; global using System.Reflection;
global using System.Runtime.InteropServices; global using System.Runtime.InteropServices;
global using System.Security.Cryptography; global using System.Security.Cryptography;
@@ -15,10 +12,7 @@ global using System.Text.Json;
global using System.Text.Json.Nodes; global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization; global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions; global using System.Text.RegularExpressions;
global using DynamicData;
global using DynamicData.Binding;
global using ReactiveUI; global using ReactiveUI;
global using ReactiveUI.Fody.Helpers;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;

View File

@@ -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();
}
}

View File

@@ -0,0 +1,66 @@
namespace ServiceLib.Base;
public class BulkObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification = false;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnCollectionChanged(e);
}
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
{
base.OnPropertyChanged(e);
}
}
public void AddRange(IEnumerable<T>? collection)
{
if (collection == null)
{
return;
}
_suppressNotification = true;
try
{
foreach (var item in collection)
{
Add(item);
}
}
finally
{
_suppressNotification = false;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public bool Replace(T oldItem, T newItem)
{
var index = Items.IndexOf(oldItem);
if (index < 0)
{
return false;
}
Items[index] = newItem;
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace,
newItem,
oldItem,
index));
return true;
}
}

View File

@@ -92,7 +92,7 @@ public static class Extension
public static bool IsComplexType(this EConfigType configType) public static bool IsComplexType(this EConfigType configType)
{ {
return configType is EConfigType.Custom or EConfigType.PolicyGroup or EConfigType.ProxyChain; return configType is EConfigType.Custom or EConfigType.Outbound or EConfigType.PolicyGroup or EConfigType.ProxyChain;
} }
/// <summary> /// <summary>

View File

@@ -14,6 +14,7 @@ public enum EConfigType
HTTP = 10, HTTP = 10,
Anytls = 11, Anytls = 11,
Naive = 12, Naive = 12,
Outbound = 13,
PolicyGroup = 101, PolicyGroup = 101,
ProxyChain = 102, ProxyChain = 102,
} }

View File

@@ -2,7 +2,7 @@ namespace ServiceLib.Events;
public static class AppEvents public static class AppEvents
{ {
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new(); public static readonly EventChannel<RxVoid> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> HasUpdateNotified = new(); public static readonly EventChannel<bool> HasUpdateNotified = new();
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new(); public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
@@ -10,7 +10,7 @@ public static class AppEvents
public static readonly EventChannel<string> SendSnackMsgRequested = new(); public static readonly EventChannel<string> SendSnackMsgRequested = new();
public static readonly EventChannel<string> SendMsgViewRequested = new(); public static readonly EventChannel<string> SendMsgViewRequested = new();
public static readonly EventChannel<Unit> AppExitRequested = new(); public static readonly EventChannel<RxVoid> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = new(); public static readonly EventChannel<bool> ShutdownRequested = new();
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new(); public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();

View File

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

View File

@@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@@ -745,7 +745,7 @@ public class Global
MozillaRootProvider, MozillaRootProvider,
]; ];
public static readonly IReadOnlyList<string> TunIpv4Address = public static readonly IReadOnlyList<string> TunIPv4Address =
[ [
"172.18.0.1/30", "172.18.0.1/30",
"172.31.0.1/30", "172.31.0.1/30",
@@ -757,15 +757,17 @@ public class Global
"10.0.0.1/30", "10.0.0.1/30",
]; ];
public static readonly IReadOnlyList<string> TunIpv6Address = // 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/128", "fc00::172:18:0:1/126",
"fc00::172:31:0:1/128", "fc00::172:31:0:1/126",
"fc00::172:20:0:1/128", "fc00::172:20:0:1/126",
"fc00::172:16:0:1/128", "fc00::172:16:0:1/126",
"fc00::192:168:100:1/128", "fc00::192:168:100:1/126",
"fc00::10:10:14:1/128", "fc00::10:10:14:1/126",
"fc00::10:1:0:1/128", "fc00::10:1:0:1/126",
"fc00::10:0:0:1/128", "fc00::10:0:0:1/126",
]; ];
} }

View File

@@ -1,11 +1,15 @@
global using System.Collections.Concurrent; global using System.Collections.Concurrent;
global using System.Collections.ObjectModel;
global using System.Collections.Specialized;
global using System.ComponentModel;
global using System.Diagnostics; global using System.Diagnostics;
global using System.Net; global using System.Net;
global using System.Net.NetworkInformation; global using System.Net.NetworkInformation;
global using System.Net.Sockets; global using System.Net.Sockets;
global using System.Reactive; global using ReactiveUI.Primitives;
global using System.Reactive.Disposables; global using ReactiveUI.Primitives.Concurrency;
global using System.Reactive.Linq; global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.Primitives.Signals;
global using System.Reflection; global using System.Reflection;
global using System.Runtime.InteropServices; global using System.Runtime.InteropServices;
global using System.Runtime.Versioning; global using System.Runtime.Versioning;
@@ -16,10 +20,8 @@ global using System.Text.Json;
global using System.Text.Json.Nodes; global using System.Text.Json.Nodes;
global using System.Text.Json.Serialization; global using System.Text.Json.Serialization;
global using System.Text.RegularExpressions; global using System.Text.RegularExpressions;
global using DynamicData;
global using DynamicData.Binding;
global using ReactiveUI; global using ReactiveUI;
global using ReactiveUI.Fody.Helpers; global using ReactiveUI.SourceGenerators;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;
global using ServiceLib.Enums; global using ServiceLib.Enums;
@@ -39,3 +41,5 @@ global using ServiceLib.Services;
global using ServiceLib.Services.CoreConfig; global using ServiceLib.Services.CoreConfig;
global using ServiceLib.Services.Statistics; global using ServiceLib.Services.Statistics;
global using SQLite; global using SQLite;

View File

@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations;
namespace ServiceLib.Handler.Builder; namespace ServiceLib.Handler.Builder;
public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult) public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult)
@@ -49,6 +51,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);
@@ -94,6 +97,7 @@ public class CoreConfigContextBuilder
context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode;
} }
} }
if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 }) if (context.IsTunEnabled && context.AppConfig.TunModeItem.RouteExcludeAddress is { Count: > 0 })
{ {
var appConfig = JsonUtils.DeepCopy(config); var appConfig = JsonUtils.DeepCopy(config);
@@ -321,14 +325,14 @@ public class CoreConfigContextBuilder
{ {
return await RegisterGroupNodeAsync(context, node); return await RegisterGroupNodeAsync(context, node);
} }
return RegisterSingleNodeAsync(context, node); return await RegisterSingleNodeAsync(context, node);
} }
/// <summary> /// <summary>
/// Validates a single (non-group) node and, on success, adds it to the proxy map /// Validates a single (non-group) node and, on success, adds it to the proxy map
/// and records any domain addresses that should bypass the proxy. /// and records any domain addresses that should bypass the proxy.
/// </summary> /// </summary>
private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node) private static async Task<NodeValidatorResult> RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node)
{ {
if (node.ConfigType.IsGroupType()) if (node.ConfigType.IsGroupType())
{ {
@@ -336,6 +340,29 @@ public class CoreConfigContextBuilder
} }
var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType); var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType);
if (node.ConfigType == EConfigType.Outbound)
{
var addressFileName = node.Address;
if (!File.Exists(addressFileName))
{
addressFileName = Utils.GetConfigPath(addressFileName);
}
if (!File.Exists(addressFileName))
{
nodeValidatorResult.Errors.Add(string.Format(ResUI.MsgCustomOutboundFileNotFound, node.Remarks, addressFileName));
}
try
{
var fileContent = await File.ReadAllTextAsync(addressFileName);
context.CustomOutboundContent[node.IndexId] = fileContent;
}
catch
{
nodeValidatorResult.Errors.Add(string.Format(ResUI.MsgCustomOutboundFileNotFound, node.Remarks, addressFileName));
}
}
var msgs = new List<string>([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]); var msgs = new List<string>([.. nodeValidatorResult.Errors, .. nodeValidatorResult.Warnings]);
if (msgs.Count > 0) if (msgs.Count > 0)
{ {
@@ -437,7 +464,7 @@ public class CoreConfigContextBuilder
if (!childNode.ConfigType.IsGroupType()) if (!childNode.ConfigType.IsGroupType())
{ {
var childNodeResult = RegisterSingleNodeAsync(context, childNode); var childNodeResult = await RegisterSingleNodeAsync(context, childNode);
childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w =>
string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w))); string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w)));
childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e =>

View File

@@ -36,6 +36,15 @@ public class NodeValidator
return; return;
} }
if (item.ConfigType is EConfigType.Outbound)
{
if (item.CoreType != coreType)
{
v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, coreType.ToString(), item.ConfigType));
}
return;
}
if (item.ConfigType.IsGroupType()) if (item.ConfigType.IsGroupType())
{ {
// Group logic is handled in ValidateGroupNode // Group logic is handled in ValidateGroupNode

View File

@@ -92,7 +92,7 @@ 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)) if (!Global.RootCertProviders.Contains(config.GuiItem.RootCertProvider))
@@ -382,6 +382,13 @@ public static class ConfigHandler
{ {
} }
} }
else if (profileItem.ConfigType == EConfigType.Outbound)
{
profileItem.Address = Utils.GetConfigPath(profileItem.Address);
if (await AddCustomOutboundServer(config, profileItem, false) == 0)
{
}
}
else else
{ {
await AddServerCommon(config, profileItem, true); await AddServerCommon(config, profileItem, true);
@@ -579,6 +586,44 @@ public static class ConfigHandler
return 0; return 0;
} }
public static async Task<int> AddCustomOutboundServer(Config config, ProfileItem profileItem, bool blDelete, bool toFile = true)
{
var fileName = profileItem.Address;
if (!File.Exists(fileName))
{
return -1;
}
var ext = Path.GetExtension(fileName);
var newFileName = $"{Utils.GetGuid()}{ext}";
//newFileName = Path.Combine(Utile.GetTempPath(), newFileName);
try
{
File.Copy(fileName, Utils.GetConfigPath(newFileName));
if (blDelete)
{
File.Delete(fileName);
}
}
catch (Exception ex)
{
Logging.SaveLog(_tag, ex);
return -1;
}
profileItem.Address = newFileName;
profileItem.ConfigType = EConfigType.Outbound;
if (profileItem.Remarks.IsNullOrEmpty())
{
profileItem.Remarks = $"import custom outbound@{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")}";
}
await AddServerCommon(config, profileItem, toFile);
return 0;
}
/// <summary> /// <summary>
/// Edit an existing custom server configuration /// Edit an existing custom server configuration
/// Updates the server's properties without changing the file /// Updates the server's properties without changing the file
@@ -600,6 +645,8 @@ public static class ConfigHandler
item.CoreType = profileItem.CoreType; item.CoreType = profileItem.CoreType;
item.DisplayLog = profileItem.DisplayLog; item.DisplayLog = profileItem.DisplayLog;
item.PreSocksPort = profileItem.PreSocksPort; item.PreSocksPort = profileItem.PreSocksPort;
item.ProtoExtra = profileItem.ProtoExtra;
} }
if (await SQLiteHelper.Instance.UpdateAsync(item) > 0) if (await SQLiteHelper.Instance.UpdateAsync(item) > 0)
@@ -1479,7 +1526,7 @@ public static class ConfigHandler
var matchedChildProfiles = childProfiles?.Where(p => var matchedChildProfiles = childProfiles?.Where(p =>
p != null && p != null &&
p.IsValid() && p.IsValid() &&
!p.ConfigType.IsComplexType() && (!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter)) (extraItem.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extraItem.Filter))
) )
.ToList() ?? []; .ToList() ?? [];
@@ -1675,68 +1722,179 @@ public static class ConfigHandler
} }
var subItem = await AppManager.Instance.GetSubItem(subid); var subItem = await AppManager.Instance.GetSubItem(subid);
if (subItem?.CustomCoreType is null)
{
return await AddBatchServersDefaultCustom(config, strData, subid, isSub, subItem);
}
return await AddBatchServersSpecificCustom(config, strData, subid, isSub, subItem);
}
private static async Task<int> AddBatchServersDefaultCustom(
Config config,
string strData,
string subid,
bool isSub,
SubItem? subItem)
{
var subRemarks = subItem?.Remarks; var subRemarks = subItem?.Remarks;
var preSocksPort = subItem?.PreSocksPort; // Safe Mode: Only allow full configuration if it's not from a subscription
var lstProfiles = V2rayFmt.ResolveToCustomOutbound(strData, subRemarks);
List<ProfileItem>? lstProfiles = null; if (lstProfiles.Count == 0)
//Is sing-box array configuration
if (lstProfiles is null || lstProfiles.Count <= 0)
{ {
lstProfiles = SingboxFmt.ResolveFullArray(strData, subRemarks); lstProfiles = SingboxFmt.ResolveToCustomOutbound(strData, subRemarks);
} }
//Is v2ray array configuration if (lstProfiles.Count == 0)
if (lstProfiles is null || lstProfiles.Count <= 0)
{
lstProfiles = V2rayFmt.ResolveFullArray(strData, subRemarks);
}
if (lstProfiles is { Count: > 0 })
{
var count = 0;
foreach (var it in lstProfiles)
{
it.Subid = subid;
it.IsSub = isSub;
it.PreSocksPort = preSocksPort;
if (await AddCustomServer(config, it, true) == 0)
{
count++;
}
}
if (count > 0)
{
return count;
}
}
ProfileItem? profileItem = null;
//Is sing-box configuration
profileItem ??= SingboxFmt.ResolveFull(strData, subRemarks);
//Is v2ray configuration
profileItem ??= V2rayFmt.ResolveFull(strData, subRemarks);
//Is Html Page
if (profileItem is null && HtmlPageFmt.IsHtmlPage(strData))
{ {
return -1; return -1;
} }
//Is Clash configuration
profileItem ??= ClashFmt.ResolveFull(strData, subRemarks); var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub);
//Is hysteria configuration if (count > 0)
profileItem ??= Hysteria2Fmt.ResolveFull2(strData, subRemarks); {
if (profileItem is null || profileItem.Address.IsNullOrEmpty()) return count;
}
if (HtmlPageFmt.IsHtmlPage(strData))
{
return -1;
}
var profileItem = ClashFmt.ResolveFull(strData, subRemarks)
?? Hysteria2Fmt.ResolveFull2(strData, subRemarks);
if (profileItem == null)
{ {
return -1; return -1;
} }
profileItem.Subid = subid; profileItem.Subid = subid;
profileItem.IsSub = isSub; profileItem.IsSub = isSub;
profileItem.PreSocksPort = preSocksPort; profileItem.PreSocksPort = subItem?.PreSocksPort;
if (await AddCustomServer(config, profileItem, true) == 0)
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
}
private static async Task<int> AddBatchServersSpecificCustom(
Config config,
string strData,
string subid,
bool isSub,
SubItem subItem)
{
var subRemarks = subItem.Remarks;
var customCoreType = subItem.CustomCoreType!.Value;
List<ProfileItem>? lstProfiles = customCoreType switch
{ {
return 1; ECoreType.Xray => V2rayFmt.ResolveToCustom(strData, subRemarks),
ECoreType.sing_box => SingboxFmt.ResolveToCustom(strData, subRemarks),
_ => null
};
if (lstProfiles is not null)
{
if (lstProfiles.Count == 0)
{
return -1;
}
var count = await AddCustomOutboundServers(config, lstProfiles, subid, isSub);
if (count > 0)
{
return count;
}
} }
else
return await SaveCustomRawFileServer(config, strData, subid, isSub, subItem, customCoreType);
}
private static async Task<int> AddCustomOutboundServers(
Config config,
List<ProfileItem> lstProfiles,
string subid,
bool isSub)
{
var count = 0;
foreach (var it in lstProfiles)
{ {
return -1; it.Subid = subid;
it.IsSub = isSub;
if (await AddCustomOutboundServer(config, it, true) == 0)
{
count++;
}
}
return count;
}
private static async Task<int> SaveCustomRawFileServer(
Config config,
string strData,
string subid,
bool isSub,
SubItem subItem,
ECoreType customCoreType)
{
var ext = DetectFileExtension(strData);
var fileName = Utils.GetTempPath($"{Utils.GetGuid(false)}{ext}");
await File.WriteAllTextAsync(fileName, strData);
var profileItem = new ProfileItem
{
CoreType = customCoreType,
ConfigType = EConfigType.Custom,
Address = fileName,
Remarks = subItem.Remarks ?? customCoreType.ToString(),
Subid = subid,
IsSub = isSub,
PreSocksPort = subItem.PreSocksPort,
};
return await AddCustomServer(config, profileItem, true) == 0 ? 1 : -1;
static string DetectFileExtension(string data)
{
var trimmed = data.AsSpan().TrimStart();
if (trimmed.IsEmpty)
{
return string.Empty;
}
if (trimmed[0] is '{' or '[')
{
return ".json";
}
if (trimmed.StartsWith("---"))
{
return ".yaml";
}
foreach (var line in trimmed.EnumerateLines())
{
var lineTrimmed = line.TrimStart();
if (lineTrimmed.IsEmpty || lineTrimmed.StartsWith("#"))
{
continue;
}
var colonIndex = lineTrimmed.IndexOf(':');
if (colonIndex > 0)
{
var keySpan = lineTrimmed[..colonIndex];
if (!keySpan.Contains(' ') && !keySpan.Contains('\t'))
{
if (colonIndex == lineTrimmed.Length - 1 || lineTrimmed[colonIndex + 1] is ' ' or '\t' or '\r' or '\n')
{
return ".yaml";
}
}
}
}
return string.Empty;
} }
} }
@@ -1837,6 +1995,7 @@ public static class ConfigHandler
EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false), EConfigType.Anytls => await AddAnytlsServer(config, profileItem, false),
EConfigType.Naive => await AddNaiveServer(config, profileItem, false), EConfigType.Naive => await AddNaiveServer(config, profileItem, false),
EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false), EConfigType.PolicyGroup or EConfigType.ProxyChain => await AddServerCommon(config, profileItem, false),
EConfigType.Outbound => await AddCustomOutboundServer(config, profileItem, true, false),
_ => -1, _ => -1,
}; };
if (addStatus == 0) if (addStatus == 0)
@@ -2038,6 +2197,7 @@ public static class ConfigHandler
item.NextProfile = subItem.NextProfile; item.NextProfile = subItem.NextProfile;
item.PreSocksPort = subItem.PreSocksPort; item.PreSocksPort = subItem.PreSocksPort;
item.Memo = subItem.Memo; item.Memo = subItem.Memo;
item.CustomCoreType = subItem.CustomCoreType;
} }
if (item.Id.IsNullOrEmpty()) if (item.Id.IsNullOrEmpty())
@@ -2078,7 +2238,7 @@ public static class ConfigHandler
{ {
return -1; return -1;
} }
var customProfile = await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid && t.ConfigType == EConfigType.Custom).ToListAsync(); var customProfile = await SQLiteHelper.Instance.TableAsync<ProfileItem>().Where(t => t.Subid == subid && (t.ConfigType == EConfigType.Custom || t.ConfigType == EConfigType.Outbound)).ToListAsync();
if (isSub) if (isSub)
{ {
await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'"); await SQLiteHelper.Instance.ExecuteAsync($"delete from ProfileItem where isSub = 1 and subid = '{subid}'");

View File

@@ -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)
}; };
} }

View File

@@ -25,6 +25,11 @@ public class AnytlsFmt : BaseFmt
var query = Utils.ParseQueryString(parsedUrl.Query); var query = Utils.ParseQueryString(parsedUrl.Query);
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
if (GetQueryValue(query, "insecure") == "1")
{
item.AllowInsecure = Global.StringTrue;
}
return item; return item;
} }
@@ -41,6 +46,10 @@ public class AnytlsFmt : BaseFmt
} }
var pw = item.Password; var pw = item.Password;
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.GetAllowInsecure())
{
dicQuery.Add("insecure", "1");
}
ToUriQuery(item, Global.None, ref dicQuery); ToUriQuery(item, Global.None, ref dicQuery);
return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark); return ToUri(EConfigType.Anytls, item.Address, item.Port, pw, dicQuery, remark);

View File

@@ -4,8 +4,6 @@ namespace ServiceLib.Handler.Fmt;
public class BaseFmt public class BaseFmt
{ {
private static readonly string[] _allowInsecureArray = new[] { "insecure", "allowInsecure", "allow_insecure" };
private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty); private static string UrlEncodeSafe(string? value) => Utils.UrlEncode(value ?? string.Empty);
protected static string GetIpv6(string address) protected static string GetIpv6(string address)
@@ -67,7 +65,6 @@ public class BaseFmt
{ {
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
} }
ToUriQueryAllowInsecure(item, ref dicQuery);
} }
if (item.EchConfigList.IsNotEmpty()) if (item.EchConfigList.IsNotEmpty())
{ {
@@ -196,25 +193,6 @@ public class BaseFmt
dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn)); dicQuery.Add("alpn", Utils.UrlEncode(item.Alpn));
} }
ToUriQueryAllowInsecure(item, ref dicQuery);
return 0;
}
private static int ToUriQueryAllowInsecure(ProfileItem item, ref Dictionary<string, string> dicQuery)
{
if (item.GetAllowInsecure())
{
// Add two for compatibility
dicQuery.Add("insecure", "1");
dicQuery.Add("allowInsecure", "1");
}
else
{
dicQuery.Add("insecure", "0");
dicQuery.Add("allowInsecure", "0");
}
return 0; return 0;
} }
@@ -252,19 +230,6 @@ public class BaseFmt
item.Finalmask = string.Empty; item.Finalmask = string.Empty;
} }
if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1"))
{
item.AllowInsecure = Global.StringTrue;
}
else if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "0"))
{
item.AllowInsecure = Global.StringFalse;
}
else
{
item.AllowInsecure = string.Empty;
}
var net = GetQueryValue(query, "type", nameof(ETransport.raw)); var net = GetQueryValue(query, "type", nameof(ETransport.raw));
if (net == Global.RawNetworkAlias) if (net == Global.RawNetworkAlias)
{ {

View File

@@ -162,6 +162,10 @@ public class Hysteria2Fmt : BaseFmt
private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item) private static void ResolveHy2UriQuery(NameValueCollection query, ref ProfileItem item)
{ {
if (GetQueryValue(query, "insecure") == "1")
{
item.AllowInsecure = Global.StringTrue;
}
if (item.CertSha.IsNullOrEmpty()) if (item.CertSha.IsNullOrEmpty())
{ {
item.CertSha = GetQueryDecoded(query, "pinSHA256"); item.CertSha = GetQueryDecoded(query, "pinSHA256");
@@ -198,6 +202,10 @@ public class Hysteria2Fmt : BaseFmt
private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery) private static void ToHy2UriQuery(ProfileItem item, ref Dictionary<string, string> dicQuery)
{ {
if (item.GetAllowInsecure())
{
dicQuery.Add("insecure", "1");
}
if (!item.CertSha.IsNullOrEmpty() if (!item.CertSha.IsNullOrEmpty()
&& !item.CertSha.Contains(',')) && !item.CertSha.Contains(','))
{ {

View File

@@ -1,6 +1,6 @@
namespace ServiceLib.Handler.Fmt; namespace ServiceLib.Handler.Fmt;
public class InnerFmt public class InnerFmt : BaseFmt
{ {
private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false)); private static readonly Lazy<string> SessionSalt = new(() => Utils.GetGuid(false));
@@ -50,19 +50,19 @@ public class InnerFmt
var protocolExtra = item.GetProtocolExtra(); var protocolExtra = item.GetProtocolExtra();
// Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks, // Only allow "self" as a special value for SubChildItems to avoid possible sources of attacks,
// which means it will be replaced with the subid, otherwise set it to null // which means it will be replaced with the subid, otherwise set it to null
//if (!protocolExtra.SubChildItems.IsNullOrEmpty()) // if (!protocolExtra.SubChildItems.IsNullOrEmpty())
if (protocolExtra.SubChildItems == "self") if (protocolExtra.SubChildItems == "self")
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
SubChildItems = subid SubChildItems = subid,
}; };
} }
else else
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
SubChildItems = null SubChildItems = null,
}; };
} }
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds) if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
@@ -73,14 +73,14 @@ public class InnerFmt
.ToList(); .ToList();
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
ChildItems = Utils.List2String(newChildIndexIds) ChildItems = Utils.List2String(newChildIndexIds),
}; };
} }
else else
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
ChildItems = null ChildItems = null,
}; };
} }
item.SetProtocolExtra(protocolExtra); item.SetProtocolExtra(protocolExtra);
@@ -120,7 +120,7 @@ public class InnerFmt
{ {
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
SubChildItems = "self" SubChildItems = "self",
}; };
} }
if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds) if (Utils.String2List(protocolExtra.ChildItems) is { Count: > 0 } childIndexIds)
@@ -131,7 +131,7 @@ public class InnerFmt
.ToList(); .ToList();
protocolExtra = protocolExtra with protocolExtra = protocolExtra with
{ {
ChildItems = Utils.List2String(newChildIndexIds) ChildItems = Utils.List2String(newChildIndexIds),
}; };
} }
itemClone.SetProtocolExtra(protocolExtra); itemClone.SetProtocolExtra(protocolExtra);
@@ -175,6 +175,19 @@ public class InnerFmt
jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false); jsonObj["TransportExtra"] = JsonUtils.Serialize(transportExtraObj, false);
jsonObj.Remove("TransportExtraObj"); jsonObj.Remove("TransportExtraObj");
} }
var customOutboundFilePath = string.Empty;
if (jsonObj.TryGetPropertyValue("CustomOutboundObj", out var customOutboundNode)
&& customOutboundNode is JsonObject customOutboundObj)
{
var customOutboundContent = JsonUtils.Serialize(customOutboundObj, new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
customOutboundFilePath = WriteAllText(customOutboundContent);
jsonObj.Remove("CustomOutboundObj");
}
var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false)); var profileItem = JsonUtils.Deserialize<ProfileItem>(JsonUtils.Serialize(jsonObj, false));
if (profileItem is null) if (profileItem is null)
{ {
@@ -193,6 +206,14 @@ public class InnerFmt
{ {
return null; return null;
} }
if (profileItem.ConfigType is EConfigType.Outbound)
{
if (customOutboundFilePath.IsNullOrEmpty())
{
return null;
}
profileItem.Address = customOutboundFilePath;
}
var protocolExtra = profileItem.GetProtocolExtra(); var protocolExtra = profileItem.GetProtocolExtra();
var multipleLoad = protocolExtra.MultipleLoad; var multipleLoad = protocolExtra.MultipleLoad;
if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad)) if (multipleLoad is not null && !Enum.IsDefined(typeof(EMultipleLoad), multipleLoad))
@@ -209,6 +230,26 @@ public class InnerFmt
{ {
return null; return null;
} }
if (item.ConfigType is EConfigType.Outbound)
{
var customOutboundFilePath = item.Address;
if (!File.Exists(customOutboundFilePath))
{
customOutboundFilePath = Utils.GetConfigPath(customOutboundFilePath);
}
if (!File.Exists(customOutboundFilePath))
{
return null;
}
if (!customOutboundFilePath.IsNullOrEmpty()
&& File.Exists(customOutboundFilePath)
&& File.ReadAllText(customOutboundFilePath) is { Length: > 0 } customOutboundContent
&& JsonUtils.ParseJson(customOutboundContent) is JsonObject customOutboundObj)
{
jsonObj["CustomOutboundObj"] = customOutboundObj;
jsonObj.Remove("Address");
}
}
// unflatten // unflatten
// move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj // move jsonObj.ProtoExtra (string) to jsonObj.ProtoExtraObj
// move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj // move jsonObj.TransportExtra (string) to jsonObj.TransportExtraObj
@@ -296,7 +337,7 @@ public class InnerFmt
JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str), JsonValue value when value.TryGetValue<string>(out var str) => string.IsNullOrEmpty(str),
JsonObject obj => obj.Count == 0, JsonObject obj => obj.Count == 0,
JsonArray arr => arr.Count == 0, JsonArray arr => arr.Count == 0,
_ => false _ => false,
}; };
} }
} }

View File

@@ -2,19 +2,102 @@ namespace ServiceLib.Handler.Fmt;
public class SingboxFmt : BaseFmt public class SingboxFmt : BaseFmt
{ {
public static List<ProfileItem>? ResolveFullArray(string strData, string? subRemarks) public static List<ProfileItem> ResolveToCustom(string strData, string? subRemarks)
{ {
var configObjects = JsonUtils.Deserialize<object[]>(strData); var jsonNode = JsonUtils.ParseJson(strData);
if (configObjects is not { Length: > 0 }) return ResolveCommon(jsonNode, subRemarks, false);
}
public static List<ProfileItem> ResolveToCustomOutbound(string strData, string? subRemarks)
{
var jsonNode = JsonUtils.ParseJson(strData);
return ResolveCommon(jsonNode, subRemarks, true);
}
private static List<ProfileItem> ResolveCommon(JsonNode? jsonNode, string? subRemarks, bool isOutbound)
{
if (jsonNode is JsonArray jsonArray)
{
return
[
.. jsonArray.Select(item => ResolveCommon(item, subRemarks, isOutbound))
.Where(list => list is { Count: > 0 })
.SelectMany(list => list),
];
}
if (jsonNode is not JsonObject jsonObject)
{
return [];
}
// Process the individual JSON object
var profileList = new List<ProfileItem>();
if (!isOutbound)
{
var fullProfile = ResolveFull(jsonObject, subRemarks);
profileList.Add(fullProfile);
if (fullProfile is not null)
{
return profileList;
}
}
profileList.AddRange(ResolveFullToOutbound(jsonObject, subRemarks));
if (profileList.Count != 0)
{
return profileList;
}
var outboundProfile = ResolveOutbound(jsonObject, subRemarks);
if (outboundProfile is not null)
{
profileList.Add(outboundProfile);
}
return profileList;
}
private static ProfileItem? ResolveFull(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject?["inbounds"] == null
|| jsonObject["outbounds"] == null)
{ {
return null; return null;
} }
List<ProfileItem> lstResult = []; if (jsonObject["outbounds"] is JsonArray outboundsArray)
foreach (var configObject in configObjects)
{ {
var objectString = JsonUtils.Serialize(configObject); if (!outboundsArray.Any(IsValidSingboxOutbound))
var profileIt = ResolveFull(objectString, subRemarks); {
return null;
}
}
else
{
return null;
}
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem
{
CoreType = ECoreType.sing_box,
Address = fileName,
Remarks = subRemarks ?? "singbox_custom",
};
return profileItem;
}
private static List<ProfileItem> ResolveFullToOutbound(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject?["outbounds"] is not JsonArray outboundsArray)
{
return [];
}
List<ProfileItem> lstResult = [];
foreach (var outbound in outboundsArray)
{
if (outbound is not JsonObject outboundObj)
{
continue;
}
var profileIt = ResolveOutbound(outboundObj, subRemarks);
if (profileIt != null) if (profileIt != null)
{ {
lstResult.Add(profileIt); lstResult.Add(profileIt);
@@ -23,25 +106,58 @@ public class SingboxFmt : BaseFmt
return lstResult; return lstResult;
} }
public static ProfileItem? ResolveFull(string strData, string? subRemarks) private static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
{ {
var config = JsonUtils.ParseJson(strData); if (!IsValidSingboxOutbound(jsonObject))
if (config?["inbounds"] == null
|| config["outbounds"] == null
|| config["route"] == null
|| config["dns"] == null)
{ {
return null; return null;
} }
var type = jsonObject["type"]?.ToString();
var fileName = WriteAllText(strData); if (type is null or "direct" or "block" or "dns" or "selector" or "urltest")
{
return null;
}
var tag = jsonObject["tag"]?.ToString();
var remarks = $"{type}_{tag}";
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem var profileItem = new ProfileItem
{ {
ConfigType = EConfigType.Outbound,
CoreType = ECoreType.sing_box, CoreType = ECoreType.sing_box,
Address = fileName, Address = fileName,
Remarks = subRemarks ?? "singbox_custom" Remarks = remarks,
}; };
return profileItem; return profileItem;
} }
private static bool IsValidSingboxOutbound(JsonNode? jsonNode)
{
if (jsonNode is not JsonObject jsonObject)
{
return false;
}
var matchedCounter = 0;
if (string.IsNullOrEmpty(jsonObject["type"]?.ToString()))
{
return false;
}
matchedCounter += 1;
if (!string.IsNullOrEmpty(jsonObject["tag"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["server"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["server_port"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["tls"]?.ToString()))
{
matchedCounter += 1;
}
return matchedCounter >= 2;
}
} }

View File

@@ -2,6 +2,8 @@ namespace ServiceLib.Handler.Fmt;
public class TrojanFmt : BaseFmt public class TrojanFmt : BaseFmt
{ {
private static readonly List<string> _insecureQueryKeys = new() { "allowInsecure", "insecure" };
public static ProfileItem? Resolve(string str, out string msg) public static ProfileItem? Resolve(string str, out string msg)
{ {
msg = ResUI.ConfigurationFormatIncorrect; msg = ResUI.ConfigurationFormatIncorrect;
@@ -23,6 +25,10 @@ public class TrojanFmt : BaseFmt
item.Password = Utils.UrlDecode(url.UserInfo); item.Password = Utils.UrlDecode(url.UserInfo);
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
if (_insecureQueryKeys.Any(q => GetQueryValue(query, q) == "1"))
{
item.AllowInsecure = Global.StringTrue;
}
item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") }); item.SetProtocolExtra(item.GetProtocolExtra() with { Flow = GetQueryValue(query, "flow") });
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
@@ -41,6 +47,10 @@ public class TrojanFmt : BaseFmt
remark = "#" + Utils.UrlEncode(item.Remarks); remark = "#" + Utils.UrlEncode(item.Remarks);
} }
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
if (item.GetAllowInsecure())
{
_insecureQueryKeys.ForEach(q => dicQuery.Add(q, "1"));
}
if (!item.GetProtocolExtra().Flow.IsNullOrEmpty()) if (!item.GetProtocolExtra().Flow.IsNullOrEmpty())
{ {
dicQuery.Add("flow", item.GetProtocolExtra().Flow); dicQuery.Add("flow", item.GetProtocolExtra().Flow);

View File

@@ -30,6 +30,10 @@ public class TuicFmt : BaseFmt
var query = Utils.ParseQueryString(url.Query); var query = Utils.ParseQueryString(url.Query);
ResolveUriQuery(query, ref item); ResolveUriQuery(query, ref item);
if (GetQueryValue(query, "allow_insecure") == "1")
{
item.AllowInsecure = Global.StringTrue;
}
item.SetProtocolExtra(item.GetProtocolExtra() with item.SetProtocolExtra(item.GetProtocolExtra() with
{ {
CongestionControl = GetQueryValue(query, "congestion_control") CongestionControl = GetQueryValue(query, "congestion_control")
@@ -53,7 +57,10 @@ public class TuicFmt : BaseFmt
var dicQuery = new Dictionary<string, string>(); var dicQuery = new Dictionary<string, string>();
ToUriQueryLite(item, ref dicQuery); ToUriQueryLite(item, ref dicQuery);
if (item.GetAllowInsecure())
{
dicQuery.Add("allow_insecure", "1");
}
if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty()) if (!item.GetProtocolExtra().CongestionControl.IsNullOrEmpty())
{ {
dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl); dicQuery.Add("congestion_control", item.GetProtocolExtra().CongestionControl);

View File

@@ -2,47 +2,165 @@ namespace ServiceLib.Handler.Fmt;
public class V2rayFmt : BaseFmt public class V2rayFmt : BaseFmt
{ {
public static List<ProfileItem>? ResolveFullArray(string strData, string? subRemarks) public static List<ProfileItem> ResolveToCustom(string strData, string? subRemarks)
{ {
var configObjects = JsonUtils.Deserialize<object[]>(strData); var jsonNode = JsonUtils.ParseJson(strData);
if (configObjects is not { Length: > 0 }) return ResolveCommon(jsonNode, subRemarks, false);
{
return null;
}
List<ProfileItem> lstResult = [];
foreach (var configObject in configObjects)
{
var objectString = JsonUtils.Serialize(configObject);
var profileIt = ResolveFull(objectString, subRemarks);
if (profileIt != null)
{
lstResult.Add(profileIt);
}
}
return lstResult;
} }
public static ProfileItem? ResolveFull(string strData, string? subRemarks) public static List<ProfileItem> ResolveToCustomOutbound(string strData, string? subRemarks)
{ {
var config = JsonUtils.ParseJson(strData); var jsonNode = JsonUtils.ParseJson(strData);
if (config?["inbounds"] == null return ResolveCommon(jsonNode, subRemarks, true);
|| config["outbounds"] == null }
|| config["routing"] == null)
private static List<ProfileItem> ResolveCommon(JsonNode? jsonNode, string? subRemarks, bool isOutbound)
{
if (jsonNode is JsonArray jsonArray)
{
return
[
.. jsonArray.Select(item => ResolveCommon(item, subRemarks, isOutbound))
.Where(list => list is { Count: > 0 })
.SelectMany(list => list),
];
}
if (jsonNode is not JsonObject jsonObject)
{
return [];
}
// Process the individual JSON object
var profileList = new List<ProfileItem>();
if (!isOutbound)
{
var fullProfile = ResolveFull(jsonObject, subRemarks);
profileList.Add(fullProfile);
if (fullProfile is not null)
{
return profileList;
}
}
profileList.AddRange(ResolveFullToOutbound(jsonObject, subRemarks));
if (profileList.Count != 0)
{
return profileList;
}
var outboundProfile = ResolveOutbound(jsonObject, subRemarks);
if (outboundProfile is not null)
{
profileList.Add(outboundProfile);
}
return profileList;
}
private static ProfileItem? ResolveFull(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject?["inbounds"] == null
|| jsonObject["outbounds"] == null)
{ {
return null; return null;
} }
var fileName = WriteAllText(strData); if (jsonObject["outbounds"] is JsonArray outboundsArray)
{
if (!outboundsArray.Any(IsValidV2rayOutbound))
{
return null;
}
}
else
{
return null;
}
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem var profileItem = new ProfileItem
{ {
CoreType = ECoreType.Xray, CoreType = ECoreType.Xray,
Address = fileName, Address = fileName,
Remarks = config?["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom" Remarks = jsonObject["remarks"]?.ToString() ?? subRemarks ?? "v2ray_custom",
}; };
return profileItem; return profileItem;
} }
public static List<ProfileItem> ResolveFullToOutbound(JsonObject jsonObject, string? subRemarks)
{
if (jsonObject["outbounds"] is not JsonArray outboundsArray)
{
return [];
}
List<ProfileItem> lstResult = [];
foreach (var outbound in outboundsArray)
{
if (outbound is not JsonObject outboundObj)
{
continue;
}
var profileIt = ResolveOutbound(outboundObj, subRemarks);
if (profileIt != null)
{
lstResult.Add(profileIt);
}
}
return lstResult;
}
public static ProfileItem? ResolveOutbound(JsonObject jsonObject, string? subRemarks)
{
if (!IsValidV2rayOutbound(jsonObject))
{
return null;
}
var protocol = jsonObject["protocol"]?.ToString();
if (protocol is null or "freedom" or "blackhole" or "dns" or "loopback")
{
return null;
}
var tag = jsonObject["tag"]?.ToString();
var remarks = $"{protocol}_{tag}";
var fileName = WriteAllText(JsonUtils.Serialize(jsonObject));
var profileItem = new ProfileItem
{
ConfigType = EConfigType.Outbound,
CoreType = ECoreType.Xray,
Address = fileName,
Remarks = remarks,
};
return profileItem;
}
private static bool IsValidV2rayOutbound(JsonNode? jsonNode)
{
if (jsonNode is not JsonObject jsonObject)
{
return false;
}
var matchedCounter = 0;
if (string.IsNullOrEmpty(jsonObject["protocol"]?.ToString()))
{
return false;
}
matchedCounter += 1;
if (!string.IsNullOrEmpty(jsonObject["settings"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["streamSettings"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["tag"]?.ToString()))
{
matchedCounter += 1;
}
if (!string.IsNullOrEmpty(jsonObject["mux"]?.ToString()))
{
matchedCounter += 1;
}
return matchedCounter >= 3;
}
} }

View File

@@ -91,6 +91,8 @@ public class CoreAdminManager
.ExecuteBufferedAsync(); .ExecuteBufferedAsync();
await UpdateFunc(false, result.StandardOutput.ToString()); await UpdateFunc(false, result.StandardOutput.ToString());
await Task.Delay(1000); // Wait for a second to ensure the process is killed
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -85,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();
@@ -183,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;
@@ -201,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;
@@ -222,7 +222,7 @@ public class CoreManager
{ {
return; return;
} }
if (!preContext.AppConfig.TunModeItem.EnableTun) if (!preContext.IsTunEnabled)
{ {
return; return;
} }
@@ -289,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())
@@ -301,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);

View File

@@ -118,7 +118,7 @@ public class GroupProfileManager
return childProfiles?.Where(p => return childProfiles?.Where(p =>
p != null && p != null &&
p.IsValid() && p.IsValid() &&
!p.ConfigType.IsComplexType() && (!p.ConfigType.IsComplexType() || p.ConfigType == EConfigType.Outbound) &&
(extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter)) (extra.Filter.IsNullOrEmpty() || Regex.IsMatch(p.Remarks, extra.Filter))
) )
.ToList() ?? []; .ToList() ?? [];

View File

@@ -1,5 +1,3 @@
//using System.Reactive.Linq;
namespace ServiceLib.Manager; namespace ServiceLib.Manager;
public class ProfileExManager public class ProfileExManager

View File

@@ -147,10 +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 IPv4Address { get; set; }
public string Ipv6Address { get; set; } public string IPv6Address { get; set; }
} }
[Serializable] [Serializable]

View File

@@ -11,6 +11,8 @@ public record CoreConfigContext
public Config AppConfig { get; init; } = new(); public Config AppConfig { get; init; } = new();
public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new(); public FullConfigTemplateItem? FullConfigTemplate { get; init; } = new();
public Dictionary<string, string> CustomOutboundContent { get; init; } = new();
// Test ServerTestItem Map // Test ServerTestItem Map
public Dictionary<string, string> ServerTestItemMap { get; init; } = new(); public Dictionary<string, string> ServerTestItemMap { get; init; } = new();
@@ -22,4 +24,7 @@ public record CoreConfigContext
public bool IsWindows { get; init; } public bool IsWindows { get; init; }
public bool IsMacOS { get; init; } public bool IsMacOS { get; init; }
// Generation Context
public Dictionary<object, string> CustomOutboundMap { get; init; } = new();
} }

View File

@@ -364,8 +364,6 @@ public class StreamSettings4Ray
public class TlsSettings4Ray public class TlsSettings4Ray
{ {
public bool? allowInsecure { get; set; }
public string? serverName { get; set; } public string? serverName { get; set; }
public List<string>? alpn { get; set; } public List<string>? alpn { get; set; }

View File

@@ -1,10 +1,10 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
public class CheckUpdateModel : ReactiveObject public partial class CheckUpdateModel : ReactiveObject
{ {
public bool? IsSelected { get; set; } public bool? IsSelected { get; set; }
public ECoreType? CoreType { get; set; } public ECoreType? CoreType { get; set; }
[Reactive] public string? Remarks { get; set; } [Reactive] public partial string? Remarks { get; set; }
public string? FileName { get; set; } public string? FileName { get; set; }
public bool? IsFinished { get; set; } public bool? IsFinished { get; set; }
public bool IsGeoFile { get; set; } public bool IsGeoFile { get; set; }

View File

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

View File

@@ -1,7 +1,7 @@
namespace ServiceLib.Models.Dto; namespace ServiceLib.Models.Dto;
[Serializable] [Serializable]
public class ProfileItemModel : ReactiveObject public partial class ProfileItemModel : ReactiveObject
{ {
public bool IsActive { get; set; } public bool IsActive { get; set; }
public string IndexId { get; set; } public string IndexId { get; set; }
@@ -16,30 +16,30 @@ public class ProfileItemModel : ReactiveObject
public int Sort { get; set; } public int Sort { get; set; }
[Reactive] [Reactive]
public int Delay { get; set; } public partial int Delay { get; set; }
public decimal Speed { get; set; } public decimal Speed { get; set; }
[Reactive] [Reactive]
public string DelayVal { get; set; } public partial string DelayVal { get; set; }
[Reactive] [Reactive]
public string SpeedVal { get; set; } public partial string SpeedVal { get; set; }
[Reactive] [Reactive]
public string IpInfo { get; set; } public partial string IpInfo { get; set; }
[Reactive] [Reactive]
public string TodayUp { get; set; } public partial string TodayUp { get; set; }
[Reactive] [Reactive]
public string TodayDown { get; set; } public partial string TodayDown { get; set; }
[Reactive] [Reactive]
public string TotalUp { get; set; } public partial string TotalUp { get; set; }
[Reactive] [Reactive]
public string TotalDown { get; set; } public partial string TotalDown { get; set; }
public string GetSummary() public string GetSummary()
{ {

View File

@@ -66,7 +66,7 @@ public class ProfileItem
public bool IsValid() public bool IsValid()
{ {
if (IsComplex()) if (IsComplex() || ConfigType == EConfigType.Outbound)
{ {
return true; return true;
} }

View File

@@ -51,4 +51,7 @@ public record ProtocolExtraItem
public string? SubChildItems { get; init; } public string? SubChildItems { get; init; }
public string? Filter { get; init; } public string? Filter { get; init; }
public EMultipleLoad? MultipleLoad { get; init; } public EMultipleLoad? MultipleLoad { get; init; }
// custom outbound
public bool? IsSingboxEndpoint { get; init; }
} }

View File

@@ -33,4 +33,6 @@ public class SubItem
public int? PreSocksPort { get; set; } public int? PreSocksPort { get; set; }
public string? Memo { get; set; } public string? Memo { get; set; }
public ECoreType? CustomCoreType { get; set; }
} }

View File

@@ -438,6 +438,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom config core 的本地化字符串。
/// </summary>
public static string LvCustomCoreType {
get {
return ResourceManager.GetString("LvCustomCoreType", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Custom icon 的本地化字符串。 /// 查找类似 Custom icon 的本地化字符串。
/// </summary> /// </summary>
@@ -735,6 +744,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Add a custom outbound 的本地化字符串。
/// </summary>
public static string menuAddCustomOutboundServer {
get {
return ResourceManager.GetString("menuAddCustomOutboundServer", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Add a custom configuration 的本地化字符串。 /// 查找类似 Add a custom configuration 的本地化字符串。
/// </summary> /// </summary>
@@ -1933,7 +1951,7 @@ namespace ServiceLib.Resx {
} }
/// <summary> /// <summary>
/// 查找类似 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. 的本地化字符串。 /// 查找类似 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 c [字符串的其余部分被截断]&quot;; 的本地化字符串。
/// </summary> /// </summary>
public static string MsgAllowInsecureDeprecated { public static string MsgAllowInsecureDeprecated {
get { get {
@@ -1977,6 +1995,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Custom outbound {0} file not found: {1} 的本地化字符串。
/// </summary>
public static string MsgCustomOutboundFileNotFound {
get {
return ResourceManager.GetString("MsgCustomOutboundFileNotFound", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。 /// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。
/// </summary> /// </summary>
@@ -2925,6 +2952,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Only single outbound/endpoint supported for xray/sing-box 的本地化字符串。
/// </summary>
public static string TbCustomOutboundTip {
get {
return ResourceManager.GetString("TbCustomOutboundTip", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Direct Target Resolution Strategy 的本地化字符串。 /// 查找类似 Direct Target Resolution Strategy 的本地化字符串。
/// </summary> /// </summary>

View File

@@ -1780,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>
@@ -1848,4 +1848,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="TbIpv4Address" xml:space="preserve"> <data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 Address</value> <value>Ipv4 Address</value>
</data> </data>
<data name="menuAddCustomOutboundServer" xml:space="preserve">
<value>Add a custom outbound</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>Custom outbound {0} file not found: {1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>Only single outbound/endpoint supported for xray/sing-box</value>
</data>
<data name="LvCustomCoreType" xml:space="preserve">
<value>Custom config core</value>
</data>
</root> </root>

View File

@@ -1774,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>
@@ -1845,4 +1849,16 @@
<data name="TbIpv4Address" xml:space="preserve"> <data name="TbIpv4Address" xml:space="preserve">
<value>Ipv4 地址</value> <value>Ipv4 地址</value>
</data> </data>
<data name="menuAddCustomOutboundServer" xml:space="preserve">
<value>添加自定义出站</value>
</data>
<data name="MsgCustomOutboundFileNotFound" xml:space="preserve">
<value>自定义出站 {0} 的文件未找到:{1}</value>
</data>
<data name="TbCustomOutboundTip" xml:space="preserve">
<value>仅支持 xray/sing-box 的单个 outbound/endpoin</value>
</data>
<data name="LvCustomCoreType" xml:space="preserve">
<value>自定义配置核心</value>
</data>
</root> </root>

View File

@@ -1774,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>

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"
} ]
} }
}

View File

@@ -66,6 +66,11 @@ kill_children "$PID"
echo "Terminating main process: $PID" echo "Terminating main process: $PID"
kill -9 "$PID" 2>/dev/null || true kill -9 "$PID" 2>/dev/null || true
# Wait a little for process/port resources to be fully released
FINAL_WAIT_SECONDS=1
echo "Waiting ${FINAL_WAIT_SECONDS}s for resources to settle..."
sleep "$FINAL_WAIT_SECONDS"
echo "============================================" echo "============================================"
echo "Process $PID and all its children have been terminated" echo "Process $PID and all its children have been terminated"
echo "============================================" echo "============================================"

View File

@@ -63,6 +63,11 @@ kill_descendants "$PID"
echo "Terminating main process: $PID" echo "Terminating main process: $PID"
kill -9 "$PID" 2>/dev/null || true kill -9 "$PID" 2>/dev/null || true
# Wait a little for process/port resources to be fully released
FINAL_WAIT_SECONDS=1
echo "Waiting ${FINAL_WAIT_SECONDS}s for resources to settle..."
sleep "$FINAL_WAIT_SECONDS"
echo "============================================" echo "============================================"
echo "Process $PID and all its descendants have been terminated" echo "Process $PID and all its descendants have been terminated"
echo "============================================" echo "============================================"

View File

@@ -1,6 +1,8 @@
[ [
{ {
"network": "udp", "network": [
"udp"
],
"port": [ "port": [
135, 135,
137, 137,

View File

@@ -10,7 +10,10 @@
<PackageReference Include="ReactiveUI"> <PackageReference Include="ReactiveUI">
<TreatAsUsed>true</TreatAsUsed> <TreatAsUsed>true</TreatAsUsed>
</PackageReference> </PackageReference>
<PackageReference Include="ReactiveUI.Fody" /> <PackageReference Include="ReactiveUI.SourceGenerators">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="sqlite-net-e" /> <PackageReference Include="sqlite-net-e" />
<PackageReference Include="Repobot.SQLite.Unofficial" /> <PackageReference Include="Repobot.SQLite.Unofficial" />
<PackageReference Include="NLog" /> <PackageReference Include="NLog" />

View File

@@ -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;
} }

View File

@@ -57,13 +57,10 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ConvertGeo2Ruleset(); ConvertGeo2Ruleset();
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = ApplyFullConfigTemplate(); ret.Data = ApplyFinalConfigModifiers();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -107,7 +104,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType())) if (!(Global.SingboxSupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType() || it.ConfigType is EConfigType.Outbound))
{ {
continue; continue;
} }
@@ -174,7 +171,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ApplyOutboundBindInterface(); ApplyOutboundBindInterface();
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -236,7 +233,7 @@ public partial class CoreConfigSingboxService(CoreConfigContext context)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -2,54 +2,137 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigSingboxService public partial class CoreConfigSingboxService
{ {
private string ApplyFullConfigTemplate() private string ApplyFinalConfigModifiers()
{
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var coreConfigContent = ApplyCustomOutboundReplace();
return ApplyFullConfigTemplate(coreConfigContent);
}
private string ApplyCustomOutboundReplace()
{
var coreConfigContent = JsonUtils.Serialize(_coreConfig);
if (context.CustomOutboundMap.Count == 0)
{
return coreConfigContent;
}
var coreConfigNode = JsonNode.Parse(coreConfigContent) as JsonObject;
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
ReplaceCustomOutbounds(_coreConfig.outbounds, coreConfigOutboundsNode);
coreConfigNode!["outbounds"] = coreConfigOutboundsNode;
var coreConfigEndpointsNode = coreConfigNode?["endpoints"] as JsonArray ?? [];
ReplaceCustomOutbounds(_coreConfig.endpoints, coreConfigEndpointsNode);
if (coreConfigEndpointsNode.Count > 0)
{
coreConfigNode!["endpoints"] = coreConfigEndpointsNode;
}
else
{
coreConfigNode?.Remove("endpoints");
}
return JsonUtils.Serialize(coreConfigNode);
void ReplaceCustomOutbounds(IReadOnlyList<BaseServer4Sbox>? source, JsonArray jsonArrayOutbounds)
{
foreach (var outbound in source ?? [])
{
if (!context.CustomOutboundMap.TryGetValue(outbound, out var customOutboundIndex))
{
continue;
}
var outboundTag = outbound.tag;
var outboundDetour = outbound.detour ?? string.Empty;
var outboundBindInterface = outbound.bind_interface ?? string.Empty;
var customOutboundContent = context.CustomOutboundContent[customOutboundIndex];
var containTagPlaceholder = customOutboundContent.Contains("{{tag}}");
var containDetourPlaceholder = customOutboundContent.Contains("{{detour}}");
var containBindInterfacePlaceholder = customOutboundContent.Contains("{{interface}}");
customOutboundContent = customOutboundContent.Replace("{{tag}}", outboundTag);
customOutboundContent = customOutboundContent.Replace("{{detour}}", outboundDetour);
customOutboundContent = customOutboundContent.Replace("{{interface}}", outboundBindInterface);
var customOutboundObj = JsonUtils.ParseJson(customOutboundContent) as JsonObject;
if (!containTagPlaceholder)
{
customOutboundObj?["tag"] = outboundTag;
}
if (!containDetourPlaceholder && !outboundDetour.IsNullOrEmpty())
{
customOutboundObj?["detour"] = outboundDetour;
}
else if (outboundDetour.IsNullOrEmpty())
{
customOutboundObj?.Remove("detour");
}
if (!containBindInterfacePlaceholder && !outboundBindInterface.IsNullOrEmpty())
{
customOutboundObj?["bind_interface"] = outboundBindInterface;
}
var index = jsonArrayOutbounds
.Select((node, idx) => new { node, idx })
.FirstOrDefault(x => x.node?["tag"]?.ToString() == outboundTag)?.idx ?? -1;
if (index != -1)
{
jsonArrayOutbounds[index] = customOutboundObj;
}
}
}
}
private string ApplyFullConfigTemplate(string coreConfigContent)
{ {
var fullConfigTemplate = context.FullConfigTemplate; var fullConfigTemplate = context.FullConfigTemplate;
if (fullConfigTemplate is not { Enabled: true }) if (fullConfigTemplate is not { Enabled: true })
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
if (fullConfigTemplateItem.IsNullOrEmpty()) if (fullConfigTemplateItem.IsNullOrEmpty())
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null) if (fullConfigTemplateNode == null)
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
// Process outbounds // Process outbounds
var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? []; var customOutboundsNode = fullConfigTemplateNode["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds) var coreConfigNode = JsonNode.Parse(coreConfigContent);
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
foreach (var outbound in coreConfigOutboundsNode)
{ {
if (outbound.type.ToLower() is "direct" or "block") if (outbound["type"]?.ToString()?.ToLower() is "direct" or "block")
{ {
if (fullConfigTemplate.AddProxyOnly == true) if (fullConfigTemplate.AddProxyOnly == true)
{ {
continue; continue;
} }
} }
else if (outbound.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound.server ?? string.Empty)) if (outbound["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty() && !Utils.IsPrivateNetwork(outbound["server"]?.ToString() ?? string.Empty))
{ {
outbound.detour = fullConfigTemplate.ProxyDetour; outbound["detour"] = fullConfigTemplate.ProxyDetour;
} }
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));
} }
fullConfigTemplateNode["outbounds"] = customOutboundsNode; fullConfigTemplateNode["outbounds"] = customOutboundsNode;
// Process endpoints // Process endpoints
if (_coreConfig.endpoints is { Count: > 0 }) if (fullConfigTemplateNode["endpoints"] is JsonArray { Count: > 0 } coreConfigEndpointsNode)
{ {
var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? []; var customEndpointsNode = fullConfigTemplateNode["endpoints"] as JsonArray ?? [];
foreach (var endpoint in _coreConfig.endpoints) foreach (var endpoint in coreConfigEndpointsNode)
{ {
if (endpoint.detour.IsNullOrEmpty() && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty()) if (endpoint["detour"] is null && !fullConfigTemplate.ProxyDetour.IsNullOrEmpty())
{ {
endpoint.detour = fullConfigTemplate.ProxyDetour; endpoint["detour"] = fullConfigTemplate.ProxyDetour;
} }
customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint)); customEndpointsNode.Add(JsonUtils.DeepCopy(endpoint));
} }

View File

@@ -68,11 +68,11 @@ public partial class CoreConfigSingboxService
tunInbound.strict_route = _config.TunModeItem.StrictRoute; tunInbound.strict_route = _config.TunModeItem.StrictRoute;
tunInbound.stack = _config.TunModeItem.Stack; tunInbound.stack = _config.TunModeItem.Stack;
var address = _config.TunModeItem.Ipv4Address.NullIfEmpty() ?? Global.TunIpv4Address.First(); var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.address = [address]; tunInbound.address = [address];
if (_config.TunModeItem.EnableIPv6Address == true) if (_config.TunModeItem.EnableIPv6Address == true)
{ {
var address6 = _config.TunModeItem.Ipv6Address.NullIfEmpty() ?? Global.TunIpv6Address.First(); var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.address.Add(address6); tunInbound.address.Add(address6);
} }
tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress; tunInbound.route_exclude_address = _config.TunModeItem.RouteExcludeAddress;

View File

@@ -11,7 +11,7 @@ public partial class CoreConfigSingboxService
private List<BaseServer4Sbox> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag, bool withSelector = true) private List<BaseServer4Sbox> BuildAllProxyOutbounds(string baseTagName = Global.ProxyTag, bool withSelector = true)
{ {
var proxyOutboundList = new List<BaseServer4Sbox>(); var proxyOutboundList = new List<BaseServer4Sbox>();
if (!_node.ConfigType.IsComplexType()) if (!_node.ConfigType.IsGroupType())
{ {
var outbound = BuildProxyOutbound(baseTagName); var outbound = BuildProxyOutbound(baseTagName);
proxyOutboundList.Add(outbound); proxyOutboundList.Add(outbound);
@@ -35,6 +35,10 @@ public partial class CoreConfigSingboxService
{ {
var outbound = BuildProxyServer(); var outbound = BuildProxyServer();
outbound.tag = baseTagName; outbound.tag = baseTagName;
if (_node.ConfigType == EConfigType.Outbound)
{
context.CustomOutboundMap[outbound] = _node.IndexId;
}
return outbound; return outbound;
} }
@@ -59,6 +63,20 @@ public partial class CoreConfigSingboxService
try try
{ {
var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound); var txtOutbound = EmbedUtils.GetEmbedText(Global.SingboxSampleOutbound);
if (_node.ConfigType == EConfigType.Outbound)
{
if (_node.GetProtocolExtra().IsSingboxEndpoint == true)
{
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
return endpoint;
}
else
{
var outbound = JsonUtils.Deserialize<Outbound4Sbox>(txtOutbound);
return outbound;
}
}
if (_node.ConfigType == EConfigType.WireGuard) if (_node.ConfigType == EConfigType.WireGuard)
{ {
var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound); var endpoint = JsonUtils.Deserialize<Endpoints4Sbox>(txtOutbound);
@@ -593,7 +611,7 @@ public partial class CoreConfigSingboxService
{ {
type = "selector", type = "selector",
tag = baseTagName, tag = baseTagName,
outbounds = JsonUtils.DeepCopy(proxyTags), outbounds = [.. proxyTags],
interrupt_exist_connections = false, interrupt_exist_connections = false,
}; };
outSelector.outbounds.Insert(0, outUrltest.tag); outSelector.outbounds.Insert(0, outUrltest.tag);
@@ -721,9 +739,9 @@ public partial class CoreConfigSingboxService
return resultOutbounds; return resultOutbounds;
} }
private static List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source) private List<BaseServer4Sbox> CloneOutbounds(List<BaseServer4Sbox> source)
{ {
if (source is null || source.Count == 0) if (source is not { Count: > 0 })
{ {
return []; return [];
} }
@@ -740,9 +758,14 @@ public partial class CoreConfigSingboxService
{ {
clone = JsonUtils.DeepCopy(endpoint); clone = JsonUtils.DeepCopy(endpoint);
} }
if (clone is not null) if (clone is null)
{ {
result.Add(clone); continue;
}
result.Add(clone);
if (context.CustomOutboundMap.ContainsKey(item))
{
context.CustomOutboundMap[clone] = context.CustomOutboundMap[item];
} }
} }
return result; return result;

View File

@@ -43,6 +43,22 @@ public partial class CoreConfigSingboxService
_coreConfig.route.rules.AddRange(tunRules); _coreConfig.route.rules.AddRange(tunRules);
} }
// Traffic addressed to the TUN interface's own addresses must never reach an
// outbound. auto_route hijacks the default route, so `direct` writes such a
// packet straight back into the TUN, which hands it to the outbound again -
// an infinite loop that pins a CPU core. Drop instead of rejecting so no
// ICMP unreachable is generated back towards the same addresses.
var tunAddresses = _coreConfig.inbounds.FirstOrDefault(i => i.type == "tun")?.address;
if (tunAddresses?.Count > 0)
{
_coreConfig.route.rules.Add(new()
{
ip_cidr = [.. tunAddresses],
action = "reject",
method = "drop",
});
}
var lstDirectExe = BuildRoutingDirectExe(); var lstDirectExe = BuildRoutingDirectExe();
if (lstDirectExe.Count > 0) if (lstDirectExe.Count > 0)
{ {
@@ -558,7 +574,8 @@ public partial class CoreConfigSingboxService
if (node == null if (node == null
|| (!Global.SingboxSupportConfigType.Contains(node.ConfigType) || (!Global.SingboxSupportConfigType.Contains(node.ConfigType)
&& !node.ConfigType.IsGroupType())) && !node.ConfigType.IsGroupType()
&& node.ConfigType is not EConfigType.Outbound))
{ {
return Global.ProxyTag; return Global.ProxyTag;
} }

View File

@@ -64,8 +64,6 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
{ {
ApplyFinalFragment(); ApplyFinalFragment();
} }
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var finalRule = BuildFinalRule(); var finalRule = BuildFinalRule();
if (!string.IsNullOrEmpty(finalRule?.balancerTag)) if (!string.IsNullOrEmpty(finalRule?.balancerTag))
@@ -75,7 +73,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = ApplyFullConfigTemplate(); ret.Data = ApplyFinalConfigModifiers();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -119,7 +117,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
foreach (var it in selecteds) foreach (var it in selecteds)
{ {
if (!(Global.XraySupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType())) if (!(Global.XraySupportConfigType.Contains(it.ConfigType) || it.ConfigType.IsGroupType() || it.ConfigType is EConfigType.Outbound))
{ {
continue; continue;
} }
@@ -216,7 +214,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
ApplyOutboundSendThrough(); ApplyOutboundSendThrough();
//ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary()); //ret.Msg =string.Format(ResUI.SuccessfulConfiguration"), node.getSummary());
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)
@@ -293,7 +291,7 @@ public partial class CoreConfigV2rayService(CoreConfigContext context)
ret.Msg = string.Format(ResUI.SuccessfulConfiguration, ""); ret.Msg = string.Format(ResUI.SuccessfulConfiguration, "");
ret.Success = true; ret.Success = true;
ret.Data = JsonUtils.Serialize(_coreConfig); ret.Data = ApplyCustomOutboundReplace();
return ret; return ret;
} }
catch (Exception ex) catch (Exception ex)

View File

@@ -2,24 +2,105 @@ namespace ServiceLib.Services.CoreConfig;
public partial class CoreConfigV2rayService public partial class CoreConfigV2rayService
{ {
private string ApplyFullConfigTemplate() private string ApplyFinalConfigModifiers()
{
ApplyOutboundBindInterface();
ApplyOutboundSendThrough();
var coreConfigContent = ApplyCustomOutboundReplace();
return ApplyFullConfigTemplate(coreConfigContent);
}
private string ApplyCustomOutboundReplace()
{
var coreConfigContent = JsonUtils.Serialize(_coreConfig);
if (context.CustomOutboundMap.Count == 0)
{
return coreConfigContent;
}
var coreConfigNode = JsonNode.Parse(coreConfigContent) as JsonObject;
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
foreach (var outbound in _coreConfig.outbounds ?? [])
{
if (!context.CustomOutboundMap.TryGetValue(outbound, out var customOutboundIndex))
{
continue;
}
var outboundTag = outbound.tag;
var outboundDetour = outbound.streamSettings?.sockopt?.dialerProxy ?? string.Empty;
var outboundBindInterface = outbound.streamSettings?.sockopt?.Interface ?? string.Empty;
var customOutboundContent = context.CustomOutboundContent[customOutboundIndex];
var containTagPlaceholder = customOutboundContent.Contains("{{tag}}");
var containDetourPlaceholder = customOutboundContent.Contains("{{detour}}");
var containBindInterfacePlaceholder = customOutboundContent.Contains("{{interface}}");
customOutboundContent = customOutboundContent.Replace("{{tag}}", outboundTag);
customOutboundContent = customOutboundContent.Replace("{{detour}}", outboundDetour);
customOutboundContent = customOutboundContent.Replace("{{interface}}", outboundBindInterface);
var customOutboundObj = JsonUtils.ParseJson(customOutboundContent) as JsonObject;
if (!containTagPlaceholder)
{
customOutboundObj?["tag"] = outboundTag;
}
if (!containDetourPlaceholder && !outboundDetour.IsNullOrEmpty())
{
customOutboundObj!["streamSettings"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"]["dialerProxy"] = outboundDetour;
if (customOutboundObj["streamSettings"]?["xhttpSettings"]?["extra"]?["downloadSettings"] is JsonObject downloadSettings)
{
downloadSettings["sockopt"] ??= new JsonObject();
downloadSettings["sockopt"]["dialerProxy"] = outboundDetour;
}
}
else if (outboundDetour.IsNullOrEmpty())
{
(customOutboundObj?["streamSettings"]?["sockopt"] as JsonObject)?.Remove("dialerProxy");
}
if (!containBindInterfacePlaceholder && !outboundBindInterface.IsNullOrEmpty())
{
customOutboundObj!["streamSettings"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"] ??= new JsonObject();
customOutboundObj["streamSettings"]["sockopt"]["interface"] = outboundBindInterface;
if (customOutboundObj["streamSettings"]?["xhttpSettings"]?["extra"]?["downloadSettings"] is JsonObject downloadSettings)
{
downloadSettings["sockopt"] ??= new JsonObject();
downloadSettings["sockopt"]["interface"] = outboundBindInterface;
}
}
var index = coreConfigOutboundsNode
.Select((node, idx) => new { node, idx })
.FirstOrDefault(x => x.node?["tag"]?.ToString() == outboundTag)?.idx ?? -1;
if (index != -1)
{
coreConfigOutboundsNode[index] = customOutboundObj;
}
}
return JsonUtils.Serialize(coreConfigNode);
}
private string ApplyFullConfigTemplate(string coreConfigContent)
{ {
var fullConfigTemplate = context.FullConfigTemplate; var fullConfigTemplate = context.FullConfigTemplate;
if (fullConfigTemplate is not { Enabled: true }) if (fullConfigTemplate is not { Enabled: true })
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config; var fullConfigTemplateItem = context.IsTunEnabled ? fullConfigTemplate.TunConfig : fullConfigTemplate.Config;
if (fullConfigTemplateItem.IsNullOrEmpty()) if (fullConfigTemplateItem.IsNullOrEmpty())
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem); var fullConfigTemplateNode = JsonNode.Parse(fullConfigTemplateItem);
if (fullConfigTemplateNode == null) if (fullConfigTemplateNode == null)
{ {
return JsonUtils.Serialize(_coreConfig); return coreConfigContent;
} }
// Handle balancer and rules modifications (for multiple load scenarios) // Handle balancer and rules modifications (for multiple load scenarios)
@@ -74,8 +155,8 @@ public partial class CoreConfigV2rayService
else else
{ {
var subjectSelector = _coreConfig.observatory.subjectSelector; var subjectSelector = _coreConfig.observatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []); subjectSelector?.AddRange(fullConfigTemplateNode["observatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["observatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); fullConfigTemplateNode["observatory"]?["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector?.Distinct().ToList()));
} }
} }
@@ -88,16 +169,18 @@ public partial class CoreConfigV2rayService
else else
{ {
var subjectSelector = _coreConfig.burstObservatory.subjectSelector; var subjectSelector = _coreConfig.burstObservatory.subjectSelector;
subjectSelector.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []); subjectSelector?.AddRange(fullConfigTemplateNode["burstObservatory"]?["subjectSelector"]?.AsArray()?.Select(x => x?.GetValue<string>()) ?? []);
fullConfigTemplateNode["burstObservatory"]["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector.Distinct().ToList())); fullConfigTemplateNode["burstObservatory"]?["subjectSelector"] = JsonNode.Parse(JsonUtils.Serialize(subjectSelector?.Distinct().ToList()));
} }
} }
var customOutboundsNode = new JsonArray(); var customOutboundsNode = new JsonArray();
foreach (var outbound in _coreConfig.outbounds) var coreConfigNode = JsonNode.Parse(coreConfigContent);
var coreConfigOutboundsNode = coreConfigNode?["outbounds"] as JsonArray ?? [];
foreach (var outbound in coreConfigOutboundsNode)
{ {
if (outbound.protocol.ToLower() is "blackhole" or "dns" or "freedom") if (outbound?["protocol"]?.ToString()?.ToLower() is "blackhole" or "dns" or "freedom")
{ {
if (fullConfigTemplate.AddProxyOnly == true) if (fullConfigTemplate.AddProxyOnly == true)
{ {
@@ -105,14 +188,22 @@ public partial class CoreConfigV2rayService
} }
} }
else if (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty() else if (!fullConfigTemplate.ProxyDetour.IsNullOrEmpty()
&& (outbound.streamSettings?.sockopt?.dialerProxy.IsNullOrEmpty() ?? true)) && (outbound["streamSettings"]?["sockopt"]?["dialerProxy"].ToString().IsNullOrEmpty() ?? true))
{ {
var outboundAddress = outbound.settings?.servers?.FirstOrDefault()?.address var outboundAddress = outbound["settings"]?["servers"]?.AsArray()?.FirstOrDefault()?["address"]?.ToString()
?? outbound.settings?.vnext?.FirstOrDefault()?.address ?? outbound["settings"]?["vnext"]?.AsArray()?.FirstOrDefault()?["address"]?.ToString()
?? string.Empty; ?? string.Empty;
if (!Utils.IsPrivateNetwork(outboundAddress)) if (!Utils.IsPrivateNetwork(outboundAddress))
{ {
FillDialerProxy(outbound, fullConfigTemplate.ProxyDetour); //FillDialerProxy(outbound, fullConfigTemplate.ProxyDetour);
outbound["streamSettings"] ??= new JsonObject();
outbound["streamSettings"]["sockopt"] ??= new JsonObject();
outbound["streamSettings"]["sockopt"]["dialerProxy"] = fullConfigTemplate.ProxyDetour;
if (outbound["streamSettings"]?["xhttpSettings"]?["extra"]?["downloadSettings"] is JsonObject downloadSettings)
{
downloadSettings["sockopt"] ??= new JsonObject();
downloadSettings["sockopt"]["dialerProxy"] = fullConfigTemplate.ProxyDetour;
}
} }
} }
customOutboundsNode.Add(JsonUtils.DeepCopy(outbound)); customOutboundsNode.Add(JsonUtils.DeepCopy(outbound));

View File

@@ -64,15 +64,16 @@ public partial class CoreConfigV2rayService
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;
var address = _config.TunModeItem.Ipv4Address.NullIfEmpty() ?? Global.TunIpv4Address.First(); var address = _config.TunModeItem.IPv4Address.NullIfEmpty() ?? Global.TunIPv4Address.First();
tunInbound.settings.gateway = [address]; tunInbound.settings.gateway = [address];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
if (_config.TunModeItem.EnableIPv6Address == true) if (_config.TunModeItem.EnableIPv6Address == true)
{ {
var address6 = _config.TunModeItem.Ipv6Address.NullIfEmpty() ?? Global.TunIpv6Address.First(); var address6 = _config.TunModeItem.IPv6Address.NullIfEmpty() ?? Global.TunIPv6Address.First();
tunInbound.settings.gateway.Add(address6); tunInbound.settings.gateway.Add(address6);
tunInbound.settings.autoSystemRoutingTable.Add("::/0");
} }
tunInbound.settings.dns = [address.Split('/').First()];
tunInbound.settings.autoSystemRoutingTable = ["0.0.0.0/0"];
var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx(); var bindInterface = _config.CoreBasicItem.BindInterface?.TrimEx();
if (!bindInterface.IsNullOrEmpty()) if (!bindInterface.IsNullOrEmpty())
{ {

View File

@@ -52,6 +52,12 @@ public partial class CoreConfigV2rayService
{ {
var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound); var txtOutbound = EmbedUtils.GetEmbedText(Global.V2raySampleOutbound);
var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound); var outbound = JsonUtils.Deserialize<Outbounds4Ray>(txtOutbound);
if (_node.ConfigType == EConfigType.Outbound)
{
outbound.tag = baseTagName;
context.CustomOutboundMap[outbound] = _node.IndexId;
return outbound;
}
FillOutbound(outbound); FillOutbound(outbound);
outbound.tag = baseTagName; outbound.tag = baseTagName;
return outbound; return outbound;
@@ -404,7 +410,6 @@ public partial class CoreConfigV2rayService
TlsSettings4Ray tlsSettings = new() TlsSettings4Ray tlsSettings = new()
{ {
allowInsecure = _node.GetAllowInsecure(),
alpn = _node.GetAlpn(), alpn = _node.GetAlpn(),
fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint, fingerprint = _node.Fingerprint.IsNullOrEmpty() ? _config.CoreBasicItem.DefFingerprint : _node.Fingerprint,
echConfigList = _node.EchConfigList.NullIfEmpty(), echConfigList = _node.EchConfigList.NullIfEmpty(),
@@ -438,12 +443,10 @@ public partial class CoreConfigV2rayService
} }
tlsSettings.certificates = certsettings; tlsSettings.certificates = certsettings;
tlsSettings.disableSystemRoot = true; tlsSettings.disableSystemRoot = true;
tlsSettings.allowInsecure = false;
} }
else if (!_node.CertSha.IsNullOrEmpty()) else if (!_node.CertSha.IsNullOrEmpty())
{ {
tlsSettings.pinnedPeerCertSha256 = _node.CertSha; tlsSettings.pinnedPeerCertSha256 = _node.CertSha;
tlsSettings.allowInsecure = false;
} }
streamSettings.tlsSettings = tlsSettings; streamSettings.tlsSettings = tlsSettings;
} }
@@ -788,12 +791,12 @@ public partial class CoreConfigV2rayService
} }
else if (chainStartNodes.Count > 1) else if (chainStartNodes.Count > 1)
{ {
var existedChainNodes = JsonUtils.DeepCopy(resultOutbounds); var existedChainNodes = CloneOutbounds(resultOutbounds);
resultOutbounds.Clear(); resultOutbounds.Clear();
var j = 0; var j = 0;
foreach (var chainStartNode in chainStartNodes) foreach (var chainStartNode in chainStartNodes)
{ {
var existedChainNodesClone = JsonUtils.DeepCopy(existedChainNodes); var existedChainNodesClone = CloneOutbounds(existedChainNodes);
foreach (var existedChainNode in existedChainNodesClone) foreach (var existedChainNode in existedChainNodesClone)
{ {
var cloneTag = $"{existedChainNode.tag}-clone-{j + 1}"; var cloneTag = $"{existedChainNode.tag}-clone-{j + 1}";
@@ -955,4 +958,19 @@ public partial class CoreConfigV2rayService
return fragmentMask; return fragmentMask;
} }
private List<Outbounds4Ray> CloneOutbounds(List<Outbounds4Ray> outbounds)
{
var clonedOutbounds = new List<Outbounds4Ray>();
foreach (var outbound in outbounds)
{
var clonedOutbound = JsonUtils.DeepCopy(outbound);
clonedOutbounds.Add(clonedOutbound);
if (context.CustomOutboundMap.ContainsKey(outbound))
{
context.CustomOutboundMap[clonedOutbound] = context.CustomOutboundMap[outbound];
}
}
return clonedOutbounds;
}
} }

View File

@@ -188,7 +188,8 @@ public partial class CoreConfigV2rayService
if (node == null if (node == null
|| (!Global.XraySupportConfigType.Contains(node.ConfigType) || (!Global.XraySupportConfigType.Contains(node.ConfigType)
&& !node.ConfigType.IsGroupType())) && !node.ConfigType.IsGroupType()
&& node.ConfigType is not EConfigType.Outbound))
{ {
return Global.ProxyTag; return Global.ProxyTag;
} }

View File

@@ -1,45 +1,45 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddGroupServerViewModel : MyReactiveObject, ICloseable public partial class AddGroupServerViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public partial ProfileItem SelectedSource { get; set; }
[Reactive] [Reactive]
public ProfileItem SelectedChild { get; set; } public partial ProfileItem SelectedChild { get; set; }
[Reactive] [Reactive]
public IList<ProfileItem> SelectedChildren { get; set; } public partial IList<ProfileItem> SelectedChildren { get; set; }
[Reactive] [Reactive]
public string? CoreType { get; set; } public partial string? CoreType { get; set; }
[Reactive] [Reactive]
public string? PolicyGroupType { get; set; } public partial string? PolicyGroupType { get; set; }
[Reactive] [Reactive]
public SubItem? SelectedSubItem { get; set; } public partial SubItem? SelectedSubItem { get; set; }
[Reactive] [Reactive]
public string? Filter { get; set; } public partial string? Filter { get; set; }
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>(); public BulkObservableCollection<SubItem> SubItems { get; } = [];
public IObservableCollection<ProfileItem> ChildItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>(); public BulkObservableCollection<ProfileItem> ChildItemsObs { get; } = [];
public IObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>(); public BulkObservableCollection<ProfileItem> AllProfilePreviewItemsObs { get; } = [];
public ReactiveCommand<Unit, Unit> AddCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RemoveCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveTopCmd { get; }
public ReactiveCommand<Unit, Unit> MoveUpCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveUpCmd { get; }
public ReactiveCommand<Unit, Unit> MoveDownCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveDownCmd { get; }
public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveBottomCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public AddGroupServerViewModel(ProfileItem profileItem) public AddGroupServerViewModel(ProfileItem profileItem)
{ {

View File

@@ -1,20 +1,23 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddServer2ViewModel : MyReactiveObject, ICloseable public partial class AddServer2ViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
public Interaction<Unit, string?> BrowseConfigFileInteraction { get; } = new(); public Interaction<RxVoid, string?> BrowseConfigFileInteraction { get; } = new();
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public partial ProfileItem SelectedSource { get; set; }
[Reactive] [Reactive]
public string? CoreType { get; set; } public partial string? CoreType { get; set; }
public ReactiveCommand<Unit, Unit> BrowseServerCmd { get; } [Reactive]
public ReactiveCommand<Unit, Unit> EditServerCmd { get; } public partial bool IsSingboxEndpoint { get; set; }
public ReactiveCommand<Unit, Unit> SaveServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> BrowseServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> EditServerCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> SaveServerCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }
public AddServer2ViewModel(ProfileItem profileItem) public AddServer2ViewModel(ProfileItem profileItem)
@@ -23,7 +26,7 @@ public class AddServer2ViewModel : MyReactiveObject, ICloseable
BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => BrowseServerCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
var fileName = await BrowseConfigFileInteraction.Handle(Unit.Default); var fileName = await BrowseConfigFileInteraction.Handle(RxVoid.Default);
if (fileName.IsNullOrEmpty()) if (fileName.IsNullOrEmpty())
{ {
return; return;
@@ -40,7 +43,10 @@ public class AddServer2ViewModel : MyReactiveObject, ICloseable
}); });
SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem); SelectedSource = profileItem.IndexId.IsNullOrEmpty() ? profileItem : JsonUtils.DeepCopy(profileItem);
CoreType = SelectedSource?.CoreType?.ToString(); var coreStr = SelectedSource?.CoreType?.ToString();
coreStr = coreStr.IsNullOrEmpty() ? Global.CoreTypes.FirstOrDefault() : coreStr;
CoreType = coreStr;
IsSingboxEndpoint = SelectedSource?.GetProtocolExtra()?.IsSingboxEndpoint ?? false;
} }
private async Task SaveServerAsync() private async Task SaveServerAsync()
@@ -58,6 +64,10 @@ public class AddServer2ViewModel : MyReactiveObject, ICloseable
return; return;
} }
SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType); SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : Enum.Parse<ECoreType>(CoreType);
SelectedSource.SetProtocolExtra(SelectedSource?.GetProtocolExtra() with
{
IsSingboxEndpoint = IsSingboxEndpoint ? true : null,
});
if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0)
{ {
@@ -80,7 +90,8 @@ public class AddServer2ViewModel : MyReactiveObject, ICloseable
var item = await AppManager.Instance.GetProfileItem(SelectedSource.IndexId); var item = await AppManager.Instance.GetProfileItem(SelectedSource.IndexId);
item ??= SelectedSource; item ??= SelectedSource;
item.Address = fileName; item.Address = fileName;
if (await ConfigHandler.AddCustomServer(_config, item, false) == 0) var result = item.ConfigType == EConfigType.Outbound ? await ConfigHandler.AddCustomOutboundServer(_config, item, false) : await ConfigHandler.AddCustomServer(_config, item, false);
if (result == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedCustomServer);
if (item.IndexId.IsNotEmpty()) if (item.IndexId.IsNotEmpty())

View File

@@ -1,131 +1,131 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class AddServerViewModel : MyReactiveObject, ICloseable public partial class AddServerViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
[Reactive] [Reactive]
public ProfileItem SelectedSource { get; set; } public partial ProfileItem SelectedSource { get; set; }
[Reactive] [Reactive]
public string? CoreType { get; set; } public partial string? CoreType { get; set; }
[Reactive] [Reactive]
public bool AllowInsecure { get; set; } public partial bool AllowInsecure { get; set; }
[Reactive] [Reactive]
public bool MuxEnabled { get; set; } public partial bool MuxEnabled { get; set; }
[Reactive] [Reactive]
public string Cert { get; set; } public partial string Cert { get; set; }
[Reactive] [Reactive]
public string CertTip { get; set; } public partial string CertTip { get; set; }
[Reactive] [Reactive]
public string CertSha { get; set; } public partial string CertSha { get; set; }
[Reactive] [Reactive]
public string SalamanderPass { get; set; } public partial string SalamanderPass { get; set; }
[Reactive] [Reactive]
public int AlterId { get; set; } public partial int AlterId { get; set; }
[Reactive] [Reactive]
public string Ports { get; set; } public partial string Ports { get; set; }
[Reactive] [Reactive]
public int? UpMbps { get; set; } public partial int? UpMbps { get; set; }
[Reactive] [Reactive]
public int? DownMbps { get; set; } public partial int? DownMbps { get; set; }
[Reactive] [Reactive]
public string HopInterval { get; set; } public partial string HopInterval { get; set; }
[Reactive] [Reactive]
public string Flow { get; set; } public partial string Flow { get; set; }
[Reactive] [Reactive]
public string VmessSecurity { get; set; } public partial string VmessSecurity { get; set; }
[Reactive] [Reactive]
public string VlessEncryption { get; set; } public partial string VlessEncryption { get; set; }
[Reactive] [Reactive]
public string SsMethod { get; set; } public partial string SsMethod { get; set; }
[Reactive] [Reactive]
public string WgPublicKey { get; set; } public partial string WgPublicKey { get; set; }
[Reactive] [Reactive]
public string WgPresharedKey { get; set; } public partial string WgPresharedKey { get; set; }
[Reactive] [Reactive]
public string WgInterfaceAddress { get; set; } public partial string WgInterfaceAddress { get; set; }
[Reactive] [Reactive]
public string WgReserved { get; set; } public partial string WgReserved { get; set; }
[Reactive] [Reactive]
public int WgMtu { get; set; } public partial int WgMtu { get; set; }
[Reactive] [Reactive]
public bool Uot { get; set; } public partial bool Uot { get; set; }
[Reactive] [Reactive]
public string CongestionControl { get; set; } public partial string CongestionControl { get; set; }
[Reactive] [Reactive]
public int? InsecureConcurrency { get; set; } public partial int? InsecureConcurrency { get; set; }
[Reactive] [Reactive]
public bool NaiveQuic { get; set; } public partial bool NaiveQuic { get; set; }
[Reactive] [Reactive]
public string HttpHeadersJson { get; set; } public partial string HttpHeadersJson { get; set; }
[Reactive] [Reactive]
public string Hy2RealmUrl { get; set; } public partial string Hy2RealmUrl { get; set; }
[Reactive] [Reactive]
public int GeckoMinPacketSize { get; set; } public partial int GeckoMinPacketSize { get; set; }
[Reactive] [Reactive]
public int GeckoMaxPacketSize { get; set; } public partial int GeckoMaxPacketSize { get; set; }
[Reactive] [Reactive]
public string RawHeaderType { get; set; } public partial string RawHeaderType { get; set; }
[Reactive] [Reactive]
public string Host { get; set; } public partial string Host { get; set; }
[Reactive] [Reactive]
public string Path { get; set; } public partial string Path { get; set; }
[Reactive] [Reactive]
public string XhttpMode { get; set; } public partial string XhttpMode { get; set; }
[Reactive] [Reactive]
public string XhttpExtra { get; set; } public partial string XhttpExtra { get; set; }
[Reactive] [Reactive]
public string GrpcAuthority { get; set; } public partial string GrpcAuthority { get; set; }
[Reactive] [Reactive]
public string GrpcServiceName { get; set; } public partial string GrpcServiceName { get; set; }
[Reactive] [Reactive]
public string GrpcMode { get; set; } public partial string GrpcMode { get; set; }
[Reactive] [Reactive]
public string KcpHeaderType { get; set; } public partial string KcpHeaderType { get; set; }
[Reactive] [Reactive]
public string KcpSeed { get; set; } public partial string KcpSeed { get; set; }
[Reactive] [Reactive]
public int? KcpMtu { get; set; } public partial int? KcpMtu { get; set; }
public string TransportHeaderType public string TransportHeaderType
{ {
@@ -239,9 +239,9 @@ public class AddServerViewModel : MyReactiveObject, ICloseable
} }
} }
public ReactiveCommand<Unit, Unit> FetchCertCmd { get; } public ReactiveCommand<RxVoid, RxVoid> FetchCertCmd { get; }
public ReactiveCommand<Unit, Unit> FetchCertChainCmd { get; } public ReactiveCommand<RxVoid, RxVoid> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public AddServerViewModel(ProfileItem profileItem) public AddServerViewModel(ProfileItem profileItem)
{ {

View File

@@ -1,19 +1,19 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class BackupAndRestoreViewModel : MyReactiveObject public partial class BackupAndRestoreViewModel : MyReactiveObject
{ {
private readonly string _guiConfigs = "guiConfigs"; private readonly string _guiConfigs = "guiConfigs";
private static string BackupFileName => $"backup_{DateTime.Now:yyyyMMddHHmmss}.zip"; private static string BackupFileName => $"backup_{DateTime.Now:yyyyMMddHHmmss}.zip";
public ReactiveCommand<Unit, Unit> RemoteBackupCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RemoteBackupCmd { get; }
public ReactiveCommand<Unit, Unit> RemoteRestoreCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RemoteRestoreCmd { get; }
public ReactiveCommand<Unit, Unit> WebDavCheckCmd { get; } public ReactiveCommand<RxVoid, RxVoid> WebDavCheckCmd { get; }
[Reactive] [Reactive]
public WebDavItem SelectedSource { get; set; } public partial WebDavItem SelectedSource { get; set; }
[Reactive] [Reactive]
public string OperationMsg { get; set; } = string.Empty; public partial string OperationMsg { get; set; } = string.Empty;
public BackupAndRestoreViewModel() public BackupAndRestoreViewModel()
{ {

View File

@@ -1,18 +1,18 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class CheckUpdateViewModel : MyReactiveObject public partial class CheckUpdateViewModel : MyReactiveObject
{ {
private const string _geo = "GeoFiles"; private const string _geo = "GeoFiles";
private readonly ECoreType _v2rayN = ECoreType.v2rayN; private readonly ECoreType _v2rayN = ECoreType.v2rayN;
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 EventChannel<RxVoid> ReloadRequested { get; } = new();
public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>(); public BulkObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = [];
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; } public ReactiveCommand<RxVoid, RxVoid> CheckUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; } public ReactiveCommand<RxVoid, RxVoid> CheckOnlyCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } [Reactive] public partial bool EnableCheckPreReleaseUpdate { get; set; }
public CheckUpdateViewModel() public CheckUpdateViewModel()
{ {
@@ -288,10 +288,9 @@ public class CheckUpdateViewModel : MyReactiveObject
private async Task UpdateFinishedSub(bool blReload) private async Task UpdateFinishedSub(bool blReload)
{ {
RxSchedulers.MainThreadScheduler.Schedule(blReload, (scheduler, blReload) => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
_ = UpdateFinishedResult(blReload); _ = UpdateFinishedResult(blReload);
return Disposable.Empty;
}); });
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -404,10 +403,9 @@ public class CheckUpdateViewModel : MyReactiveObject
Remarks = msg, Remarks = msg,
}; };
RxSchedulers.MainThreadScheduler.Schedule(item, (scheduler, model) => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
_ = UpdateViewResult(model); _ = UpdateViewResult(item);
return Disposable.Empty;
}); });
await Task.CompletedTask; await Task.CompletedTask;
} }

View File

@@ -1,20 +1,20 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ClashConnectionsViewModel : MyReactiveObject public partial class ClashConnectionsViewModel : MyReactiveObject
{ {
public IObservableCollection<ClashConnectionModel> ConnectionItems { get; } = new ObservableCollectionExtended<ClashConnectionModel>(); public BulkObservableCollection<ClashConnectionModel> ConnectionItems { get; } = [];
[Reactive] [Reactive]
public ClashConnectionModel SelectedSource { get; set; } public partial ClashConnectionModel SelectedSource { get; set; }
public ReactiveCommand<Unit, Unit> ConnectionCloseCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ConnectionCloseCmd { get; }
public ReactiveCommand<Unit, Unit> ConnectionCloseAllCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ConnectionCloseAllCmd { get; }
[Reactive] [Reactive]
public string HostFilter { get; set; } public partial string HostFilter { get; set; }
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public partial bool AutoRefresh { get; set; }
public ClashConnectionsViewModel() public ClashConnectionsViewModel()
{ {
@@ -55,10 +55,9 @@ public class ClashConnectionsViewModel : MyReactiveObject
return; return;
} }
RxSchedulers.MainThreadScheduler.Schedule(ret?.connections, (scheduler, model) => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
_ = RefreshConnections(model); _ = RefreshConnections(ret?.connections);
return Disposable.Empty;
}); });
} }

View File

@@ -1,37 +1,36 @@
using System.Reactive.Concurrency;
using static ServiceLib.Models.Dto.ClashProviders; using static ServiceLib.Models.Dto.ClashProviders;
using static ServiceLib.Models.Dto.ClashProxies; using static ServiceLib.Models.Dto.ClashProxies;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ClashProxiesViewModel : MyReactiveObject public partial class ClashProxiesViewModel : MyReactiveObject
{ {
private Dictionary<string, ProxiesItem>? _proxies; private Dictionary<string, ProxiesItem>? _proxies;
private Dictionary<string, ProvidersItem>? _providers; private Dictionary<string, ProvidersItem>? _providers;
private readonly int _delayTimeout = 99999999; private readonly int _delayTimeout = 99999999;
public IObservableCollection<ClashProxyModel> ProxyGroups { get; } = new ObservableCollectionExtended<ClashProxyModel>(); public BulkObservableCollection<ClashProxyModel> ProxyGroups { get; } = [];
public IObservableCollection<ClashProxyModel> ProxyDetails { get; } = new ObservableCollectionExtended<ClashProxyModel>(); public BulkObservableCollection<ClashProxyModel> ProxyDetails { get; } = [];
[Reactive] [Reactive]
public ClashProxyModel SelectedGroup { get; set; } public partial ClashProxyModel SelectedGroup { get; set; }
[Reactive] [Reactive]
public ClashProxyModel SelectedDetail { get; set; } public partial ClashProxyModel SelectedDetail { get; set; }
public ReactiveCommand<Unit, Unit> ProxiesReloadCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ProxiesReloadCmd { get; }
public ReactiveCommand<Unit, Unit> ProxiesDelayTestCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ProxiesDelayTestCmd { get; }
public ReactiveCommand<Unit, Unit> ProxiesDelayTestPartCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ProxiesDelayTestPartCmd { get; }
public ReactiveCommand<Unit, Unit> ProxiesSelectActivityCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ProxiesSelectActivityCmd { get; }
[Reactive] [Reactive]
public int RuleModeSelected { get; set; } public partial int RuleModeSelected { get; set; }
[Reactive] [Reactive]
public int SortingSelected { get; set; } public partial int SortingSelected { get; set; }
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public partial bool AutoRefresh { get; set; }
public ClashProxiesViewModel() public ClashProxiesViewModel()
{ {
@@ -379,10 +378,9 @@ public class ClashProxiesViewModel : MyReactiveObject
} }
var model = new SpeedTestResult() { IndexId = item.Name, Delay = result }; var model = new SpeedTestResult() { IndexId = item.Name, Delay = result };
RxSchedulers.MainThreadScheduler.Schedule(model, (scheduler, model) => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
_ = ProxiesDelayTestResult(model); _ = ProxiesDelayTestResult(model);
return Disposable.Empty;
}); });
await Task.CompletedTask; await Task.CompletedTask;
}); });

View File

@@ -1,44 +1,44 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class DNSSettingViewModel : MyReactiveObject, ICloseable public partial class DNSSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
[Reactive] public bool UseSystemHosts { get; set; } [Reactive] public partial bool UseSystemHosts { get; set; }
[Reactive] public bool AddCommonHosts { get; set; } [Reactive] public partial bool AddCommonHosts { get; set; }
[Reactive] public bool FakeIP { get; set; } [Reactive] public partial bool FakeIP { get; set; }
[Reactive] public string FakeIPRange { get; set; } [Reactive] public partial string FakeIPRange { get; set; }
[Reactive] public bool BlockBindingQuery { get; set; } [Reactive] public partial bool BlockBindingQuery { get; set; }
[Reactive] public string DirectDNS { get; set; } [Reactive] public partial string DirectDNS { get; set; }
[Reactive] public string RemoteDNS { get; set; } [Reactive] public partial string RemoteDNS { get; set; }
[Reactive] public string BootstrapDNS { get; set; } [Reactive] public partial string BootstrapDNS { get; set; }
[Reactive] public string Strategy4Freedom { get; set; } [Reactive] public partial string Strategy4Freedom { get; set; }
[Reactive] public string Strategy4Proxy { get; set; } [Reactive] public partial string Strategy4Proxy { get; set; }
[Reactive] public string Strategy4ProxyDial { get; set; } [Reactive] public partial string Strategy4ProxyDial { get; set; }
[Reactive] public string Hosts { get; set; } [Reactive] public partial string Hosts { get; set; }
[Reactive] public string DirectExpectedIPs { get; set; } [Reactive] public partial string DirectExpectedIPs { get; set; }
[Reactive] public bool ParallelQuery { get; set; } [Reactive] public partial bool ParallelQuery { get; set; }
[Reactive] public bool ServeStale { get; set; } [Reactive] public partial bool ServeStale { get; set; }
[Reactive] public bool EnableHappyEyeballs { get; set; } [Reactive] public partial bool EnableHappyEyeballs { get; set; }
[Reactive] public bool UseSystemHostsCompatible { get; set; } [Reactive] public partial bool UseSystemHostsCompatible { get; set; }
[Reactive] public string DomainStrategy4FreedomCompatible { get; set; } = string.Empty; [Reactive] public partial string DomainStrategy4FreedomCompatible { get; set; } = string.Empty;
[Reactive] public string DomainDNSAddressCompatible { get; set; } = string.Empty; [Reactive] public partial string DomainDNSAddressCompatible { get; set; } = string.Empty;
[Reactive] public string NormalDNSCompatible { get; set; } = string.Empty; [Reactive] public partial string NormalDNSCompatible { get; set; } = string.Empty;
[Reactive] public string TunDNSCompatible { get; set; } = string.Empty; [Reactive] public partial string TunDNSCompatible { get; set; } = string.Empty;
[Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } = string.Empty; [Reactive] public partial string DomainStrategy4Freedom2Compatible { get; set; } = string.Empty;
[Reactive] public string DomainDNSAddress2Compatible { get; set; } = string.Empty; [Reactive] public partial string DomainDNSAddress2Compatible { get; set; } = string.Empty;
[Reactive] public string NormalDNS2Compatible { get; set; } = string.Empty; [Reactive] public partial string NormalDNS2Compatible { get; set; } = string.Empty;
[Reactive] public string TunDNS2Compatible { get; set; } = string.Empty; [Reactive] public partial string TunDNS2Compatible { get; set; } = string.Empty;
[Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public partial bool RayCustomDNSEnableCompatible { get; set; }
[Reactive] public bool SBCustomDNSEnableCompatible { get; set; } [Reactive] public partial bool SBCustomDNSEnableCompatible { get; set; }
[ObservableAsProperty] public bool IsSimpleDNSEnabled { get; } public bool IsSimpleDNSEnabled => !(RayCustomDNSEnableCompatible && SBCustomDNSEnableCompatible);
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ImportDefConfig4SingboxCompatibleCmd { get; }
public DNSSettingViewModel() public DNSSettingViewModel()
{ {
@@ -60,8 +60,7 @@ public class DNSSettingViewModel : MyReactiveObject, ICloseable
}); });
this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible) this.WhenAnyValue(x => x.RayCustomDNSEnableCompatible, x => x.SBCustomDNSEnableCompatible)
.Select(x => x is not { Item1: true, Item2: true }) .Subscribe(_ => this.RaisePropertyChanged(nameof(IsSimpleDNSEnabled)));
.ToPropertyEx(this, x => x.IsSimpleDNSEnabled);
_ = Init(); _ = Init();
} }

View File

@@ -1,42 +1,42 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class FullConfigTemplateViewModel : MyReactiveObject, ICloseable public partial class FullConfigTemplateViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
#region Reactive #region Reactive
[Reactive] [Reactive]
public bool EnableFullConfigTemplate4Ray { get; set; } public partial bool EnableFullConfigTemplate4Ray { get; set; }
[Reactive] [Reactive]
public bool EnableFullConfigTemplate4Singbox { get; set; } public partial bool EnableFullConfigTemplate4Singbox { get; set; }
[Reactive] [Reactive]
public string FullConfigTemplate4Ray { get; set; } = string.Empty; public partial string FullConfigTemplate4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullTunConfigTemplate4Ray { get; set; } = string.Empty; public partial string FullTunConfigTemplate4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullConfigTemplate4Singbox { get; set; } = string.Empty; public partial string FullConfigTemplate4Singbox { get; set; } = string.Empty;
[Reactive] [Reactive]
public string FullTunConfigTemplate4Singbox { get; set; } = string.Empty; public partial string FullTunConfigTemplate4Singbox { get; set; } = string.Empty;
[Reactive] [Reactive]
public bool AddProxyOnly4Ray { get; set; } public partial bool AddProxyOnly4Ray { get; set; }
[Reactive] [Reactive]
public bool AddProxyOnly4Singbox { get; set; } public partial bool AddProxyOnly4Singbox { get; set; }
[Reactive] [Reactive]
public string ProxyDetour4Ray { get; set; } = string.Empty; public partial string ProxyDetour4Ray { get; set; } = string.Empty;
[Reactive] [Reactive]
public string ProxyDetour4Singbox { get; set; } = string.Empty; public partial string ProxyDetour4Singbox { get; set; } = string.Empty;
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
#endregion Reactive #endregion Reactive

View File

@@ -6,7 +6,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject, ICloseable
private readonly List<KeyEventItem> _globalHotkeys; private readonly List<KeyEventItem> _globalHotkeys;
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public GlobalHotkeySettingViewModel() public GlobalHotkeySettingViewModel()
{ {

View File

@@ -1,13 +1,11 @@
using System.Reactive.Concurrency;
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class MainWindowViewModel : MyReactiveObject public partial class MainWindowViewModel : MyReactiveObject
{ {
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new(); public Interaction<RxVoid, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, byte[]?> ScanScreenInteraction { get; } = new(); public Interaction<RxVoid, byte[]?> ScanScreenInteraction { get; } = new();
public Interaction<Unit, string?> BrowseImageFileInteraction { get; } = new(); public Interaction<RxVoid, string?> BrowseImageFileInteraction { get; } = new();
public Interaction<bool?, Unit> ShowHideWindowInteraction { get; } = new(); public Interaction<bool?, RxVoid> ShowHideWindowInteraction { get; } = new();
public bool DesignMode { get; set; } public bool DesignMode { get; set; }
@@ -22,70 +20,73 @@ public class MainWindowViewModel : MyReactiveObject
#region Menu #region Menu
//servers //servers
public ReactiveCommand<Unit, Unit> AddVmessServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddVmessServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddVlessServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddVlessServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddShadowsocksServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddShadowsocksServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddSocksServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddSocksServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddHttpServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddHttpServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddTrojanServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddTrojanServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddHysteria2ServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddHysteria2ServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddTuicServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddTuicServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddWireguardServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddWireguardServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddAnytlsServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddAnytlsServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddNaiveServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddNaiveServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddCustomServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddCustomServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddPolicyGroupServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddCustomOutboundServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddProxyChainServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddPolicyGroupServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddProxyChainServerCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddServerViaClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaImageCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddServerViaScanCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> AddServerViaImageCmd { get; }
//Subscription //Subscription
public ReactiveCommand<Unit, Unit> SubSettingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubSettingCmd { get; }
public ReactiveCommand<Unit, Unit> SubUpdateCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> SubUpdateViaProxyCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubUpdateViaProxyCmd { get; }
public ReactiveCommand<Unit, Unit> SubGroupUpdateCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubGroupUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> SubGroupUpdateViaProxyCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubGroupUpdateViaProxyCmd { get; }
//Setting //Setting
public ReactiveCommand<Unit, Unit> OptionSettingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> OptionSettingCmd { get; }
public ReactiveCommand<Unit, Unit> RoutingSettingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RoutingSettingCmd { get; }
public ReactiveCommand<Unit, Unit> DNSSettingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> DNSSettingCmd { get; }
public ReactiveCommand<Unit, Unit> FullConfigTemplateCmd { get; } public ReactiveCommand<RxVoid, RxVoid> FullConfigTemplateCmd { get; }
public ReactiveCommand<Unit, Unit> GlobalHotkeySettingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> GlobalHotkeySettingCmd { get; }
public ReactiveCommand<Unit, Unit> RebootAsAdminCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RebootAsAdminCmd { get; }
public ReactiveCommand<Unit, Unit> ClearServerStatisticsCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ClearServerStatisticsCmd { get; }
public ReactiveCommand<Unit, Unit> OpenTheFileLocationCmd { get; } public ReactiveCommand<RxVoid, RxVoid> OpenTheFileLocationCmd { get; }
//Presets //Presets
public ReactiveCommand<Unit, Unit> RegionalPresetDefaultCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RegionalPresetDefaultCmd { get; }
public ReactiveCommand<Unit, Unit> RegionalPresetRussiaCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RegionalPresetRussiaCmd { get; }
public ReactiveCommand<Unit, Unit> RegionalPresetIranCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RegionalPresetIranCmd { get; }
public ReactiveCommand<Unit, Unit> ReloadCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ReloadCmd { get; }
[Reactive] [Reactive]
public bool BlReloadEnabled { get; set; } public partial bool BlReloadEnabled { get; set; }
[Reactive] [Reactive]
public bool ShowClashUI { get; set; } public partial bool ShowClashUI { get; set; }
[Reactive] [Reactive]
public int TabMainSelectedIndex { get; set; } public partial int TabMainSelectedIndex { get; set; }
[Reactive] public bool BlIsWindows { get; set; } [Reactive] public partial bool BlIsWindows { get; set; }
[Reactive] public bool BlNewUpdate { get; set; } [Reactive] public partial bool BlNewUpdate { get; set; }
[Reactive] public EGirdOrientation MainGirdOrientation { get; set; } [Reactive] public partial EGirdOrientation MainGirdOrientation { get; set; }
#endregion Menu #endregion Menu
private readonly SynchronizationContext _uiContext = SynchronizationContext.Current;
#region Init #region Init
public MainWindowViewModel() public MainWindowViewModel()
@@ -145,6 +146,10 @@ public class MainWindowViewModel : MyReactiveObject
{ {
await AddServerAsync(EConfigType.Custom); await AddServerAsync(EConfigType.Custom);
}); });
AddCustomOutboundServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AddServerAsync(EConfigType.Outbound);
});
AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () => AddPolicyGroupServerCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
await AddServerAsync(EConfigType.PolicyGroup); await AddServerAsync(EConfigType.PolicyGroup);
@@ -268,7 +273,7 @@ public class MainWindowViewModel : MyReactiveObject
.ObserveOn(RxSchedulers.MainThreadScheduler) .ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServers()); .Subscribe(async _ => await RefreshServers());
var vmReloadRequestedList = new List<IObservable<Unit>> var vmReloadRequestedList = new List<IObservable<RxVoid>>
{ {
ProfilesViewModel.ReloadRequested.AsObservable(), ProfilesViewModel.ReloadRequested.AsObservable(),
StatusBarViewModel.ReloadRequested.AsObservable(), StatusBarViewModel.ReloadRequested.AsObservable(),
@@ -407,12 +412,15 @@ public class MainWindowViewModel : MyReactiveObject
private async Task RefreshServersDispatcherAsync() private async Task RefreshServersDispatcherAsync()
{ {
await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler); //await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler);
_uiContext?.Post(_ => _ = RefreshServers(), null);
} }
private async Task RefreshSubscriptions() private async Task RefreshSubscriptions()
{ {
await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler); //await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler);
_uiContext?.Post(_ => _ = ProfilesViewModel.RefreshSubscriptions(), null);
} }
#endregion Servers && Groups #endregion Servers && Groups
@@ -429,7 +437,7 @@ public class MainWindowViewModel : MyReactiveObject
}; };
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType is EConfigType.Custom or EConfigType.Outbound)
{ {
var addServer2ViewModel = new AddServer2ViewModel(item); var addServer2ViewModel = new AddServer2ViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel); ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel);
@@ -459,7 +467,7 @@ public class MainWindowViewModel : MyReactiveObject
var stringData = clipboardData; var stringData = clipboardData;
if (clipboardData == null) if (clipboardData == null)
{ {
var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default); var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);
@@ -482,7 +490,7 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaScanAsync() public async Task AddServerViaScanAsync()
{ {
var result = await ScanScreenInteraction.Handle(Unit.Default); var result = await ScanScreenInteraction.Handle(RxVoid.Default);
await ScanScreenResult(result); await ScanScreenResult(result);
} }
@@ -494,7 +502,7 @@ public class MainWindowViewModel : MyReactiveObject
public async Task AddServerViaImageAsync() public async Task AddServerViaImageAsync()
{ {
var imageFileName = await BrowseImageFileInteraction.Handle(Unit.Default); var imageFileName = await BrowseImageFileInteraction.Handle(RxVoid.Default);
await AddScanResultAsync(imageFileName); await AddScanResultAsync(imageFileName);
} }

View File

@@ -1,8 +1,8 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject public partial class MsgViewModel : MyReactiveObject
{ {
public Interaction<string, Unit> DispatcherShowMsgInteraction { get; } = new(); public Interaction<string, RxVoid> DispatcherShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _queueMsg = new(); private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable; private volatile bool _lastMsgFilterNotAvailable;
@@ -10,10 +10,10 @@ public class MsgViewModel : MyReactiveObject
public int NumMaxMsg { get; } = 500; public int NumMaxMsg { get; } = 500;
[Reactive] [Reactive]
public string MsgFilter { get; set; } public partial string MsgFilter { get; set; }
[Reactive] [Reactive]
public bool AutoRefresh { get; set; } public partial bool AutoRefresh { get; set; }
public MsgViewModel() public MsgViewModel()
{ {
@@ -36,6 +36,11 @@ public class MsgViewModel : MyReactiveObject
.Subscribe(content => _ = AppendQueueMsg(content)); .Subscribe(content => _ = AppendQueueMsg(content));
} }
public void FlushQueueMsg()
{
_ = AppendQueueMsg(string.Empty);
}
private async Task AppendQueueMsg(string msg) private async Task AppendQueueMsg(string msg)
{ {
if (AutoRefresh == false) if (AutoRefresh == false)
@@ -65,7 +70,17 @@ public class MsgViewModel : MyReactiveObject
sb.Append(line); sb.Append(line);
} }
await DispatcherShowMsgInteraction.Handle(sb.ToString()); if (sb.Length > 0)
{
try
{
await DispatcherShowMsgInteraction.Handle(sb.ToString());
}
catch (Exception)
{
_queueMsg.Enqueue(sb.ToString());
}
}
} }
finally finally
{ {
@@ -75,6 +90,11 @@ public class MsgViewModel : MyReactiveObject
private void EnqueueQueueMsg(string msg) private void EnqueueQueueMsg(string msg)
{ {
if (string.IsNullOrEmpty(msg))
{
return;
}
//filter msg //filter msg
if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable) if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable)
{ {

View File

@@ -1,119 +1,119 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class OptionSettingViewModel : MyReactiveObject, ICloseable public partial class OptionSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
#region Core #region Core
[Reactive] public int LocalPort { get; set; } [Reactive] public partial int LocalPort { get; set; }
[Reactive] public bool SecondLocalPortEnabled { get; set; } [Reactive] public partial bool SecondLocalPortEnabled { get; set; }
[Reactive] public bool UdpEnabled { get; set; } [Reactive] public partial bool UdpEnabled { get; set; }
[Reactive] public bool SniffingEnabled { get; set; } [Reactive] public partial bool SniffingEnabled { get; set; }
public IList<string> DestOverride { get; set; } public IList<string> DestOverride { get; set; }
[Reactive] public bool RouteOnly { get; set; } [Reactive] public partial bool RouteOnly { get; set; }
[Reactive] public bool AllowLANConn { get; set; } [Reactive] public partial bool AllowLANConn { get; set; }
[Reactive] public bool NewPort4LAN { get; set; } [Reactive] public partial bool NewPort4LAN { get; set; }
[Reactive] public string User { get; set; } [Reactive] public partial string User { get; set; }
[Reactive] public string Pass { get; set; } [Reactive] public partial string Pass { get; set; }
[Reactive] public bool LogEnabled { get; set; } [Reactive] public partial bool LogEnabled { get; set; }
[Reactive] public string Loglevel { get; set; } [Reactive] public partial string Loglevel { get; set; }
[Reactive] public string DefFingerprint { get; set; } [Reactive] public partial string DefFingerprint { get; set; }
[Reactive] public string DefUserAgent { get; set; } [Reactive] public partial string DefUserAgent { get; set; }
[Reactive] public string SendThrough { get; set; } [Reactive] public partial string SendThrough { get; set; }
[Reactive] public string BindInterface { get; set; } [Reactive] public partial string BindInterface { get; set; }
[Reactive] public string Mux4SboxProtocol { get; set; } [Reactive] public partial string Mux4SboxProtocol { get; set; }
[Reactive] public bool EnableCacheFile4Sbox { get; set; } [Reactive] public partial bool EnableCacheFile4Sbox { get; set; }
[Reactive] public int? HyUpMbps { get; set; } [Reactive] public partial int? HyUpMbps { get; set; }
[Reactive] public int? HyDownMbps { get; set; } [Reactive] public partial int? HyDownMbps { get; set; }
[Reactive] public bool EnableFragment { get; set; } [Reactive] public partial bool EnableFragment { get; set; }
[Reactive] public bool EnableFinalFragment { get; set; } [Reactive] public partial bool EnableFinalFragment { get; set; }
[Reactive] public string FragmentPackets { get; set; } [Reactive] public partial string FragmentPackets { get; set; }
[Reactive] public string FragmentLengths { get; set; } [Reactive] public partial string FragmentLengths { get; set; }
[Reactive] public string FragmentDelays { get; set; } [Reactive] public partial string FragmentDelays { get; set; }
[Reactive] public string FragmentMaxSplit { get; set; } [Reactive] public partial string FragmentMaxSplit { get; set; }
#endregion Core #endregion Core
#region UI #region UI
[Reactive] public bool AutoRun { get; set; } [Reactive] public partial bool AutoRun { get; set; }
[Reactive] public bool EnableStatistics { get; set; } [Reactive] public partial bool EnableStatistics { get; set; }
[Reactive] public bool KeepOlderDedupl { get; set; } [Reactive] public partial bool KeepOlderDedupl { get; set; }
[Reactive] public bool DisplayRealTimeSpeed { get; set; } [Reactive] public partial bool DisplayRealTimeSpeed { get; set; }
[Reactive] public bool EnableAutoAdjustMainLvColWidth { get; set; } [Reactive] public partial bool EnableAutoAdjustMainLvColWidth { get; set; }
[Reactive] public bool AutoHideStartup { get; set; } [Reactive] public partial bool AutoHideStartup { get; set; }
[Reactive] public bool Hide2TrayWhenClose { get; set; } [Reactive] public partial bool Hide2TrayWhenClose { get; set; }
[Reactive] public bool MacOSShowInDock { get; set; } [Reactive] public partial bool MacOSShowInDock { get; set; }
[Reactive] public bool EnableDragDropSort { get; set; } [Reactive] public partial bool EnableDragDropSort { get; set; }
[Reactive] public bool DoubleClick2Activate { get; set; } [Reactive] public partial bool DoubleClick2Activate { get; set; }
[Reactive] public int AutoUpdateInterval { get; set; } [Reactive] public partial int AutoUpdateInterval { get; set; }
[Reactive] public int TrayMenuServersLimit { get; set; } [Reactive] public partial int TrayMenuServersLimit { get; set; }
[Reactive] public string CurrentFontFamily { get; set; } [Reactive] public partial string CurrentFontFamily { get; set; }
[Reactive] public int SpeedTestTimeout { get; set; } [Reactive] public partial int SpeedTestTimeout { get; set; }
[Reactive] public string SpeedTestUrl { get; set; } [Reactive] public partial string SpeedTestUrl { get; set; }
[Reactive] public string SpeedPingTestUrl { get; set; } [Reactive] public partial string SpeedPingTestUrl { get; set; }
[Reactive] public string UdpTestTarget { get; set; } [Reactive] public partial string UdpTestTarget { get; set; }
[Reactive] public int MixedConcurrencyCount { get; set; } [Reactive] public partial int MixedConcurrencyCount { get; set; }
[Reactive] public bool EnableHWA { get; set; } [Reactive] public partial bool EnableHWA { get; set; }
[Reactive] public string SubConvertUrl { get; set; } [Reactive] public partial string SubConvertUrl { get; set; }
[Reactive] public int MainGirdOrientation { get; set; } [Reactive] public partial int MainGirdOrientation { get; set; }
[Reactive] public string GeoFileSourceUrl { get; set; } [Reactive] public partial string GeoFileSourceUrl { get; set; }
[Reactive] public string SrsFileSourceUrl { get; set; } [Reactive] public partial string SrsFileSourceUrl { get; set; }
[Reactive] public string RoutingRulesSourceUrl { get; set; } [Reactive] public partial string RoutingRulesSourceUrl { get; set; }
[Reactive] public string IPAPIUrl { get; set; } [Reactive] public partial string IPAPIUrl { get; set; }
[Reactive] public string RootCertProvider { get; set; } [Reactive] public partial string RootCertProvider { get; set; }
#endregion UI #endregion UI
#region UI visibility #region UI visibility
[Reactive] public bool BlIsWindows { get; set; } [Reactive] public partial bool BlIsWindows { get; set; }
[Reactive] public bool BlIsLinux { get; set; } [Reactive] public partial bool BlIsLinux { get; set; }
[Reactive] public bool BlIsIsMacOS { get; set; } [Reactive] public partial bool BlIsIsMacOS { get; set; }
[Reactive] public bool BlIsNonWindows { get; set; } [Reactive] public partial bool BlIsNonWindows { get; set; }
#endregion UI visibility #endregion UI visibility
#region System proxy #region System proxy
[Reactive] public bool NotProxyLocalAddress { get; set; } [Reactive] public partial bool NotProxyLocalAddress { get; set; }
[Reactive] public string SystemProxyAdvancedProtocol { get; set; } [Reactive] public partial string SystemProxyAdvancedProtocol { get; set; }
[Reactive] public string SystemProxyExceptions { get; set; } [Reactive] public partial string SystemProxyExceptions { get; set; }
[Reactive] public string CustomSystemProxyPacPath { get; set; } [Reactive] public partial string CustomSystemProxyPacPath { get; set; }
[Reactive] public string CustomSystemProxyScriptPath { get; set; } [Reactive] public partial string CustomSystemProxyScriptPath { get; set; }
#endregion System proxy #endregion System proxy
#region Tun mode #region Tun mode
[Reactive] public bool TunAutoRoute { get; set; } [Reactive] public partial bool TunAutoRoute { get; set; }
[Reactive] public bool TunStrictRoute { get; set; } [Reactive] public partial bool TunStrictRoute { get; set; }
[Reactive] public string TunStack { get; set; } [Reactive] public partial string TunStack { get; set; }
[Reactive] public int TunMtu { get; set; } [Reactive] public partial int TunMtu { get; set; }
[Reactive] public bool TunEnableIPv6Address { get; set; } [Reactive] public partial bool TunEnableIPv6Address { get; set; }
[Reactive] public string TunIcmpRouting { get; set; } [Reactive] public partial string TunIcmpRouting { get; set; }
[Reactive] public bool TunEnableLegacyProtect { get; set; } [Reactive] public partial bool TunEnableLegacyProtect { get; set; }
[Reactive] public string TunRouteExcludeAddress { get; set; } [Reactive] public partial string TunRouteExcludeAddress { get; set; }
[Reactive] public string TunIpv4Address { get; set; } [Reactive] public partial string TunIPv4Address { get; set; }
[Reactive] public string TunIpv6Address { get; set; } [Reactive] public partial string TunIPv6Address { get; set; }
#endregion Tun mode #endregion Tun mode
#region CoreType #region CoreType
[Reactive] public string CoreType1 { get; set; } [Reactive] public partial string CoreType1 { get; set; }
[Reactive] public string CoreType2 { get; set; } [Reactive] public partial string CoreType2 { get; set; }
[Reactive] public string CoreType3 { get; set; } [Reactive] public partial string CoreType3 { get; set; }
[Reactive] public string CoreType4 { get; set; } [Reactive] public partial string CoreType4 { get; set; }
[Reactive] public string CoreType5 { get; set; } [Reactive] public partial string CoreType5 { get; set; }
[Reactive] public string CoreType6 { get; set; } [Reactive] public partial string CoreType6 { get; set; }
[Reactive] public string CoreType7 { get; set; } [Reactive] public partial string CoreType7 { get; set; }
[Reactive] public string CoreType9 { get; set; } [Reactive] public partial string CoreType9 { get; set; }
#endregion CoreType #endregion CoreType
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public OptionSettingViewModel() public OptionSettingViewModel()
{ {
@@ -216,8 +216,8 @@ public class OptionSettingViewModel : MyReactiveObject, ICloseable
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; TunIPv4Address = _config.TunModeItem.IPv4Address;
TunIpv6Address = _config.TunModeItem.Ipv6Address; TunIPv6Address = _config.TunModeItem.IPv6Address;
#endregion Tun mode #endregion Tun mode
@@ -382,8 +382,8 @@ public class OptionSettingViewModel : MyReactiveObject, ICloseable
_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.IPv4Address = TunIPv4Address;
_config.TunModeItem.Ipv6Address = TunIpv6Address; _config.TunModeItem.IPv6Address = TunIPv6Address;
//coreType //coreType
await SaveCoreType(); await SaveCoreType();

View File

@@ -1,10 +1,10 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject, ICloseable public partial class ProfilesSelectViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new(); public Interaction<RxVoid, RxVoid> ProfilesFocusInteraction { get; } = new();
#region private prop #region private prop
@@ -16,34 +16,34 @@ public class ProfilesSelectViewModel : MyReactiveObject, ICloseable
#endregion private prop #endregion private prop
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
#region ObservableCollection #region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>(); public BulkObservableCollection<ProfileItemModel> ProfileItems { get; } = [];
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>(); public BulkObservableCollection<SubItem> SubItems { get; } = [];
[Reactive] [Reactive]
public ProfileItemModel SelectedProfile { get; set; } public partial ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; } public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive] [Reactive]
public SubItem SelectedSub { get; set; } public partial SubItem SelectedSub { get; set; }
[Reactive] [Reactive]
public string ServerFilter { get; set; } public partial string ServerFilter { get; set; }
// Include/Exclude filter for ConfigType // Include/Exclude filter for ConfigType
[Reactive] [Reactive]
public List<EConfigType> FilterConfigTypes { get; set; } public partial List<EConfigType> FilterConfigTypes { get; set; }
[Reactive] [Reactive]
public bool FilterExclude { get; set; } public partial bool FilterExclude { get; set; }
[Reactive] [Reactive]
public bool MultiSelect { get; set; } public partial bool MultiSelect { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
@@ -140,9 +140,9 @@ public class ProfilesSelectViewModel : MyReactiveObject, ICloseable
try try
{ {
await ProfilesFocusInteraction.Handle(Unit.Default); await ProfilesFocusInteraction.Handle(RxVoid.Default);
} }
catch (UnhandledInteractionException<Unit, Unit>) catch (UnhandledInteractionException<RxVoid, RxVoid>)
{ {
} }
} }

View File

@@ -1,17 +1,17 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class ProfilesViewModel : MyReactiveObject public partial class ProfilesViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new(); public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<ProfileItem, bool> SaveFileDialogInteraction { get; } = new(); public Interaction<ProfileItem, bool> SaveFileDialogInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new(); public Interaction<string, RxVoid> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new(); public Interaction<RxVoid, RxVoid> ProfilesFocusInteraction { get; } = new();
public Interaction<string, Unit> ShareServerInteraction { get; } = new(); public Interaction<string, RxVoid> ShareServerInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshServersBizInteraction { get; } = new(); public Interaction<RxVoid, RxVoid> DispatcherRefreshServersBizInteraction { get; } = new();
public Interaction<Unit, Unit> AdjustMainLvColWidthInteraction { get; } = new(); public Interaction<RxVoid, RxVoid> AdjustMainLvColWidthInteraction { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new(); public EventChannel<RxVoid> ReloadRequested { get; } = new();
public EventChannel<Unit> RefreshServersRequested { get; } = new(); public EventChannel<RxVoid> RefreshServersRequested { get; } = new();
#region private prop #region private prop
@@ -25,69 +25,69 @@ public class ProfilesViewModel : MyReactiveObject
#region ObservableCollection #region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>(); public BulkObservableCollection<ProfileItemModel> ProfileItems { get; } = [];
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>(); public BulkObservableCollection<SubItem> SubItems { get; } = [];
[Reactive] [Reactive]
public ProfileItemModel SelectedProfile { get; set; } public partial ProfileItemModel SelectedProfile { get; set; }
public IList<ProfileItemModel> SelectedProfiles { get; set; } public IList<ProfileItemModel> SelectedProfiles { get; set; }
[Reactive] [Reactive]
public SubItem SelectedSub { get; set; } public partial SubItem SelectedSub { get; set; }
[Reactive] [Reactive]
public SubItem SelectedMoveToGroup { get; set; } public partial SubItem SelectedMoveToGroup { get; set; }
[Reactive] [Reactive]
public string ServerFilter { get; set; } public partial string ServerFilter { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
#region Menu #region Menu
//servers delete //servers delete
public ReactiveCommand<Unit, Unit> EditServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> EditServerCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RemoveServerCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveDuplicateServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RemoveDuplicateServerCmd { get; }
public ReactiveCommand<Unit, Unit> CopyServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> CopyServerCmd { get; }
public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SetDefaultServerCmd { get; }
public ReactiveCommand<Unit, Unit> ShareServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ShareServerCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupAllServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> GenGroupAllServerCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupRegionServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> GenGroupRegionServerCmd { get; }
//servers move //servers move
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveTopCmd { get; }
public ReactiveCommand<Unit, Unit> MoveUpCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveUpCmd { get; }
public ReactiveCommand<Unit, Unit> MoveDownCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveDownCmd { get; }
public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveBottomCmd { get; }
public ReactiveCommand<SubItem, Unit> MoveToGroupCmd { get; } public ReactiveCommand<SubItem, RxVoid> MoveToGroupCmd { get; }
//servers ping //servers ping
public ReactiveCommand<Unit, Unit> MixedTestServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MixedTestServerCmd { get; }
public ReactiveCommand<Unit, Unit> TcpingServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> TcpingServerCmd { get; }
public ReactiveCommand<Unit, Unit> RealPingServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RealPingServerCmd { get; }
public ReactiveCommand<Unit, Unit> UdpTestServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> UdpTestServerCmd { get; }
public ReactiveCommand<Unit, Unit> SpeedServerCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SpeedServerCmd { get; }
public ReactiveCommand<Unit, Unit> SortServerResultCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SortServerResultCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveInvalidServerResultCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RemoveInvalidServerResultCmd { get; }
public ReactiveCommand<Unit, Unit> FastRealPingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> FastRealPingCmd { get; }
//servers export //servers export
public ReactiveCommand<Unit, Unit> Export2ClientConfigCmd { get; } public ReactiveCommand<RxVoid, RxVoid> Export2ClientConfigCmd { get; }
public ReactiveCommand<Unit, Unit> Export2ClientConfigClipboardCmd { get; } public ReactiveCommand<RxVoid, RxVoid> Export2ClientConfigClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> Export2ShareUrlCmd { get; } public ReactiveCommand<RxVoid, RxVoid> Export2ShareUrlCmd { get; }
public ReactiveCommand<Unit, Unit> Export2ShareUrlBase64Cmd { get; } public ReactiveCommand<RxVoid, RxVoid> Export2ShareUrlBase64Cmd { get; }
public ReactiveCommand<Unit, Unit> Export2InnerUriCmd { get; } public ReactiveCommand<RxVoid, RxVoid> Export2InnerUriCmd { get; }
public ReactiveCommand<Unit, Unit> AddSubCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddSubCmd { get; }
public ReactiveCommand<Unit, Unit> EditSubCmd { get; } public ReactiveCommand<RxVoid, RxVoid> EditSubCmd { get; }
public ReactiveCommand<Unit, Unit> DeleteSubCmd { get; } public ReactiveCommand<RxVoid, RxVoid> DeleteSubCmd { get; }
#endregion Menu #endregion Menu
@@ -347,9 +347,9 @@ public class ProfilesViewModel : MyReactiveObject
try try
{ {
await ProfilesFocusInteraction.Handle(Unit.Default); await ProfilesFocusInteraction.Handle(RxVoid.Default);
} }
catch (UnhandledInteractionException<Unit, Unit>) catch (UnhandledInteractionException<RxVoid, RxVoid>)
{ {
} }
} }
@@ -397,9 +397,9 @@ public class ProfilesViewModel : MyReactiveObject
try try
{ {
await DispatcherRefreshServersBizInteraction.Handle(Unit.Default); await DispatcherRefreshServersBizInteraction.Handle(RxVoid.Default);
} }
catch (UnhandledInteractionException<Unit, Unit>) catch (UnhandledInteractionException<RxVoid, RxVoid>)
{ {
} }
} }
@@ -419,7 +419,7 @@ public class ProfilesViewModel : MyReactiveObject
public async Task AdjustMainLvColWidth() public async Task AdjustMainLvColWidth()
{ {
await AdjustMainLvColWidthInteraction.Handle(Unit.Default); await AdjustMainLvColWidthInteraction.Handle(RxVoid.Default);
} }
private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter) private async Task<List<ProfileItemModel>?> GetProfileItemsEx(string subid, string filter)
@@ -503,7 +503,7 @@ public class ProfilesViewModel : MyReactiveObject
var eConfigType = item.ConfigType; var eConfigType = item.ConfigType;
bool? ret = false; bool? ret = false;
if (eConfigType == EConfigType.Custom) if (eConfigType is EConfigType.Custom or EConfigType.Outbound)
{ {
var addServer2ViewModel = new AddServer2ViewModel(item); var addServer2ViewModel = new AddServer2ViewModel(item);
ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel); ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel);
@@ -761,10 +761,9 @@ public class ProfilesViewModel : MyReactiveObject
_speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) =>
{ {
RxSchedulers.MainThreadScheduler.Schedule(result, (scheduler, result) => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
_ = SetSpeedTestResult(result); _ = SetSpeedTestResult(result);
return Disposable.Empty;
}); });
await Task.CompletedTask; await Task.CompletedTask;
}); });

View File

@@ -1,6 +1,6 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable public partial class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
@@ -8,25 +8,25 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
public IList<string> InboundTagItems { get; set; } public IList<string> InboundTagItems { get; set; }
[Reactive] [Reactive]
public RulesItem SelectedSource { get; set; } public partial RulesItem SelectedSource { get; set; }
[Reactive] [Reactive]
public string Domain { get; set; } public partial string Domain { get; set; }
[Reactive] [Reactive]
public string IP { get; set; } public partial string IP { get; set; }
[Reactive] [Reactive]
public string Process { get; set; } public partial string Process { get; set; }
[Reactive] [Reactive]
public string? RuleType { get; set; } public partial string? RuleType { get; set; }
[Reactive] [Reactive]
public bool AutoSort { get; set; } public partial bool AutoSort { get; set; }
public ReactiveCommand<Unit, Unit> SelectProfileCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SelectProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public RoutingRuleDetailsViewModel(RulesItem rulesItem) public RoutingRuleDetailsViewModel(RulesItem rulesItem)
{ {

View File

@@ -1,38 +1,38 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable public partial class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
public Interaction<string, bool> ShowYesNoInteraction { get; } = new(); public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new(); public Interaction<string, RxVoid> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new(); public Interaction<RxVoid, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, string?> BrowseRulesFileInteraction { get; } = new(); public Interaction<RxVoid, string?> BrowseRulesFileInteraction { get; } = new();
private List<RulesItem> _rules; private List<RulesItem> _rules;
[Reactive] [Reactive]
public RoutingItem SelectedRouting { get; set; } public partial RoutingItem SelectedRouting { get; set; }
public IObservableCollection<RulesItemModel> RulesItems { get; } = new ObservableCollectionExtended<RulesItemModel>(); public BulkObservableCollection<RulesItemModel> RulesItems { get; } = [];
[Reactive] [Reactive]
public RulesItemModel SelectedSource { get; set; } public partial RulesItemModel SelectedSource { get; set; }
public IList<RulesItemModel> SelectedSources { get; set; } public IList<RulesItemModel> SelectedSources { get; set; }
public ReactiveCommand<Unit, Unit> RuleAddCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RuleAddCmd { get; }
public ReactiveCommand<Unit, Unit> ImportRulesFromFileCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ImportRulesFromFileCmd { get; }
public ReactiveCommand<Unit, Unit> ImportRulesFromClipboardCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ImportRulesFromClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> ImportRulesFromUrlCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ImportRulesFromUrlCmd { get; }
public ReactiveCommand<Unit, Unit> RuleRemoveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RuleRemoveCmd { get; }
public ReactiveCommand<Unit, Unit> RuleExportSelectedCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RuleExportSelectedCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveTopCmd { get; }
public ReactiveCommand<Unit, Unit> MoveUpCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveUpCmd { get; }
public ReactiveCommand<Unit, Unit> MoveDownCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveDownCmd { get; }
public ReactiveCommand<Unit, Unit> MoveBottomCmd { get; } public ReactiveCommand<RxVoid, RxVoid> MoveBottomCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public RoutingRuleSettingViewModel(RoutingItem routingItem) public RoutingRuleSettingViewModel(RoutingItem routingItem)
{ {
@@ -48,7 +48,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
}); });
ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () => ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () =>
{ {
var fileName = await BrowseRulesFileInteraction.Handle(Unit.Default); var fileName = await BrowseRulesFileInteraction.Handle(RxVoid.Default);
await ImportRulesFromFileAsync(fileName); await ImportRulesFromFileAsync(fileName);
}); });
ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () => ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () =>
@@ -277,7 +277,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
var stringData = clipboardData; var stringData = clipboardData;
if (clipboardData == null) if (clipboardData == null)
{ {
var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default); var result = await ReadTextFromClipboardInteraction.Handle(RxVoid.Default);
if (result.IsNullOrEmpty()) if (result.IsNullOrEmpty())
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationFailed); NoticeManager.Instance.Enqueue(ResUI.OperationFailed);

View File

@@ -1,28 +1,28 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class RoutingSettingViewModel : MyReactiveObject public partial class RoutingSettingViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new(); public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
#region Reactive #region Reactive
public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>(); public BulkObservableCollection<RoutingItemModel> RoutingItems { get; } = [];
[Reactive] [Reactive]
public RoutingItemModel SelectedSource { get; set; } public partial RoutingItemModel SelectedSource { get; set; }
public IList<RoutingItemModel> SelectedSources { get; set; } public IList<RoutingItemModel> SelectedSources { get; set; }
[Reactive] [Reactive]
public string DomainStrategy { get; set; } public partial string DomainStrategy { get; set; }
[Reactive] [Reactive]
public string DomainStrategy4Singbox { get; set; } public partial string DomainStrategy4Singbox { get; set; }
public ReactiveCommand<Unit, Unit> RoutingAdvancedAddCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RoutingAdvancedAddCmd { get; }
public ReactiveCommand<Unit, Unit> RoutingAdvancedRemoveCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RoutingAdvancedRemoveCmd { get; }
public ReactiveCommand<Unit, Unit> RoutingAdvancedSetDefaultCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RoutingAdvancedSetDefaultCmd { get; }
public ReactiveCommand<Unit, Unit> RoutingAdvancedImportRulesCmd { get; } public ReactiveCommand<RxVoid, RxVoid> RoutingAdvancedImportRulesCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }

View File

@@ -1,10 +1,10 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class StatusBarViewModel : MyReactiveObject public partial class StatusBarViewModel : MyReactiveObject
{ {
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new(); public Interaction<string, RxVoid> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> PasswordInputInteraction { get; } = new(); public Interaction<RxVoid, string?> PasswordInputInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshIconInteraction { get; } = new(); public Interaction<RxVoid, RxVoid> DispatcherRefreshIconInteraction { get; } = new();
public EventChannel<bool> SubscriptionsUpdateRequested { get; } = new(); public EventChannel<bool> SubscriptionsUpdateRequested { get; } = new();
public EventChannel<bool?> ShowHideWindowRequested { get; } = new(); public EventChannel<bool?> ShowHideWindowRequested { get; } = new();
@@ -12,94 +12,94 @@ public class StatusBarViewModel : MyReactiveObject
public static StatusBarViewModel Instance => _instance.Value; public static StatusBarViewModel Instance => _instance.Value;
public EventChannel<string> SetDefaultServerRequested { get; } = new(); public EventChannel<string> SetDefaultServerRequested { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new(); public EventChannel<RxVoid> ReloadRequested { get; } = new();
public EventChannel<Unit> AddServerViaScanRequested { get; } = new(); public EventChannel<RxVoid> AddServerViaScanRequested { get; } = new();
public EventChannel<Unit> AddServerViaClipboardRequested { get; } = new(); public EventChannel<RxVoid> AddServerViaClipboardRequested { get; } = new();
#region ObservableCollection #region ObservableCollection
public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>(); public BulkObservableCollection<RoutingItem> RoutingItems { get; } = [];
public IObservableCollection<ComboItem> Servers { get; } = new ObservableCollectionExtended<ComboItem>(); public BulkObservableCollection<ComboItem> Servers { get; } = [];
[Reactive] [Reactive]
public RoutingItem SelectedRouting { get; set; } public partial RoutingItem SelectedRouting { get; set; }
[Reactive] [Reactive]
public ComboItem SelectedServer { get; set; } public partial ComboItem SelectedServer { get; set; }
[Reactive] [Reactive]
public bool BlServers { get; set; } public partial bool BlServers { get; set; }
#endregion ObservableCollection #endregion ObservableCollection
public ReactiveCommand<Unit, Unit> AddServerViaClipboardCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddServerViaClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> AddServerViaScanCmd { get; } public ReactiveCommand<RxVoid, RxVoid> AddServerViaScanCmd { get; }
public ReactiveCommand<Unit, Unit> SubUpdateCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> SubUpdateViaProxyCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubUpdateViaProxyCmd { get; }
public ReactiveCommand<Unit, Unit> CopyProxyCmdToClipboardCmd { get; } public ReactiveCommand<RxVoid, RxVoid> CopyProxyCmdToClipboardCmd { get; }
public ReactiveCommand<Unit, Unit> NotifyLeftClickCmd { get; } public ReactiveCommand<RxVoid, RxVoid> NotifyLeftClickCmd { get; }
public ReactiveCommand<Unit, Unit> ShowWindowCmd { get; } public ReactiveCommand<RxVoid, RxVoid> ShowWindowCmd { get; }
public ReactiveCommand<Unit, Unit> HideWindowCmd { get; } public ReactiveCommand<RxVoid, RxVoid> HideWindowCmd { get; }
#region System Proxy #region System Proxy
[Reactive] [Reactive]
public bool BlSystemProxyClear { get; set; } public partial bool BlSystemProxyClear { get; set; }
[Reactive] [Reactive]
public bool BlSystemProxySet { get; set; } public partial bool BlSystemProxySet { get; set; }
[Reactive] [Reactive]
public bool BlSystemProxyNothing { get; set; } public partial bool BlSystemProxyNothing { get; set; }
[Reactive] [Reactive]
public bool BlSystemProxyPac { get; set; } public partial bool BlSystemProxyPac { get; set; }
public ReactiveCommand<Unit, Unit> SystemProxyClearCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SystemProxyClearCmd { get; }
public ReactiveCommand<Unit, Unit> SystemProxySetCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SystemProxySetCmd { get; }
public ReactiveCommand<Unit, Unit> SystemProxyNothingCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SystemProxyNothingCmd { get; }
public ReactiveCommand<Unit, Unit> SystemProxyPacCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SystemProxyPacCmd { get; }
[Reactive] [Reactive]
public bool BlRouting { get; set; } public partial bool BlRouting { get; set; }
[Reactive] [Reactive]
public int SystemProxySelected { get; set; } public partial int SystemProxySelected { get; set; }
[Reactive] [Reactive]
public bool BlSystemProxyPacVisible { get; set; } public partial bool BlSystemProxyPacVisible { get; set; }
#endregion System Proxy #endregion System Proxy
#region UI #region UI
[Reactive] [Reactive]
public string InboundDisplay { get; set; } public partial string InboundDisplay { get; set; }
[Reactive] [Reactive]
public string InboundLanDisplay { get; set; } public partial string InboundLanDisplay { get; set; }
[Reactive] [Reactive]
public string RunningServerDisplay { get; set; } public partial string RunningServerDisplay { get; set; }
[Reactive] [Reactive]
public string RunningServerToolTipText { get; set; } public partial string RunningServerToolTipText { get; set; }
[Reactive] [Reactive]
public string RunningInfoDisplay { get; set; } public partial string RunningInfoDisplay { get; set; }
[Reactive] [Reactive]
public string SpeedProxyDisplay { get; set; } public partial string SpeedProxyDisplay { get; set; }
[Reactive] [Reactive]
public string SpeedDirectDisplay { get; set; } public partial string SpeedDirectDisplay { get; set; }
[Reactive] [Reactive]
public bool EnableTun { get; set; } public partial bool EnableTun { get; set; }
[Reactive] [Reactive]
public bool BlIsNonWindows { get; set; } public partial bool BlIsNonWindows { get; set; }
#endregion UI #endregion UI
@@ -349,10 +349,9 @@ public class StatusBarViewModel : MyReactiveObject
private async Task TestServerAvailabilitySub(string msg) private async Task TestServerAvailabilitySub(string msg)
{ {
RxSchedulers.MainThreadScheduler.Schedule(msg, (scheduler, msg) => RxSchedulers.MainThreadScheduler.Schedule(() =>
{ {
_ = TestServerAvailabilityResult(msg); _ = TestServerAvailabilityResult(msg);
return Disposable.Empty;
}); });
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -392,9 +391,9 @@ public class StatusBarViewModel : MyReactiveObject
{ {
try try
{ {
await DispatcherRefreshIconInteraction.Handle(Unit.Default); await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
} }
catch (UnhandledInteractionException<Unit, Unit>) catch (UnhandledInteractionException<RxVoid, RxVoid>)
{ {
// Ignore // Ignore
} }
@@ -433,7 +432,7 @@ public class StatusBarViewModel : MyReactiveObject
{ {
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
ReloadRequested.Publish(); ReloadRequested.Publish();
await DispatcherRefreshIconInteraction.Handle(Unit.Default); await DispatcherRefreshIconInteraction.Handle(RxVoid.Default);
} }
} }
@@ -470,7 +469,7 @@ public class StatusBarViewModel : MyReactiveObject
} }
else else
{ {
var password = await PasswordInputInteraction.Handle(Unit.Default); var password = await PasswordInputInteraction.Handle(RxVoid.Default);
if (password.IsNullOrEmpty()) if (password.IsNullOrEmpty())
{ {
_config.TunModeItem.EnableTun = false; _config.TunModeItem.EnableTun = false;

View File

@@ -1,15 +1,18 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class SubEditViewModel : MyReactiveObject, ICloseable public partial class SubEditViewModel : MyReactiveObject, ICloseable
{ {
public event EventHandler? RequestClose; public event EventHandler? RequestClose;
[Reactive] [Reactive]
public SubItem SelectedSource { get; set; } public partial SubItem SelectedSource { get; set; }
public ReactiveCommand<Unit, Unit> SelectPrevProfileCmd { get; } [Reactive]
public ReactiveCommand<Unit, Unit> SelectNextProfileCmd { get; } public partial string CustomCoreType { get; set; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> SelectPrevProfileCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> SelectNextProfileCmd { get; }
public ReactiveCommand<RxVoid, RxVoid> SaveCmd { get; }
public SubEditViewModel(SubItem subItem) public SubEditViewModel(SubItem subItem)
{ {
@@ -39,6 +42,7 @@ public class SubEditViewModel : MyReactiveObject, ICloseable
}); });
SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem); SelectedSource = subItem.Id.IsNullOrEmpty() ? subItem : JsonUtils.DeepCopy(subItem);
CustomCoreType = SelectedSource.CustomCoreType?.ToString() ?? string.Empty;
} }
private async Task SaveSubAsync() private async Task SaveSubAsync()
@@ -67,6 +71,8 @@ public class SubEditViewModel : MyReactiveObject, ICloseable
} }
} }
SelectedSource.CustomCoreType = Enum.TryParse<ECoreType>(CustomCoreType, out var coreType) ? coreType : null;
if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0)
{ {
NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess);

View File

@@ -1,21 +1,21 @@
namespace ServiceLib.ViewModels; namespace ServiceLib.ViewModels;
public class SubSettingViewModel : MyReactiveObject public partial class SubSettingViewModel : MyReactiveObject
{ {
public Interaction<string, bool> ShowYesNoInteraction { get; } = new(); public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> ShareSubInteraction { get; } = new(); public Interaction<string, RxVoid> ShareSubInteraction { get; } = new();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>(); public BulkObservableCollection<SubItem> SubItems { get; } = [];
[Reactive] [Reactive]
public SubItem SelectedSource { get; set; } public partial SubItem SelectedSource { get; set; }
public IList<SubItem> SelectedSources { get; set; } public IList<SubItem> SelectedSources { get; set; }
public ReactiveCommand<Unit, Unit> SubAddCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubAddCmd { get; }
public ReactiveCommand<Unit, Unit> SubDeleteCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubDeleteCmd { get; }
public ReactiveCommand<Unit, Unit> SubEditCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubEditCmd { get; }
public ReactiveCommand<Unit, Unit> SubShareCmd { get; } public ReactiveCommand<RxVoid, RxVoid> SubShareCmd { get; }
public bool IsModified { get; set; } public bool IsModified { get; set; }
public SubSettingViewModel() public SubSettingViewModel()

View File

@@ -35,11 +35,16 @@ public class WindowBase<TViewModel> : ReactiveWindow<TViewModel> where TViewMode
var width = Math.Min(sizeItem.Width, workingArea.Width / scaling); var width = Math.Min(sizeItem.Width, workingArea.Width / scaling);
var height = Math.Min(sizeItem.Height, workingArea.Height / scaling); var height = Math.Min(sizeItem.Height, workingArea.Height / scaling);
var x = workingArea.X + ((workingArea.Width - (width * scaling)) / 2);
var y = workingArea.Y + ((workingArea.Height - (height * scaling)) / 2);
Width = width; Width = width;
Height = height; Height = height;
var frameDiff = (FrameSize ?? ClientSize) - ClientSize;
var totalWidth = (width + frameDiff.Width) * scaling;
var totalHeight = (height + frameDiff.Height) * scaling;
var x = workingArea.X + ((workingArea.Width - totalWidth) / 2);
var y = workingArea.Y + ((workingArea.Height - totalHeight) / 2);
Position = new PixelPoint((int)x, (int)y); Position = new PixelPoint((int)x, (int)y);
} }
catch { } catch { }

View File

@@ -1,3 +0,0 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@@ -3,9 +3,6 @@ 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.Linq;
global using System.Runtime.Versioning; global using System.Runtime.Versioning;
global using System.Text; global using System.Text;
global using System.Threading; global using System.Threading;
@@ -21,10 +18,11 @@ global using Avalonia.Media.Imaging;
global using Avalonia.Platform; global using Avalonia.Platform;
global using Avalonia.Styling; global using Avalonia.Styling;
global using Avalonia.Threading; global using Avalonia.Threading;
global using DynamicData;
global using ReactiveUI; global using ReactiveUI;
global using ReactiveUI.Avalonia; global using ReactiveUI.Avalonia;
global using ReactiveUI.Fody.Helpers; global using ReactiveUI.Primitives;
global using ReactiveUI.Primitives.Disposables;
global using ReactiveUI.SourceGenerators;
global using ServiceLib; global using ServiceLib;
global using ServiceLib.Base; global using ServiceLib.Base;
global using ServiceLib.Common; global using ServiceLib.Common;

View File

@@ -5,13 +5,13 @@ using Semi.Avalonia;
namespace v2rayN.Desktop.ViewModels; namespace v2rayN.Desktop.ViewModels;
public class ThemeSettingViewModel : MyReactiveObject public partial class ThemeSettingViewModel : MyReactiveObject
{ {
[Reactive] public string CurrentTheme { get; set; } [Reactive] public partial string CurrentTheme { get; set; }
[Reactive] public int CurrentFontSize { get; set; } [Reactive] public partial int CurrentFontSize { get; set; }
[Reactive] public string CurrentLanguage { get; set; } [Reactive] public partial string CurrentLanguage { get; set; }
public ThemeSettingViewModel() public ThemeSettingViewModel()
{ {

View File

@@ -27,7 +27,7 @@ public partial class AddGroupServerWindow : WindowBase<AddGroupServerViewModel>
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(v => v.ViewModel.SelectedSource) this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.WhereNotNull() .KeepNotNull()
.Subscribe(InitializeData) .Subscribe(InitializeData)
.DisposeWith(disposables); .DisposeWith(disposables);

View File

@@ -103,57 +103,102 @@
HorizontalAlignment="Left" HorizontalAlignment="Left"
MaxDropDownHeight="1000" /> MaxDropDownHeight="1000" />
<TextBlock <Separator
Grid.Row="4" Grid.Row="4"
Grid.Column="0" Grid.Column="0"
Margin="{StaticResource Margin4}" Grid.ColumnSpan="3"
VerticalAlignment="Center" Margin="{StaticResource Margin4}" />
Text="{x:Static resx:ResUI.TbDisplayLog}" />
<StackPanel <Grid
Grid.Row="4" x:Name="gridCustomServer"
Grid.Column="1" Grid.Row="5"
Margin="{StaticResource Margin4}" Grid.Column="0"
Orientation="Horizontal"> Grid.ColumnSpan="3"
ColumnDefinitions="Auto,Auto,Auto"
RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbDisplayLog}" />
<StackPanel
Grid.Row="0"
Grid.Column="1"
Margin="{StaticResource Margin4}"
Orientation="Horizontal">
<ToggleSwitch
x:Name="togDisplayLog"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<TextBlock
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipDisplayLog}" />
</StackPanel>
<TextBlock
Grid.Row="1"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="1"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<StackPanel
Grid.Row="2"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="{StaticResource Margin4}">
<TextBlock
Width="500"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipPreSocksPort}"
TextWrapping="Wrap" />
<TextBlock
Width="500"
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.CustomServerTips}"
TextWrapping="Wrap" />
</StackPanel>
</Grid>
<Grid
x:Name="gridCustomOutbound"
Grid.Row="5"
Grid.Column="0"
Grid.ColumnSpan="3"
ColumnDefinitions="Auto,Auto,Auto"
IsVisible="False"
RowDefinitions="Auto,Auto,Auto">
<TextBlock
Grid.Row="0"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="Sing-Box Endpoint" />
<ToggleSwitch <ToggleSwitch
x:Name="togDisplayLog" x:Name="togSingBoxEndpoint"
Grid.Row="0"
Grid.Column="1"
Margin="{StaticResource Margin4}" Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<TextBlock <TextBlock
Margin="{StaticResource MarginLr8}" Grid.Row="1"
VerticalAlignment="Center" Grid.Column="1"
Text="{x:Static resx:ResUI.TipDisplayLog}" /> Grid.ColumnSpan="2"
</StackPanel>
<TextBlock
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource Margin4}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TbPreSocksPort}" />
<TextBox
x:Name="txtPreSocksPort"
Grid.Row="5"
Grid.Column="1"
Width="200"
Margin="{StaticResource Margin4}"
HorizontalAlignment="Left" />
<StackPanel
Grid.Row="6"
Grid.Column="1"
Grid.ColumnSpan="2"
Margin="{StaticResource Margin4}">
<TextBlock
Width="500" Width="500"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="{x:Static resx:ResUI.TipPreSocksPort}" Text="{x:Static resx:ResUI.TbCustomOutboundTip}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
<TextBlock </Grid>
Width="500"
Margin="{StaticResource MarginLr8}"
VerticalAlignment="Center"
Text="{x:Static resx:ResUI.CustomServerTips}"
TextWrapping="Wrap" />
</StackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>
</DockPanel> </DockPanel>

View File

@@ -12,15 +12,19 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
Loaded += Window_Loaded; Loaded += Window_Loaded;
btnCancel.Click += (s, e) => Close(); btnCancel.Click += (s, e) => Close();
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty();
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.KeepNotNull()
.Subscribe(InitializeData)
.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Address, v => v.txtAddress.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Address, v => v.txtAddress.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.DisplayLog, v => v.togDisplayLog.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.DisplayLog, v => v.togDisplayLog.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.PreSocksPort, v => v.txtPreSocksPort.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.IsSingboxEndpoint, v => v.togSingBoxEndpoint.IsChecked).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.BrowseServerCmd, v => v.btnBrowse).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.BrowseServerCmd, v => v.btnBrowse).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.EditServerCmd, v => v.btnEdit).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.EditServerCmd, v => v.btnEdit).DisposeWith(disposables);
@@ -34,6 +38,24 @@ public partial class AddServer2Window : WindowBase<AddServer2ViewModel>
}); });
} }
private void InitializeData(ProfileItem profileItem)
{
if (profileItem.ConfigType is EConfigType.Custom)
{
Title = ResUI.menuAddCustomServer;
cmbCoreType.ItemsSource = Utils.GetEnumNames<ECoreType>().Where(t => t != nameof(ECoreType.v2rayN)).ToList();
gridCustomServer.IsVisible = true;
gridCustomOutbound.IsVisible = false;
}
else if (profileItem.ConfigType is EConfigType.Outbound)
{
Title = ResUI.menuAddCustomOutboundServer;
cmbCoreType.ItemsSource = Global.CoreTypes;
gridCustomServer.IsVisible = false;
gridCustomOutbound.IsVisible = true;
}
}
private void Window_Loaded(object? sender, RoutedEventArgs e) private void Window_Loaded(object? sender, RoutedEventArgs e)
{ {
txtRemarks.Focus(); txtRemarks.Focus();

View File

@@ -1,4 +1,3 @@
using System.Reactive.Disposables;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
@@ -39,11 +38,12 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(v => v.ViewModel.SelectedSource) this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.WhereNotNull() .KeepNotNull()
.Subscribe(InitializeData) .Subscribe(InitializeData)
.DisposeWith(disposables); .DisposeWith(disposables);
var configTypeBindings = new SerialDisposable().DisposeWith(disposables); var configTypeBindings = new SingleReplaceableDisposable();
configTypeBindings.DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables);
@@ -53,8 +53,8 @@ public partial class AddServerWindow : WindowBase<AddServerViewModel>
this.WhenAnyValue(v => v.ViewModel.SelectedSource.ConfigType) this.WhenAnyValue(v => v.ViewModel.SelectedSource.ConfigType)
.Subscribe(configType => .Subscribe(configType =>
{ {
var currentTypeDisposables = new CompositeDisposable(); var currentTypeDisposables = new MultipleDisposable();
configTypeBindings.Disposable = currentTypeDisposables; configTypeBindings.Create(currentTypeDisposables);
switch (configType) switch (configType)
{ {

View File

@@ -33,9 +33,11 @@
Header="{x:Static resx:ResUI.menuAddServerViaScan}" Header="{x:Static resx:ResUI.menuAddServerViaScan}"
InputGesture="Ctrl+S" /> InputGesture="Ctrl+S" />
<MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" /> <MenuItem x:Name="menuAddServerViaImage" Header="{x:Static resx:ResUI.menuAddServerViaImage}" />
<Separator />
<MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" /> <MenuItem x:Name="menuAddCustomServer" Header="{x:Static resx:ResUI.menuAddCustomServer}" />
<MenuItem x:Name="menuAddPolicyGroupServer" Header="{x:Static resx:ResUI.menuAddPolicyGroupServer}" /> <MenuItem x:Name="menuAddPolicyGroupServer" Header="{x:Static resx:ResUI.menuAddPolicyGroupServer}" />
<MenuItem x:Name="menuAddProxyChainServer" Header="{x:Static resx:ResUI.menuAddProxyChainServer}" /> <MenuItem x:Name="menuAddProxyChainServer" Header="{x:Static resx:ResUI.menuAddProxyChainServer}" />
<MenuItem x:Name="menuAddCustomOutboundServer" Header="{x:Static resx:ResUI.menuAddCustomOutboundServer}" />
<Separator /> <Separator />
<MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" /> <MenuItem x:Name="menuAddVmessServer" Header="{x:Static resx:ResUI.menuAddVmessServer}" />
<MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" /> <MenuItem x:Name="menuAddVlessServer" Header="{x:Static resx:ResUI.menuAddVlessServer}" />

View File

@@ -1,4 +1,3 @@
using System.Reactive.Disposables;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using DialogHostAvalonia; using DialogHostAvalonia;
using v2rayN.Desktop.Base; using v2rayN.Desktop.Base;
@@ -10,7 +9,7 @@ namespace v2rayN.Desktop.Views;
public partial class MainWindow : WindowBase<MainWindowViewModel> public partial class MainWindow : WindowBase<MainWindowViewModel>
{ {
private static Config _config; private static Config _config;
private readonly SerialDisposable _layoutBindingsDisposable = new(); private readonly SingleReplaceableDisposable _layoutBindingsDisposable = new();
private readonly WindowNotificationManager? _manager; private readonly WindowNotificationManager? _manager;
private CheckUpdateView? _checkUpdateView; private CheckUpdateView? _checkUpdateView;
private BackupAndRestoreView? _backupAndRestoreView; private BackupAndRestoreView? _backupAndRestoreView;
@@ -48,6 +47,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddAnytlsServerCmd, v => v.menuAddAnytlsServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddNaiveServerCmd, v => v.menuAddNaiveServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddCustomServerCmd, v => v.menuAddCustomServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddCustomOutboundServerCmd, v => v.menuAddCustomOutboundServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddPolicyGroupServerCmd, v => v.menuAddPolicyGroupServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddProxyChainServerCmd, v => v.menuAddProxyChainServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.AddServerViaClipboardCmd, v => v.menuAddServerViaClipboard).DisposeWith(disposables);
@@ -111,7 +111,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
ViewModel.ShowHideWindowInteraction.RegisterHandler(interaction => ViewModel.ShowHideWindowInteraction.RegisterHandler(interaction =>
{ {
ShowHideWindow(interaction.Input); ShowHideWindow(interaction.Input);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
AppEvents.SendSnackMsgRequested AppEvents.SendSnackMsgRequested
@@ -402,8 +402,8 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
private void UpdateLayout(EGirdOrientation orientation) private void UpdateLayout(EGirdOrientation orientation)
{ {
var currentLayoutDisposables = new CompositeDisposable(); var currentLayoutDisposables = new MultipleDisposable();
_layoutBindingsDisposable.Disposable = currentLayoutDisposables; _layoutBindingsDisposable.Create(currentLayoutDisposables);
gridMain.IsVisible = orientation == EGirdOrientation.Horizontal; gridMain.IsVisible = orientation == EGirdOrientation.Horizontal;
gridMain1.IsVisible = orientation == EGirdOrientation.Vertical; gridMain1.IsVisible = orientation == EGirdOrientation.Vertical;

View File

@@ -21,8 +21,10 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
var msg = interaction.Input; var msg = interaction.Input;
Dispatcher.UIThread.Post(() => ShowMsg(msg), Dispatcher.UIThread.Post(() => ShowMsg(msg),
DispatcherPriority.ApplicationIdle); DispatcherPriority.ApplicationIdle);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel?.FlushQueueMsg();
}); });
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary( TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(

View File

@@ -33,8 +33,8 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
cmbMtu.ItemsSource = Global.TunMtus; cmbMtu.ItemsSource = Global.TunMtus;
cmbStack.ItemsSource = Global.TunStacks; cmbStack.ItemsSource = Global.TunStacks;
cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies; cmbIcmpRoutingPolicy.ItemsSource = Global.TunIcmpRoutingPolicies;
cmbIpv4Address.ItemsSource = Global.TunIpv4Address; cmbIpv4Address.ItemsSource = Global.TunIPv4Address;
cmbIpv6Address.ItemsSource = Global.TunIpv6Address; cmbIpv6Address.ItemsSource = Global.TunIPv6Address;
cmbFragmentPackets.ItemsSource = Global.FragmentPacketsOptions; cmbFragmentPackets.ItemsSource = Global.FragmentPacketsOptions;
cmbCoreType1.ItemsSource = Global.CoreTypes; cmbCoreType1.ItemsSource = Global.CoreTypes;
@@ -109,8 +109,7 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.UdpTestTarget, v => v.cmbUdpTestTarget.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.UdpTestTarget, v => v.cmbUdpTestTarget.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.MixedConcurrencyCount, v => v.cmbMixedConcurrencyCount.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.MixedConcurrencyCount, v => v.cmbMixedConcurrencyCount.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SubConvertUrl, v => v.cmbSubConvertUrl.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SubConvertUrl, v => v.cmbSubConvertUrl.Text).DisposeWith(disposables);
this.Bind<OptionSettingViewModel, OptionSettingWindow, int, int>(ViewModel, this.Bind(ViewModel, vm => vm.MainGirdOrientation, view => view.cmbMainGirdOrientation.SelectedIndex).DisposeWith(disposables);
vm => vm.MainGirdOrientation, view => view.cmbMainGirdOrientation.SelectedIndex).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.GeoFileSourceUrl, v => v.cmbGetFilesSourceUrl.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.GeoFileSourceUrl, v => v.cmbGetFilesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.SrsFileSourceUrl, v => v.cmbSrsFilesSourceUrl.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SrsFileSourceUrl, v => v.cmbSrsFilesSourceUrl.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.RoutingRulesSourceUrl, v => v.cmbRoutingRulesSourceUrl.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.RoutingRulesSourceUrl, v => v.cmbRoutingRulesSourceUrl.Text).DisposeWith(disposables);
@@ -131,8 +130,8 @@ public partial class OptionSettingWindow : WindowBase<OptionSettingViewModel>
this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunIcmpRouting, v => v.cmbIcmpRoutingPolicy.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunEnableLegacyProtect, v => v.togEnableLegacyProtect.IsChecked).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunEnableLegacyProtect, v => v.togEnableLegacyProtect.IsChecked).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunRouteExcludeAddress, v => v.txtRouteExcludeAddress.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunRouteExcludeAddress, v => v.txtRouteExcludeAddress.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunIpv4Address, v => v.cmbIpv4Address.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunIPv4Address, v => v.cmbIpv4Address.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.TunIpv6Address, v => v.cmbIpv6Address.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.TunIPv6Address, v => v.cmbIpv6Address.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType1, v => v.cmbCoreType1.SelectedValue).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.CoreType2, v => v.cmbCoreType2.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CoreType2, v => v.cmbCoreType2.SelectedValue).DisposeWith(disposables);

View File

@@ -35,7 +35,7 @@ public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction => ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction =>
{ {
lstProfiles.Focus(); lstProfiles.Focus();
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
}); });
} }

View File

@@ -1,6 +1,5 @@
using Avalonia.VisualTree; using Avalonia.VisualTree;
using DialogHostAvalonia; using DialogHostAvalonia;
using DynamicData.Binding;
using v2rayN.Desktop.Common; using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views; namespace v2rayN.Desktop.Views;
@@ -113,13 +112,13 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
{ {
var strData = interaction.Input; var strData = interaction.Input;
await AvaUtils.SetClipboardData(this, strData); await AvaUtils.SetClipboardData(this, strData);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction => ViewModel.ProfilesFocusInteraction.RegisterHandler(interaction =>
{ {
lstProfiles.Focus(); lstProfiles.Focus();
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel.ShareServerInteraction.RegisterHandler(async interaction => ViewModel.ShareServerInteraction.RegisterHandler(async interaction =>
@@ -127,23 +126,23 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
var url = interaction.Input; var url = interaction.Input;
if (url.IsNullOrEmpty()) if (url.IsNullOrEmpty())
{ {
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
return; return;
} }
await ShareServer(url); await ShareServer(url);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel.DispatcherRefreshServersBizInteraction.RegisterHandler(interaction => ViewModel.DispatcherRefreshServersBizInteraction.RegisterHandler(interaction =>
{ {
Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default); Dispatcher.UIThread.Post(RefreshServersBiz, DispatcherPriority.Default);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel.AdjustMainLvColWidthInteraction.RegisterHandler(interaction => ViewModel.AdjustMainLvColWidthInteraction.RegisterHandler(interaction =>
{ {
//AutofitColumnWidth(); //AutofitColumnWidth();
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
AppEvents.AppExitRequested AppEvents.AppExitRequested

View File

@@ -22,7 +22,7 @@ public partial class RoutingRuleDetailsWindow : WindowBase<RoutingRuleDetailsVie
this.WhenActivated(disposables => this.WhenActivated(disposables =>
{ {
this.WhenAnyValue(v => v.ViewModel.SelectedSource) this.WhenAnyValue(v => v.ViewModel.SelectedSource)
.WhereNotNull() .KeepNotNull()
.Subscribe(InitializeData) .Subscribe(InitializeData)
.DisposeWith(disposables); .DisposeWith(disposables);

View File

@@ -61,7 +61,7 @@ public partial class RoutingRuleSettingWindow : WindowBase<RoutingRuleSettingVie
{ {
var strData = interaction.Input; var strData = interaction.Input;
await AvaUtils.SetClipboardData(this, strData); await AvaUtils.SetClipboardData(this, strData);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel.ReadTextFromClipboardInteraction.RegisterHandler(async interaction => ViewModel.ReadTextFromClipboardInteraction.RegisterHandler(async interaction =>

View File

@@ -34,7 +34,7 @@ public partial class StatusBarView : ReactiveUserControl<StatusBarViewModel>
{ {
var strData = interaction.Input; var strData = interaction.Input;
await AvaUtils.SetClipboardData(this, strData); await AvaUtils.SetClipboardData(this, strData);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
ViewModel.PasswordInputInteraction.RegisterHandler(async interaction => ViewModel.PasswordInputInteraction.RegisterHandler(async interaction =>
@@ -46,7 +46,7 @@ public partial class StatusBarView : ReactiveUserControl<StatusBarViewModel>
ViewModel.DispatcherRefreshIconInteraction.RegisterHandler(interaction => ViewModel.DispatcherRefreshIconInteraction.RegisterHandler(interaction =>
{ {
Dispatcher.UIThread.Post(RefreshIcon, DispatcherPriority.Default); Dispatcher.UIThread.Post(RefreshIcon, DispatcherPriority.Default);
interaction.SetOutput(Unit.Default); interaction.SetOutput(RxVoid.Default);
}).DisposeWith(disposables); }).DisposeWith(disposables);
}); });

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