diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.cs b/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.cs index 8e67da5e5d0..ad9d3045d34 100644 --- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.cs +++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.cs @@ -1,8 +1,5 @@ -using AppResources = BD.WTTS.Client.Resources.Strings; - using BD.WTTS.UI.Views.Pages; -using STUN.StunResult; -using STUN.Enums; +using AppResources = BD.WTTS.Client.Resources.Strings; namespace BD.WTTS.UI.ViewModels; @@ -78,22 +75,47 @@ public AcceleratorPageViewModel() var natCheckResult = await networkTestService.TestStunClient3489Async(testServerHostName: SelectedSTUNAddress) ?? new ClassicStunResult { NatType = NatType.Unknown }; var (netSucc, _) = await networkTestService.TestOpenUrlAsync("https://www.baidu.com"); - var natStatus = natCheckResult.NatType switch + var publicEndPoint = natCheckResult.PublicEndPoint?.Address.ToString() ?? "Unknown"; + var localEndPoint = natCheckResult.LocalEndPoint?.Address.ToString() ?? "Unknown"; + + var (natLevel, natTypeTip) = natCheckResult.NatType switch { - NatType.OpenInternet or NatType.FullCone => NatTypeSimple.Open, - NatType.RestrictedCone or NatType.PortRestrictedCone or NatType.SymmetricUdpFirewall => NatTypeSimple.Moderate, - NatType.Symmetric or NatType.UdpBlocked => NatTypeSimple.Strict, - NatType.Unknown or NatType.UnsupportedServer or _ => NatTypeSimple.Unknown, + // Open + NatType.OpenInternet or NatType.FullCone => ("开放 NAT", "您可与在其网络上具有任意 NAT 类型的用户玩多人游戏和发起多人游戏。"), + // Moderate + NatType.RestrictedCone or NatType.PortRestrictedCone or NatType.SymmetricUdpFirewall => ("中等 NAT", "您可与一些用户玩多人游戏;但是,并且通常你将不会被选为比赛的主持人。"), + // Strict + NatType.Symmetric or NatType.UdpBlocked => ("严格 NAT", "您只能与具有开放 NAT 类型的用户玩多人游戏。您不能被选为比赛的主持人。"), + // Unknown + NatType.Unknown or NatType.UnsupportedServer or _ => ("不可用 NAT", "如果 NAT 不可用,您将无法使用群聊天或连接到某些 Xbox 游戏的多人游戏。"), }; - PublicEndPoint = natCheckResult.PublicEndPoint?.Address.ToString() ?? "Unknown"; - LocalEndPoint = natCheckResult.LocalEndPoint?.Address.ToString() ?? "Unknown"; - - return (natStatus, netSucc); + return new NATFetchResult(publicEndPoint, localEndPoint, natLevel, natTypeTip, netSucc); }); NATCheckCommand .IsExecuting .ToPropertyEx(this, x => x.IsNATChecking); + NATCheckCommand + .Select(x => x.PublicEndPoint) + .ToPropertyEx(this, x => x.PublicEndPoint); + NATCheckCommand + .Select(x => x.LocalEndPoint) + .ToPropertyEx(this, x => x.LocalEndPoint); + NATCheckCommand + .Select(x => x.NATLevel) + .ToPropertyEx(this, x => x.NATLevel); + NATCheckCommand + .Select(x => x.NATTypeTip) + .ToPropertyEx(this, x => x.NATTypeTip); + + var hidePingResultStream = NATCheckCommand + .IsExecuting + .Where(x => x == true) + .Select(x => PingStatus.Blank); + NATCheckCommand + .Select(x => x.PingResult == true ? PingStatus.Ok : PingStatus.Error) + .Merge(hidePingResultStream) + .ToPropertyEx(this, x => x.PingResultStatus); DNSCheckCommand = ReactiveCommand.CreateFromTask(async () => { diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.props.cs b/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.props.cs index 7ae8707dd4a..3ae539c4be7 100644 --- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.props.cs +++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/AcceleratorPageViewModel.props.cs @@ -4,12 +4,13 @@ namespace BD.WTTS.UI.ViewModels; public sealed partial class AcceleratorPageViewModel : TabItemViewModel { - public enum NatTypeSimple + public record NATFetchResult(string PublicEndPoint, string LocalEndPoint, string NATLevel, string NATTypeTip, bool PingResult); + + public enum PingStatus { - Unknown, - Open, - Moderate, - Strict, + Blank, + Ok, + Error, } public override string Name => Strings.Welcome; @@ -25,6 +26,21 @@ public enum NatTypeSimple "stun.miwifi.com", ]; + [ObservableAsProperty] + public string NATLevel { get; } = string.Empty; + + [ObservableAsProperty] + public string NATTypeTip { get; } = string.Empty; + + [ObservableAsProperty] + public string LocalEndPoint { get; set; } = string.Empty; + + [ObservableAsProperty] + public string PublicEndPoint { get; set; } = string.Empty; + + [ObservableAsProperty] + public PingStatus PingResultStatus { get; } + [ObservableAsProperty] public bool IsNATChecking { get; } @@ -40,12 +56,6 @@ public enum NatTypeSimple [Reactive] public ReadOnlyCollection? EnableProxyDomainGroupVMs { get; set; } - [Reactive] - public string LocalEndPoint { get; set; } = string.Empty; - - [Reactive] - public string PublicEndPoint { get; set; } = string.Empty; - [Reactive] public string DNSTestDelay { get; set; } = string.Empty; @@ -58,7 +68,7 @@ public enum NatTypeSimple [Reactive] public bool IsSupportIPv6 { get; set; } - public ReactiveCommand NATCheckCommand { get; } + public ReactiveCommand NATCheckCommand { get; } public ReactiveCommand DNSCheckCommand { get; } diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/NetworkCheckControlViewModel.cs b/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/NetworkCheckControlViewModel.cs new file mode 100644 index 00000000000..ab7c574cb6a --- /dev/null +++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/ViewModels/NetworkCheckControlViewModel.cs @@ -0,0 +1,183 @@ +using STUN.Enums; +using STUN.StunResult; +using System.Reactive; + +namespace BD.WTTS.UI.ViewModels; + +public class NetworkCheckControlViewModel : ViewModelBase +{ + private readonly INetworkTestService _networkTestService = INetworkTestService.Instance; + + public record NATFetchResult(string PublicEndPoint, string LocalEndPoint, string NATLevel, string NATTypeTip, bool PingResult); + + [Reactive] + public string SelectedSTUNAddress { get; set; } + + public string[] STUNAddress { get; } = + [ + "stun.syncthing.net", + "stun.hot-chilli.net", + "stun.fitauto.ru", + "stun.miwifi.com", + ]; + + [ObservableAsProperty] + public string NATLevel { get; } = string.Empty; + + [ObservableAsProperty] + public string NATTypeTip { get; } = string.Empty; + + [ObservableAsProperty] + public string LocalEndPoint { get; } = string.Empty; + + [ObservableAsProperty] + public string PublicEndPoint { get; } = string.Empty; + + [ObservableAsProperty] + public bool PingOkVisible { get; } + + [ObservableAsProperty] + public bool PingErrorVisible { get; } + + [ObservableAsProperty] + public bool IsNATChecking { get; } + + [ObservableAsProperty] + public bool IsDNSChecking { get; } + + [ObservableAsProperty] + public bool IsIPv6Checking { get; } + + [Reactive] + public string DomainPendingTest { get; set; } = string.Empty; + + [Reactive] + public string DNSTestDelay { get; set; } = string.Empty; + + [Reactive] + public string DNSTestResult { get; set; } = string.Empty; + + [Reactive] + public string IPv6Address { get; set; } = string.Empty; + + [Reactive] + public bool IsSupportIPv6 { get; set; } + + public ReactiveCommand NATCheckCommand { get; } + + public ReactiveCommand DNSCheckCommand { get; } + + public ReactiveCommand IPv6CheckCommand { get; } + + public NetworkCheckControlViewModel() + { + SelectedSTUNAddress = STUNAddress[0]; + + NATCheckCommand = ReactiveCommand.CreateFromTask(async () => + { + var natCheckResult = await _networkTestService.TestStunClient3489Async(testServerHostName: SelectedSTUNAddress) ?? new ClassicStunResult { NatType = NatType.Unknown }; + var (netSucc, _) = await _networkTestService.TestOpenUrlAsync("https://www.baidu.com"); + + var publicEndPoint = natCheckResult.PublicEndPoint?.Address.ToString() ?? "Unknown"; + var localEndPoint = natCheckResult.LocalEndPoint?.Address.ToString() ?? "Unknown"; + + var (natLevel, natTypeTip) = natCheckResult.NatType switch + { + // Open + NatType.OpenInternet or NatType.FullCone => ("开放 NAT", "您可与在其网络上具有任意 NAT 类型的用户玩多人游戏和发起多人游戏。"), + // Moderate + NatType.RestrictedCone or NatType.PortRestrictedCone or NatType.SymmetricUdpFirewall => ("中等 NAT", "您可与一些用户玩多人游戏;但是,并且通常你将不会被选为比赛的主持人。"), + // Strict + NatType.Symmetric or NatType.UdpBlocked => ("严格 NAT", "您只能与具有开放 NAT 类型的用户玩多人游戏。您不能被选为比赛的主持人。"), + // Unknown + NatType.Unknown or NatType.UnsupportedServer or _ => ("不可用 NAT", "如果 NAT 不可用,您将无法使用群聊天或连接到某些 Xbox 游戏的多人游戏。"), + }; + + return new NATFetchResult(publicEndPoint, localEndPoint, natLevel, natTypeTip, netSucc); + }); + NATCheckCommand + .IsExecuting + .ToPropertyEx(this, x => x.IsNATChecking); + NATCheckCommand + .Select(x => x.PublicEndPoint) + .ToPropertyEx(this, x => x.PublicEndPoint); + NATCheckCommand + .Select(x => x.LocalEndPoint) + .ToPropertyEx(this, x => x.LocalEndPoint); + NATCheckCommand + .Select(x => x.NATLevel) + .Merge(Observable.Return("未知")) + .ToPropertyEx(this, x => x.NATLevel); + NATCheckCommand + .Select(x => x.NATTypeTip) + .Merge(Observable.Return("未知类型")) + .ToPropertyEx(this, x => x.NATTypeTip); + + var hidePingResultStream = NATCheckCommand + .IsExecuting + .Where(x => x == true) + .Select(x => false); + NATCheckCommand + .Select(x => x.PingResult == true) + .Merge(hidePingResultStream) + .ToPropertyEx(this, x => x.PingOkVisible); + NATCheckCommand + .Select(x => x.PingResult == false) + .Merge(hidePingResultStream) + .ToPropertyEx(this, x => x.PingErrorVisible); + + DNSCheckCommand = ReactiveCommand.CreateFromTask(async () => + { + var testDomain = DomainPendingTest == string.Empty ? "store.steampowered.com" : DomainPendingTest; + try + { + long delayMs; + IPAddress[] address; + if (ProxySettings.UseDoh) + { + var configDoh = ProxySettings.CustomDohAddres2.Value ?? ProxySettingsWindowViewModel.DohAddress.FirstOrDefault() ?? string.Empty; + (delayMs, address) = await _networkTestService.TestDNSOverHttpsAsync(testDomain, configDoh); + } + else + { + var configDns = ProxySettings.ProxyMasterDns.Value ?? string.Empty; + (delayMs, address) = await _networkTestService.TestDNSAsync(testDomain, configDns, 53); + } + if (address.Length == 0) + throw new Exception("Parsing failed. Return empty ip address."); + + DNSTestDelay = delayMs + "ms "; + DNSTestResult = string.Empty + address.FirstOrDefault(); + } + catch (Exception ex) + { + Log.Error(nameof(AcceleratorPageViewModel), ex.ToString()); + DNSTestDelay = string.Empty; + DNSTestResult = "error"; + } + }); + DNSCheckCommand + .IsExecuting + .ToPropertyEx(this, x => x.IsDNSChecking); + + IPv6CheckCommand = ReactiveCommand.CreateFromTask(async () => + { + var result = await IMicroServiceClient.Instance.Accelerate.GetMyIP(ipV6: true); + if (result.IsSuccess) + { + IsSupportIPv6 = true; + IPv6Address = result.Content ?? string.Empty; + } + else + { + IsSupportIPv6 = false; + IPv6Address = string.Empty; + } + }); + IPv6CheckCommand + .IsExecuting + .ToPropertyEx(this, x => x.IsIPv6Checking); + + IPv6CheckCommand.Execute().Subscribe(); + } +} \ No newline at end of file diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/NetworkCheck.axaml b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/NetworkCheck.axaml index 952f15c5adf..dd217d38212 100644 --- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/NetworkCheck.axaml +++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Controls/NetworkCheck.axaml @@ -11,7 +11,7 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" d:DesignHeight="450" d:DesignWidth="800" - x:DataType="spp:AcceleratorPageViewModel" + x:DataType="spp:NetworkCheckControlViewModel" mc:Ignorable="d"> @@ -63,14 +62,14 @@ x:Name="NATTextBlock" VerticalAlignment="Center" Foreground="Gray" - Text="未知" /> + Text="{Binding NATLevel}" /> - + @@ -126,13 +125,13 @@ + Glyph="" + IsVisible="{Binding PingOkVisible}" /> - + { public NetworkCheck() { InitializeComponent(); + + Loaded += (_, _) => + { + ViewModel = DataContext as AcceleratorPageViewModel; + + this.OneWayBind(ViewModel, vm => vm.PingResultStatus, v => v.PingOK.IsVisible, result => result == AcceleratorPageViewModel.PingStatus.Ok); + this.OneWayBind(ViewModel, vm => vm.PingResultStatus, v => v.PingError.IsVisible, result => result == AcceleratorPageViewModel.PingStatus.Error); + this.OneWayBind(ViewModel, vm => vm.NATLevel, v => v.NATTextBlock.Text); + this.OneWayBind(ViewModel, vm => vm.NATTypeTip, v => v.NATTypeTip.Text); + }; } + + public AcceleratorPageViewModel? ViewModel { get; set; } + + object? IViewFor.ViewModel { get; set; } } \ No newline at end of file diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml index e286f1c7aee..7315660509e 100644 --- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml +++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml @@ -648,7 +648,7 @@ - + diff --git a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml.cs b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml.cs index ebf109666a8..441eef8035b 100644 --- a/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml.cs +++ b/src/BD.WTTS.Client.Plugins.Accelerator/UI/Views/Pages/AcceleratorPage2.axaml.cs @@ -73,31 +73,6 @@ public AcceleratorPage2() GameAccTab.IsVisible = false; AcceleratorTabs.SelectedIndex = 0; } - - NetworkCheckControl.PingOK.IsVisible = false; - NetworkCheckControl.PingError.IsVisible = false; - NetworkCheckControl.NATCheckButton.Click += (_, _) => NetworkCheckControl.PingError.IsVisible = NetworkCheckControl.PingOK.IsVisible = false; - ViewModel!.NATCheckCommand - .Subscribe(result => - { - (NetworkCheckControl.NATTextBlock.Text, NetworkCheckControl.NATTypeTip.Text) = result.Nat switch - { - AcceleratorPageViewModel.NatTypeSimple.Open => ("开放 NAT", "您可与在其网络上具有任意 NAT 类型的用户玩多人游戏和发起多人游戏。"), - AcceleratorPageViewModel.NatTypeSimple.Moderate => ("中等 NAT", "您可与一些用户玩多人游戏;但是,并且通常你将不会被选为比赛的主持人。"), - AcceleratorPageViewModel.NatTypeSimple.Strict => ("严格 NAT", "您只能与具有开放 NAT 类型的用户玩多人游戏。您不能被选为比赛的主持人。"), - _ => ("不可用 NAT", "如果 NAT 不可用,您将无法使用群聊天或连接到某些 Xbox 游戏的多人游戏。"), - }; - - if (result.PingSuccess) - { - NetworkCheckControl.PingOK.IsVisible = true; - } - else - { - NetworkCheckControl.PingError.IsVisible = true; - } - }).DisposeWith(disposables); - ViewModel.IPv6CheckCommand.Execute().Subscribe(); }); SearchGameBox.DropDownClosed += SearchGameBox_DropDownClosed;