diff --git a/v2rayN/ServiceLib/Base/ICloseable.cs b/v2rayN/ServiceLib/Base/ICloseable.cs new file mode 100644 index 00000000..e181c5e5 --- /dev/null +++ b/v2rayN/ServiceLib/Base/ICloseable.cs @@ -0,0 +1,6 @@ +namespace ServiceLib.Base; + +public interface ICloseable +{ + public event EventHandler? RequestClose; +} diff --git a/v2rayN/ServiceLib/Base/IWindowDialog.cs b/v2rayN/ServiceLib/Base/IWindowDialog.cs new file mode 100644 index 00000000..03a8be92 --- /dev/null +++ b/v2rayN/ServiceLib/Base/IWindowDialog.cs @@ -0,0 +1,7 @@ +namespace ServiceLib.Base; + +public interface IWindowDialog +{ + public Task ShowDialogAsync(TViewModel vm) + where TViewModel : class; +} diff --git a/v2rayN/ServiceLib/Base/MyReactiveObject.cs b/v2rayN/ServiceLib/Base/MyReactiveObject.cs index 50563151..aa0758e9 100644 --- a/v2rayN/ServiceLib/Base/MyReactiveObject.cs +++ b/v2rayN/ServiceLib/Base/MyReactiveObject.cs @@ -3,5 +3,4 @@ namespace ServiceLib.Base; public class MyReactiveObject : ReactiveObject { protected static Config? _config; - protected Func>? _updateView; } diff --git a/v2rayN/ServiceLib/Enums/EViewAction.cs b/v2rayN/ServiceLib/Enums/EViewAction.cs deleted file mode 100644 index fba340dc..00000000 --- a/v2rayN/ServiceLib/Enums/EViewAction.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace ServiceLib.Enums; - -public enum EViewAction -{ - CloseWindow, - ShowYesNo, - SaveFileDialog, - AddBatchRoutingRulesYesNo, - SetClipboardData, - AddServerViaClipboard, - ImportRulesFromClipboard, - ProfilesFocus, - ShareSub, - ShareServer, - ScanScreenTask, - ScanImageTask, - BrowseServer, - ImportRulesFromFile, - InitSettingFont, - PasswordInput, - SubEditWindow, - RoutingRuleSettingWindow, - RoutingRuleDetailsWindow, - AddServerWindow, - AddServer2Window, - AddGroupServerWindow, - DNSSettingWindow, - RoutingSettingWindow, - OptionSettingWindow, - FullConfigTemplateWindow, - GlobalHotkeySettingWindow, - SubSettingWindow, - DispatcherRefreshServersBiz, - DispatcherRefreshIcon, - DispatcherShowMsg, -} diff --git a/v2rayN/ServiceLib/Events/AppEvents.cs b/v2rayN/ServiceLib/Events/AppEvents.cs index 2f97bc6b..c8ba1064 100644 --- a/v2rayN/ServiceLib/Events/AppEvents.cs +++ b/v2rayN/ServiceLib/Events/AppEvents.cs @@ -2,16 +2,9 @@ namespace ServiceLib.Events; public static class AppEvents { - public static readonly EventChannel ReloadRequested = new(); - public static readonly EventChannel ShowHideWindowRequested = new(); - public static readonly EventChannel AddServerViaScanRequested = new(); public static readonly EventChannel AddServerViaClipboardRequested = new(); - public static readonly EventChannel SubscriptionsUpdateRequested = new(); public static readonly EventChannel HasUpdateNotified = new(); - public static readonly EventChannel ProfilesRefreshRequested = new(); - public static readonly EventChannel SubscriptionsRefreshRequested = new(); - public static readonly EventChannel ProxiesReloadRequested = new(); public static readonly EventChannel DispatcherStatisticsRequested = new(); public static readonly EventChannel SendSnackMsgRequested = new(); @@ -20,12 +13,5 @@ public static class AppEvents public static readonly EventChannel AppExitRequested = new(); public static readonly EventChannel ShutdownRequested = new(); - public static readonly EventChannel AdjustMainLvColWidthRequested = new(); - - public static readonly EventChannel SetDefaultServerRequested = new(); - - public static readonly EventChannel RoutingsMenuRefreshRequested = new(); - public static readonly EventChannel TestServerRequested = new(); - public static readonly EventChannel InboundDisplayRequested = new(); public static readonly EventChannel SysProxyChangeRequested = new(); } diff --git a/v2rayN/ServiceLib/Manager/AppManager.cs b/v2rayN/ServiceLib/Manager/AppManager.cs index 5e97e3d5..de16cf35 100644 --- a/v2rayN/ServiceLib/Manager/AppManager.cs +++ b/v2rayN/ServiceLib/Manager/AppManager.cs @@ -10,6 +10,7 @@ public sealed class AppManager private int? _statePort2; public static AppManager Instance => _instance.Value; public Config Config => _config; + public IWindowDialog WindowDialog { get; set; } = null!; public int StatePort { diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index 7a55dde0..41aae3ae 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -1,7 +1,9 @@ namespace ServiceLib.ViewModels; -public class AddGroupServerViewModel : MyReactiveObject +public class AddGroupServerViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + [Reactive] public ProfileItem SelectedSource { get; set; } @@ -29,7 +31,7 @@ public class AddGroupServerViewModel : MyReactiveObject public IObservableCollection AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended(); - //public ReactiveCommand AddCmd { get; } + public ReactiveCommand AddCmd { get; } public ReactiveCommand RemoveCmd { get; } public ReactiveCommand MoveTopCmd { get; } @@ -39,15 +41,18 @@ public class AddGroupServerViewModel : MyReactiveObject public ReactiveCommand SaveCmd { get; } - public AddGroupServerViewModel(ProfileItem profileItem, Func>? updateView) + public AddGroupServerViewModel(ProfileItem profileItem) { _config = AppManager.Instance.Config; - _updateView = updateView; var canEditRemove = this.WhenAnyValue( x => x.SelectedChild, - SelectedChild => SelectedChild != null && !SelectedChild.Remarks.IsNullOrEmpty()); + selectedChild => selectedChild != null && !selectedChild.Remarks.IsNullOrEmpty()); + AddCmd = ReactiveCommand.CreateFromTask(async () => + { + await AddChildAsync(); + }); RemoveCmd = ReactiveCommand.CreateFromTask(async () => { await ChildRemoveAsync(); @@ -103,6 +108,20 @@ public class AddGroupServerViewModel : MyReactiveObject ChildItemsObs.AddRange(childItemList); } + public async Task AddChildAsync() + { + var profileSelectViewModel = new ProfilesSelectViewModel(); + profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true); + profileSelectViewModel.MultiSelect = true; + var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel); + if (result != true) + { + return; + } + var profiles = await profileSelectViewModel.GetProfileItems() ?? []; + ChildItemsObs.AddRange(profiles); + } + public async Task ChildRemoveAsync() { if (SelectedChild == null || SelectedChild.IndexId.IsNullOrEmpty()) @@ -230,7 +249,7 @@ public class AddGroupServerViewModel : MyReactiveObject if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { diff --git a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs index 6df0e049..b66f2118 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServer2ViewModel.cs @@ -1,7 +1,10 @@ namespace ServiceLib.ViewModels; -public class AddServer2ViewModel : MyReactiveObject +public class AddServer2ViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + public Interaction BrowseConfigFileInteraction { get; } = new(); + [Reactive] public ProfileItem SelectedSource { get; set; } @@ -13,15 +16,18 @@ public class AddServer2ViewModel : MyReactiveObject public ReactiveCommand SaveServerCmd { get; } public bool IsModified { get; set; } - public AddServer2ViewModel(ProfileItem profileItem, Func>? updateView) + public AddServer2ViewModel(ProfileItem profileItem) { _config = AppManager.Instance.Config; - _updateView = updateView; BrowseServerCmd = ReactiveCommand.CreateFromTask(async () => { - _updateView?.Invoke(EViewAction.BrowseServer, null); - await Task.CompletedTask; + var fileName = await BrowseConfigFileInteraction.Handle(Unit.Default); + if (fileName.IsNullOrEmpty()) + { + return; + } + await BrowseServer(fileName); }); EditServerCmd = ReactiveCommand.CreateFromTask(async () => { @@ -55,7 +61,7 @@ public class AddServer2ViewModel : MyReactiveObject if (await ConfigHandler.EditCustomServer(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 22be8bc2..536323a7 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -1,7 +1,9 @@ namespace ServiceLib.ViewModels; -public class AddServerViewModel : MyReactiveObject +public class AddServerViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + [Reactive] public ProfileItem SelectedSource { get; set; } @@ -241,10 +243,9 @@ public class AddServerViewModel : MyReactiveObject public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } - public AddServerViewModel(ProfileItem profileItem, Func>? updateView) + public AddServerViewModel(ProfileItem profileItem) { _config = AppManager.Instance.Config; - _updateView = updateView; FetchCertCmd = ReactiveCommand.CreateFromTask(async () => { @@ -443,7 +444,7 @@ public class AddServerViewModel : MyReactiveObject if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { diff --git a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs index dbe3b24d..3b1d544c 100644 --- a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -13,12 +13,11 @@ public class BackupAndRestoreViewModel : MyReactiveObject public WebDavItem SelectedSource { get; set; } [Reactive] - public string OperationMsg { get; set; } + public string OperationMsg { get; set; } = string.Empty; - public BackupAndRestoreViewModel(Func>? updateView) + public BackupAndRestoreViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => { diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index 1db559b4..c8f4efc4 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -7,15 +7,16 @@ public class CheckUpdateViewModel : MyReactiveObject private List _lstUpdated = []; private static readonly string _tag = "CheckUpdateViewModel"; + public EventChannel ReloadRequested { get; } = new(); + public IObservableCollection CheckUpdateModels { get; } = new ObservableCollectionExtended(); public ReactiveCommand CheckUpdateCmd { get; } public ReactiveCommand CheckOnlyCmd { get; } [Reactive] public bool EnableCheckPreReleaseUpdate { get; set; } - public CheckUpdateViewModel(Func>? updateView) + public CheckUpdateViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; CheckUpdateCmd = ReactiveCommand.CreateFromTask(CheckUpdate); CheckUpdateCmd.ThrownExceptions.Subscribe(ex => @@ -299,7 +300,7 @@ public class CheckUpdateViewModel : MyReactiveObject { if (blReload) { - AppEvents.ReloadRequested.Publish(); + ReloadRequested.Publish(); } else { diff --git a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs index fb464870..384f39c4 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashConnectionsViewModel.cs @@ -16,10 +16,9 @@ public class ClashConnectionsViewModel : MyReactiveObject [Reactive] public bool AutoRefresh { get; set; } - public ClashConnectionsViewModel(Func>? updateView) + public ClashConnectionsViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh; var canEditRemove = this.WhenAnyValue( diff --git a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs index f327c4df..5b5be60e 100644 --- a/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ClashProxiesViewModel.cs @@ -33,10 +33,9 @@ public class ClashProxiesViewModel : MyReactiveObject [Reactive] public bool AutoRefresh { get; set; } - public ClashProxiesViewModel(Func>? updateView) + public ClashProxiesViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; ProxiesReloadCmd = ReactiveCommand.CreateFromTask(async () => { @@ -86,15 +85,6 @@ public class ClashProxiesViewModel : MyReactiveObject #endregion WhenAnyValue && ReactiveCommand - #region AppEvents - - AppEvents.ProxiesReloadRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await ProxiesReload()); - - #endregion AppEvents - _ = Init(); } diff --git a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs index b484cd24..4b389bff 100644 --- a/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/DNSSettingViewModel.cs @@ -1,7 +1,9 @@ namespace ServiceLib.ViewModels; -public class DNSSettingViewModel : MyReactiveObject +public class DNSSettingViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + [Reactive] public bool? UseSystemHosts { get; set; } [Reactive] public bool? AddCommonHosts { get; set; } [Reactive] public bool? FakeIP { get; set; } @@ -17,15 +19,15 @@ public class DNSSettingViewModel : MyReactiveObject [Reactive] public bool? ServeStale { get; set; } [Reactive] public bool UseSystemHostsCompatible { get; set; } - [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } - [Reactive] public string DomainDNSAddressCompatible { get; set; } - [Reactive] public string NormalDNSCompatible { get; set; } - [Reactive] public string TunDNSCompatible { get; set; } + [Reactive] public string DomainStrategy4FreedomCompatible { get; set; } = string.Empty; + [Reactive] public string DomainDNSAddressCompatible { get; set; } = string.Empty; + [Reactive] public string NormalDNSCompatible { get; set; } = string.Empty; + [Reactive] public string TunDNSCompatible { get; set; } = string.Empty; - [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } - [Reactive] public string DomainDNSAddress2Compatible { get; set; } - [Reactive] public string NormalDNS2Compatible { get; set; } - [Reactive] public string TunDNS2Compatible { get; set; } + [Reactive] public string DomainStrategy4Freedom2Compatible { get; set; } = string.Empty; + [Reactive] public string DomainDNSAddress2Compatible { get; set; } = string.Empty; + [Reactive] public string NormalDNS2Compatible { get; set; } = string.Empty; + [Reactive] public string TunDNS2Compatible { get; set; } = string.Empty; [Reactive] public bool RayCustomDNSEnableCompatible { get; set; } [Reactive] public bool SBCustomDNSEnableCompatible { get; set; } @@ -35,10 +37,9 @@ public class DNSSettingViewModel : MyReactiveObject public ReactiveCommand ImportDefConfig4V2rayCompatibleCmd { get; } public ReactiveCommand ImportDefConfig4SingboxCompatibleCmd { get; } - public DNSSettingViewModel(Func>? updateView) + public DNSSettingViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; SaveCmd = ReactiveCommand.CreateFromTask(SaveSettingAsync); ImportDefConfig4V2rayCompatibleCmd = ReactiveCommand.CreateFromTask(async () => @@ -183,9 +184,6 @@ public class DNSSettingViewModel : MyReactiveObject await ConfigHandler.SaveDNSItems(_config, item2); await ConfigHandler.SaveConfig(_config); - if (_updateView != null) - { - await _updateView(EViewAction.CloseWindow, null); - } + RequestClose?.Invoke(this, EventArgs.Empty); } } diff --git a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs index 78cd10a2..8a104e1b 100644 --- a/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/FullConfigTemplateViewModel.cs @@ -1,7 +1,9 @@ namespace ServiceLib.ViewModels; -public class FullConfigTemplateViewModel : MyReactiveObject +public class FullConfigTemplateViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + #region Reactive [Reactive] @@ -11,16 +13,16 @@ public class FullConfigTemplateViewModel : MyReactiveObject public bool EnableFullConfigTemplate4Singbox { get; set; } [Reactive] - public string FullConfigTemplate4Ray { get; set; } + public string FullConfigTemplate4Ray { get; set; } = string.Empty; [Reactive] - public string FullTunConfigTemplate4Ray { get; set; } + public string FullTunConfigTemplate4Ray { get; set; } = string.Empty; [Reactive] - public string FullConfigTemplate4Singbox { get; set; } + public string FullConfigTemplate4Singbox { get; set; } = string.Empty; [Reactive] - public string FullTunConfigTemplate4Singbox { get; set; } + public string FullTunConfigTemplate4Singbox { get; set; } = string.Empty; [Reactive] public bool AddProxyOnly4Ray { get; set; } @@ -29,19 +31,18 @@ public class FullConfigTemplateViewModel : MyReactiveObject public bool AddProxyOnly4Singbox { get; set; } [Reactive] - public string ProxyDetour4Ray { get; set; } + public string ProxyDetour4Ray { get; set; } = string.Empty; [Reactive] - public string ProxyDetour4Singbox { get; set; } + public string ProxyDetour4Singbox { get; set; } = string.Empty; public ReactiveCommand SaveCmd { get; } #endregion Reactive - public FullConfigTemplateViewModel(Func>? updateView) + public FullConfigTemplateViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; SaveCmd = ReactiveCommand.CreateFromTask(async () => { await SaveSettingAsync(); @@ -84,7 +85,7 @@ public class FullConfigTemplateViewModel : MyReactiveObject } NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _ = _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } private async Task SaveXrayConfigAsync() diff --git a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs index 581007f1..3933aa5b 100644 --- a/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/GlobalHotkeySettingViewModel.cs @@ -1,15 +1,16 @@ namespace ServiceLib.ViewModels; -public class GlobalHotkeySettingViewModel : MyReactiveObject +public class GlobalHotkeySettingViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + private readonly List _globalHotkeys; public ReactiveCommand SaveCmd { get; } - public GlobalHotkeySettingViewModel(Func>? updateView) + public GlobalHotkeySettingViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; _globalHotkeys = JsonUtils.DeepCopy(_config.GlobalHotkeys); @@ -51,7 +52,7 @@ public class GlobalHotkeySettingViewModel : MyReactiveObject if (await ConfigHandler.SaveConfig(_config) == 0) { - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 66ffd051..085ecc74 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -4,6 +4,21 @@ namespace ServiceLib.ViewModels; public class MainWindowViewModel : MyReactiveObject { + public Interaction ReadTextFromClipboardInteraction { get; } = new(); + public Interaction ScanScreenInteraction { get; } = new(); + public Interaction BrowseImageFileInteraction { get; } = new(); + public Interaction ShowHideWindowInteraction { get; } = new(); + + public bool DesignMode { get; set; } + + public ProfilesViewModel ProfilesViewModel { get; } = new(); + public MsgViewModel MsgViewModel { get; } = new(); + public ClashProxiesViewModel ClashProxiesViewModel { get; } = new(); + public ClashConnectionsViewModel ClashConnectionsViewModel { get; } = new(); + public CheckUpdateViewModel CheckUpdateViewModel { get; } = new(); + public BackupAndRestoreViewModel BackupAndRestoreViewModel { get; } = new(); + public StatusBarViewModel StatusBarViewModel { get; } = StatusBarViewModel.Instance; + #region Menu //servers @@ -67,15 +82,17 @@ public class MainWindowViewModel : MyReactiveObject [Reactive] public bool BlNewUpdate { get; set; } + [Reactive] public EGirdOrientation MainGirdOrientation { get; set; } + #endregion Menu #region Init - public MainWindowViewModel(Func>? updateView) + public MainWindowViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; BlIsWindows = Utils.IsWindows(); + MainGirdOrientation = _config.UiItem.MainGirdOrientation; #region WhenAnyValue && ReactiveCommand @@ -191,7 +208,8 @@ public class MainWindowViewModel : MyReactiveObject }); GlobalHotkeySettingCmd = ReactiveCommand.CreateFromTask(async () => { - if (await _updateView?.Invoke(EViewAction.GlobalHotkeySettingWindow, null) == true) + var globalHotkeySettingViewModel = new GlobalHotkeySettingViewModel(); + if (await AppManager.Instance.WindowDialog.ShowDialogAsync(globalHotkeySettingViewModel) == true) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } @@ -233,26 +251,11 @@ public class MainWindowViewModel : MyReactiveObject #region AppEvents - AppEvents.ReloadRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await Reload()); - - AppEvents.AddServerViaScanRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await AddServerViaScanAsync()); - AppEvents.AddServerViaClipboardRequested .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async _ => await AddServerViaClipboardAsync(null)); - AppEvents.SubscriptionsUpdateRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy)); - AppEvents.HasUpdateNotified .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) @@ -260,6 +263,48 @@ public class MainWindowViewModel : MyReactiveObject #endregion AppEvents + var vmReloadRequestedList = new List> + { + ProfilesViewModel.ReloadRequested.AsObservable(), + StatusBarViewModel.ReloadRequested.AsObservable(), + CheckUpdateViewModel.ReloadRequested.AsObservable(), + }; + + foreach (var reloadRequested in vmReloadRequestedList) + { + reloadRequested + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async _ => await Reload()); + } + + StatusBarViewModel.AddServerViaScanRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async _ => await AddServerViaScanAsync()); + + StatusBarViewModel.AddServerViaClipboardRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async _ => await AddServerViaClipboardAsync(null)); + + StatusBarViewModel.ShowHideWindowRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async blShow => + { + await ShowHideWindowInteraction.Handle(blShow); + }); + + StatusBarViewModel.SetDefaultServerRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async indexId => await ProfilesViewModel.SetDefaultServer(indexId)); + + StatusBarViewModel.SubscriptionsUpdateRequested + .AsObservable() + .ObserveOn(RxSchedulers.MainThreadScheduler) + .Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy)); + _ = Init(); } @@ -267,6 +312,11 @@ public class MainWindowViewModel : MyReactiveObject { AppManager.Instance.ShowInTaskbar = true; + if (DesignMode) + { + return; + } + //await ConfigHandler.InitBuiltinRouting(_config); await ConfigHandler.InitBuiltinDNS(_config); await ConfigHandler.InitBuiltinFullConfigTemplate(_config); @@ -279,7 +329,7 @@ public class MainWindowViewModel : MyReactiveObject { await StatisticsManager.Instance.Init(_config, UpdateStatisticsHandler); } - await RefreshServers(); + await RefreshServersDispatcherAsync(); await Reload(); } @@ -304,7 +354,7 @@ public class MainWindowViewModel : MyReactiveObject if (success) { var indexIdOld = _config.IndexId; - await RefreshServers(); + await RefreshServersDispatcherAsync(); // If indexId changed or subIndexId is empty, directly reload. if (indexIdOld != _config.IndexId || _config.SubIndexId.IsNullOrEmpty()) @@ -323,7 +373,7 @@ public class MainWindowViewModel : MyReactiveObject if (_config.UiItem.EnableAutoAdjustMainLvColWidth) { - AppEvents.AdjustMainLvColWidthRequested.Publish(); + await ProfilesViewModel.AdjustMainLvColWidth(); } } } @@ -344,14 +394,20 @@ public class MainWindowViewModel : MyReactiveObject private async Task RefreshServers() { - AppEvents.ProfilesRefreshRequested.Publish(); + await ProfilesViewModel.RefreshServers(); + await StatusBarViewModel.RefreshServers(); - await Task.Delay(200); + // await Task.Delay(200); } - private void RefreshSubscriptions() + private async Task RefreshServersDispatcherAsync() { - AppEvents.SubscriptionsRefreshRequested.Publish(); + await Observable.Start(async () => await RefreshServers(), RxSchedulers.MainThreadScheduler); + } + + private async Task RefreshSubscriptions() + { + await Observable.Start(async () => await ProfilesViewModel.RefreshSubscriptions(), RxSchedulers.MainThreadScheduler); } #endregion Servers && Groups @@ -370,19 +426,22 @@ public class MainWindowViewModel : MyReactiveObject bool? ret = false; if (eConfigType == EConfigType.Custom) { - ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); + var addServer2ViewModel = new AddServer2ViewModel(item); + ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel); } else if (eConfigType.IsGroupType()) { - ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + var addGroupServerViewModel = new AddGroupServerViewModel(item); + ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel); } else { - ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); + var addServerViewModel = new AddServerViewModel(item); + ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel); } if (ret == true) { - await RefreshServers(); + await RefreshServersDispatcherAsync(); if (item.IndexId == _config.IndexId) { await Reload(); @@ -392,16 +451,22 @@ public class MainWindowViewModel : MyReactiveObject public async Task AddServerViaClipboardAsync(string? clipboardData) { + var stringData = clipboardData; if (clipboardData == null) { - await _updateView?.Invoke(EViewAction.AddServerViaClipboard, null); - return; + var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default); + if (result.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + return; + } + stringData = result; } - var ret = await ConfigHandler.AddBatchServers(_config, clipboardData, _config.SubIndexId, false); + var ret = await ConfigHandler.AddBatchServers(_config, stringData, _config.SubIndexId, false); if (ret > 0) { - RefreshSubscriptions(); - await RefreshServers(); + await RefreshSubscriptions(); + await RefreshServersDispatcherAsync(); NoticeManager.Instance.Enqueue(string.Format(ResUI.SuccessfullyImportedServerViaClipboard, ret)); } else @@ -412,8 +477,8 @@ public class MainWindowViewModel : MyReactiveObject public async Task AddServerViaScanAsync() { - _updateView?.Invoke(EViewAction.ScanScreenTask, null); - await Task.CompletedTask; + var result = await ScanScreenInteraction.Handle(Unit.Default); + await ScanScreenResult(result); } public async Task ScanScreenResult(byte[]? bytes) @@ -424,8 +489,8 @@ public class MainWindowViewModel : MyReactiveObject public async Task AddServerViaImageAsync() { - _updateView?.Invoke(EViewAction.ScanImageTask, null); - await Task.CompletedTask; + var imageFileName = await BrowseImageFileInteraction.Handle(Unit.Default); + await AddScanResultAsync(imageFileName); } public async Task ScanImageResult(string fileName) @@ -450,8 +515,8 @@ public class MainWindowViewModel : MyReactiveObject var ret = await ConfigHandler.AddBatchServers(_config, result, _config.SubIndexId, false); if (ret > 0) { - RefreshSubscriptions(); - await RefreshServers(); + await RefreshSubscriptions(); + await RefreshServersDispatcherAsync(); NoticeManager.Instance.Enqueue(ResUI.SuccessfullyImportedServerViaScan); } else @@ -467,9 +532,10 @@ public class MainWindowViewModel : MyReactiveObject private async Task SubSettingAsync() { - if (await _updateView?.Invoke(EViewAction.SubSettingWindow, null) == true) + var subSettingViewModel = new SubSettingViewModel(); + if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subSettingViewModel) == true) { - RefreshSubscriptions(); + await RefreshSubscriptions(); } } @@ -484,28 +550,38 @@ public class MainWindowViewModel : MyReactiveObject private async Task OptionSettingAsync() { - var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null); + var settingViewModel = new OptionSettingViewModel(); + var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(settingViewModel); if (ret == true) { - AppEvents.InboundDisplayRequested.Publish(); + MainGirdOrientation = _config.UiItem.MainGirdOrientation; + RxSchedulers.MainThreadScheduler.Schedule(async () => + { + await StatusBarViewModel.InboundDisplayStatus(); + }); await Reload(); } } private async Task RoutingSettingAsync() { - var ret = await _updateView?.Invoke(EViewAction.RoutingSettingWindow, null); + var routingSettingViewModel = new RoutingSettingViewModel(); + var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(routingSettingViewModel); if (ret == true) { await ConfigHandler.InitBuiltinRouting(_config); - AppEvents.RoutingsMenuRefreshRequested.Publish(); + RxSchedulers.MainThreadScheduler.Schedule(async () => + { + await StatusBarViewModel.RefreshRoutingsMenu(); + }); await Reload(); } } private async Task DNSSettingAsync() { - var ret = await _updateView?.Invoke(EViewAction.DNSSettingWindow, null); + var dnsSettingViewModel = new DNSSettingViewModel(); + var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(dnsSettingViewModel); if (ret == true) { await Reload(); @@ -514,7 +590,8 @@ public class MainWindowViewModel : MyReactiveObject private async Task FullConfigTemplateAsync() { - var ret = await _updateView?.Invoke(EViewAction.FullConfigTemplateWindow, null); + var fullConfigTemplateViewModel = new FullConfigTemplateViewModel(); + var ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(fullConfigTemplateViewModel); if (ret == true) { await Reload(); @@ -524,7 +601,7 @@ public class MainWindowViewModel : MyReactiveObject private async Task ClearServerStatistics() { await StatisticsManager.Instance.ClearAllServerStatistics(); - await RefreshServers(); + await RefreshServersDispatcherAsync(); } private async Task OpenTheFileLocation() @@ -561,6 +638,12 @@ public class MainWindowViewModel : MyReactiveObject return; } + if (DesignMode) + { + _reloadSemaphore.Release(); + return; + } + try { SetReloadEnabled(false); @@ -583,12 +666,22 @@ public class MainWindowViewModel : MyReactiveObject await SysProxyHandler.UpdateSysProxy(_config, false); await Task.Delay(1000); }); - AppEvents.TestServerRequested.Publish(); + RxSchedulers.MainThreadScheduler.Schedule(async () => + { + await StatusBarViewModel.TestServerAvailability(); + }); var showClashUI = AppManager.Instance.IsRunningCore(ECoreType.sing_box); if (showClashUI) { - AppEvents.ProxiesReloadRequested.Publish(); + //await Observable.Start(async () => + //{ + // await ClashProxiesViewModel.ProxiesReload(); + //}, RxSchedulers.MainThreadScheduler); + RxSchedulers.MainThreadScheduler.Schedule(async () => + { + await ClashProxiesViewModel.ProxiesReload(); + }); } ReloadResult(showClashUI); @@ -633,7 +726,10 @@ public class MainWindowViewModel : MyReactiveObject { await ConfigHandler.ApplyRegionalPreset(_config, type); await ConfigHandler.InitRouting(_config); - AppEvents.RoutingsMenuRefreshRequested.Publish(); + RxSchedulers.MainThreadScheduler.Schedule(async () => + { + await StatusBarViewModel.RefreshRoutingsMenu(); + }); await ConfigHandler.SaveConfig(_config); await new UpdateService(_config, UpdateTaskHandler).UpdateGeoFileAll(); diff --git a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs index fbe06cb0..7bc84c30 100644 --- a/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MsgViewModel.cs @@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels; public class MsgViewModel : MyReactiveObject { + public Interaction DispatcherShowMsgInteraction { get; } = new(); + private readonly ConcurrentQueue _queueMsg = new(); private volatile bool _lastMsgFilterNotAvailable; private int _showLock = 0; // 0 = unlocked, 1 = locked @@ -13,10 +15,9 @@ public class MsgViewModel : MyReactiveObject [Reactive] public bool AutoRefresh { get; set; } - public MsgViewModel(Func>? updateView) + public MsgViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; MsgFilter = _config.MsgUIItem.MainMsgFilter ?? string.Empty; AutoRefresh = _config.MsgUIItem.AutoRefresh ?? true; @@ -64,7 +65,7 @@ public class MsgViewModel : MyReactiveObject sb.Append(line); } - await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString()); + await DispatcherShowMsgInteraction.Handle(sb.ToString()); } finally { diff --git a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs index c2e82232..885ebc52 100644 --- a/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/OptionSettingViewModel.cs @@ -1,7 +1,9 @@ namespace ServiceLib.ViewModels; -public class OptionSettingViewModel : MyReactiveObject +public class OptionSettingViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + #region Core [Reactive] public int LocalPort { get; set; } @@ -123,10 +125,9 @@ public class OptionSettingViewModel : MyReactiveObject public ReactiveCommand SaveCmd { get; } - public OptionSettingViewModel(Func>? updateView) + public OptionSettingViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; BlIsWindows = Utils.IsWindows(); BlIsLinux = Utils.IsLinux(); BlIsIsMacOS = Utils.IsMacOS(); @@ -142,8 +143,6 @@ public class OptionSettingViewModel : MyReactiveObject private async Task Init() { - await _updateView?.Invoke(EViewAction.InitSettingFont, null); - #region Core var inbound = _config.Inbound.First(); @@ -314,8 +313,7 @@ public class OptionSettingViewModel : MyReactiveObject || DisplayRealTimeSpeed != _config.GuiItem.DisplayRealTimeSpeed || EnableDragDropSort != _config.UiItem.EnableDragDropSort || EnableHWA != _config.GuiItem.EnableHWA - || CurrentFontFamily != _config.UiItem.CurrentFontFamily - || MainGirdOrientation != (int)_config.UiItem.MainGirdOrientation; + || CurrentFontFamily != _config.UiItem.CurrentFontFamily; //if (Utile.IsNullOrEmpty(Kcpmtu.ToString()) || !Utile.IsNumeric(Kcpmtu.ToString()) // || Utile.IsNullOrEmpty(Kcptti.ToString()) || !Utile.IsNumeric(Kcptti.ToString()) @@ -432,7 +430,7 @@ public class OptionSettingViewModel : MyReactiveObject AppManager.Instance.Reset(); NoticeManager.Instance.Enqueue(needReboot ? ResUI.NeedRebootTips : ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs index b9a52936..0f3e992d 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesSelectViewModel.cs @@ -1,7 +1,10 @@ namespace ServiceLib.ViewModels; -public class ProfilesSelectViewModel : MyReactiveObject +public class ProfilesSelectViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + public Interaction ProfilesFocusInteraction { get; } = new(); + #region private prop private string _serverFilter = string.Empty; @@ -12,6 +15,8 @@ public class ProfilesSelectViewModel : MyReactiveObject #endregion private prop + public ReactiveCommand SaveCmd { get; } + #region ObservableCollection public IObservableCollection ProfileItems { get; } = new ObservableCollectionExtended(); @@ -36,18 +41,25 @@ public class ProfilesSelectViewModel : MyReactiveObject [Reactive] public bool FilterExclude { get; set; } + [Reactive] + public bool MultiSelect { get; set; } + #endregion ObservableCollection #region Init - public ProfilesSelectViewModel(Func>? updateView) + public ProfilesSelectViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; _subIndexId = _config.SubIndexId ?? string.Empty; #region WhenAnyValue && ReactiveCommand + SaveCmd = ReactiveCommand.Create(() => + { + SelectFinish(); + }); + this.WhenAnyValue( x => x.SelectedSub, y => y != null && !y.Remarks.IsNullOrEmpty() && _subIndexId != y.Id) @@ -107,7 +119,7 @@ public class ProfilesSelectViewModel : MyReactiveObject { return false; } - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); return true; } @@ -125,7 +137,13 @@ public class ProfilesSelectViewModel : MyReactiveObject await RefreshServers(); - await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + try + { + await ProfilesFocusInteraction.Handle(Unit.Default); + } + catch (UnhandledInteractionException) + { + } } private async Task ServerFilterChanged(bool c) @@ -157,8 +175,6 @@ public class ProfilesSelectViewModel : MyReactiveObject var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); SelectedProfile = selected ?? lstModel.First(); } - - await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); } private async Task RefreshSubscriptions() diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 47dfc91c..15ff8b26 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -2,6 +2,16 @@ namespace ServiceLib.ViewModels; public class ProfilesViewModel : MyReactiveObject { + public Interaction ShowYesNoInteraction { get; } = new(); + public Interaction SaveFileDialogInteraction { get; } = new(); + public Interaction SetClipboardDataInteraction { get; } = new(); + public Interaction ProfilesFocusInteraction { get; } = new(); + public Interaction ShareServerInteraction { get; } = new(); + public Interaction DispatcherRefreshServersBizInteraction { get; } = new(); + public Interaction AdjustMainLvColWidthInteraction { get; } = new(); + + public EventChannel ReloadRequested { get; } = new(); + #region private prop private List _lstProfile; @@ -82,10 +92,9 @@ public class ProfilesViewModel : MyReactiveObject #region Init - public ProfilesViewModel(Func>? updateView) + public ProfilesViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; #region WhenAnyValue && ReactiveCommand @@ -236,26 +245,11 @@ public class ProfilesViewModel : MyReactiveObject #region AppEvents - AppEvents.ProfilesRefreshRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await RefreshServersBiz()); - - AppEvents.SubscriptionsRefreshRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await RefreshSubscriptions()); - AppEvents.DispatcherStatisticsRequested .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); - AppEvents.SetDefaultServerRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async indexId => await SetDefaultServer(indexId)); - #endregion AppEvents _ = Init(); @@ -277,7 +271,7 @@ public class ProfilesViewModel : MyReactiveObject private void Reload() { - AppEvents.ReloadRequested.Publish(); + ReloadRequested.Publish(); } public async Task SetSpeedTestResult(SpeedTestResult result) @@ -350,7 +344,13 @@ public class ProfilesViewModel : MyReactiveObject await RefreshServers(); - await _updateView?.Invoke(EViewAction.ProfilesFocus, null); + try + { + await ProfilesFocusInteraction.Handle(Unit.Default); + } + catch (UnhandledInteractionException) + { + } } private async Task ServerFilterChanged(bool c) @@ -368,9 +368,9 @@ public class ProfilesViewModel : MyReactiveObject public async Task RefreshServers() { - AppEvents.ProfilesRefreshRequested.Publish(); + await RefreshServersBiz(); - await Task.Delay(200); + // await Task.Delay(200); } private async Task RefreshServersBiz() @@ -379,8 +379,8 @@ public class ProfilesViewModel : MyReactiveObject _lstProfile = JsonUtils.Deserialize>(JsonUtils.Serialize(lstModel)) ?? []; ProfileItems.Clear(); - ProfileItems.AddRange(lstModel); - if (lstModel.Count > 0) + ProfileItems.AddRange(lstModel ?? []); + if (lstModel?.Count > 0) { ProfileItemModel? selected = null; if (!_pendingSelectIndexId.IsNullOrEmpty()) @@ -392,10 +392,16 @@ public class ProfilesViewModel : MyReactiveObject SelectedProfile = selected ?? lstModel.First(); } - await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); + try + { + await DispatcherRefreshServersBizInteraction.Handle(Unit.Default); + } + catch (UnhandledInteractionException) + { + } } - private async Task RefreshSubscriptions() + public async Task RefreshSubscriptions() { var subItems = await AppManager.Instance.SubItems(); subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers }); @@ -408,6 +414,11 @@ public class ProfilesViewModel : MyReactiveObject : null) ?? subItems.FirstOrDefault(); } + public async Task AdjustMainLvColWidth() + { + await AdjustMainLvColWidthInteraction.Handle(Unit.Default); + } + private async Task?> GetProfileItemsEx(string subid, string filter) { var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter); @@ -491,15 +502,18 @@ public class ProfilesViewModel : MyReactiveObject bool? ret = false; if (eConfigType == EConfigType.Custom) { - ret = await _updateView?.Invoke(EViewAction.AddServer2Window, item); + var addServer2ViewModel = new AddServer2ViewModel(item); + ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel); } else if (eConfigType.IsGroupType()) { - ret = await _updateView?.Invoke(EViewAction.AddGroupServerWindow, item); + var addGroupServerViewModel = new AddGroupServerViewModel(item); + ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel); } else { - ret = await _updateView?.Invoke(EViewAction.AddServerWindow, item); + var addServerViewModel = new AddServerViewModel(item); + ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel); } if (ret == true) { @@ -518,7 +532,7 @@ public class ProfilesViewModel : MyReactiveObject { return; } - if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } @@ -539,7 +553,7 @@ public class ProfilesViewModel : MyReactiveObject private async Task RemoveDuplicateServer() { - if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } @@ -576,7 +590,7 @@ public class ProfilesViewModel : MyReactiveObject await SetDefaultServer(SelectedProfile.IndexId); } - private async Task SetDefaultServer(string? indexId) + public async Task SetDefaultServer(string? indexId) { if (indexId.IsNullOrEmpty()) { @@ -614,7 +628,7 @@ public class ProfilesViewModel : MyReactiveObject return; } - await _updateView?.Invoke(EViewAction.ShareServer, url); + await ShareServerInteraction.Handle(url); } private async Task GenGroupAllServer() @@ -783,13 +797,13 @@ public class ProfilesViewModel : MyReactiveObject } else { - await _updateView?.Invoke(EViewAction.SetClipboardData, result.Data); + await SetClipboardDataInteraction.Handle((string)result.Data); NoticeManager.Instance.SendMessage(ResUI.OperationSuccess); } } else { - await _updateView?.Invoke(EViewAction.SaveFileDialog, item); + await SaveFileDialogInteraction.Handle(item); } } @@ -838,11 +852,11 @@ public class ProfilesViewModel : MyReactiveObject { if (blEncode) { - await _updateView?.Invoke(EViewAction.SetClipboardData, Utils.Base64Encode(sb.ToString())); + await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString())); } else { - await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); + await SetClipboardDataInteraction.Handle(sb.ToString()); } NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); } @@ -865,7 +879,7 @@ public class ProfilesViewModel : MyReactiveObject if (!result.IsNullOrEmpty()) { - await _updateView?.Invoke(EViewAction.SetClipboardData, result); + await SetClipboardDataInteraction.Handle(result); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); } else @@ -893,7 +907,8 @@ public class ProfilesViewModel : MyReactiveObject return; } } - if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) + var subEditViewModel = new SubEditViewModel(item); + if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true) { await RefreshSubscriptions(); await SubSelectedChangedAsync(true); @@ -908,7 +923,7 @@ public class ProfilesViewModel : MyReactiveObject return; } - if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs index e0016911..03737c48 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleDetailsViewModel.cs @@ -1,7 +1,9 @@ namespace ServiceLib.ViewModels; -public class RoutingRuleDetailsViewModel : MyReactiveObject +public class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + public IList ProtocolItems { get; set; } public IList InboundTagItems { get; set; } @@ -23,13 +25,17 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject [Reactive] public bool AutoSort { get; set; } + public ReactiveCommand SelectProfileCmd { get; } public ReactiveCommand SaveCmd { get; } - public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func>? updateView) + public RoutingRuleDetailsViewModel(RulesItem rulesItem) { _config = AppManager.Instance.Config; - _updateView = updateView; + SelectProfileCmd = ReactiveCommand.CreateFromTask(async () => + { + await SelectProfileAsync(); + }); SaveCmd = ReactiveCommand.CreateFromTask(async () => { await SaveRulesAsync(); @@ -88,6 +94,24 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject return; } //NoticeHandler.Instance.Enqueue(ResUI.OperationSuccess); - await _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); + await Task.CompletedTask; + } + + private async Task SelectProfileAsync() + { + var profileSelectViewModel = new ProfilesSelectViewModel(); + profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true); + var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel); + if (result != true) + { + return; + } + var profileItem = await profileSelectViewModel.GetProfileItem(); + if (profileItem != null) + { + SelectedSource.OutboundTag = profileItem.Remarks; + SelectedSource = JsonUtils.DeepCopy(SelectedSource); + } } } diff --git a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs index bdcbfe94..949c77d8 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingRuleSettingViewModel.cs @@ -1,7 +1,13 @@ namespace ServiceLib.ViewModels; -public class RoutingRuleSettingViewModel : MyReactiveObject +public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + public Interaction ShowYesNoInteraction { get; } = new(); + public Interaction SetClipboardDataInteraction { get; } = new(); + public Interaction ReadTextFromClipboardInteraction { get; } = new(); + public Interaction BrowseRulesFileInteraction { get; } = new(); + private List _rules; [Reactive] @@ -27,10 +33,9 @@ public class RoutingRuleSettingViewModel : MyReactiveObject public ReactiveCommand SaveCmd { get; } - public RoutingRuleSettingViewModel(RoutingItem routingItem, Func>? updateView) + public RoutingRuleSettingViewModel(RoutingItem routingItem) { _config = AppManager.Instance.Config; - _updateView = updateView; var canEditRemove = this.WhenAnyValue( x => x.SelectedSource, @@ -42,7 +47,8 @@ public class RoutingRuleSettingViewModel : MyReactiveObject }); ImportRulesFromFileCmd = ReactiveCommand.CreateFromTask(async () => { - await _updateView?.Invoke(EViewAction.ImportRulesFromFile, null); + var fileName = await BrowseRulesFileInteraction.Handle(Unit.Default); + await ImportRulesFromFileAsync(fileName); }); ImportRulesFromClipboardCmd = ReactiveCommand.CreateFromTask(async () => { @@ -131,7 +137,8 @@ public class RoutingRuleSettingViewModel : MyReactiveObject return; } } - if (await _updateView?.Invoke(EViewAction.RoutingRuleDetailsWindow, item) == true) + var routingRuleDetailsViewModel = new RoutingRuleDetailsViewModel(item); + if (await AppManager.Instance.WindowDialog.ShowDialogAsync(routingRuleDetailsViewModel) == true) { if (blNew) { @@ -148,7 +155,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } - if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } @@ -191,7 +198,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - await _updateView?.Invoke(EViewAction.SetClipboardData, JsonUtils.Serialize(lst, options)); + await SetClipboardDataInteraction.Handle(JsonUtils.Serialize(lst, options)); } } @@ -234,7 +241,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject if (await ConfigHandler.SaveRoutingItem(_config, item) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { @@ -266,12 +273,18 @@ public class RoutingRuleSettingViewModel : MyReactiveObject public async Task ImportRulesFromClipboardAsync(string? clipboardData) { + var stringData = clipboardData; if (clipboardData == null) { - await _updateView?.Invoke(EViewAction.ImportRulesFromClipboard, null); - return; + var result = await ReadTextFromClipboardInteraction.Handle(Unit.Default); + if (result.IsNullOrEmpty()) + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + return; + } + stringData = result; } - var ret = await AddBatchRoutingRulesAsync(SelectedRouting, clipboardData); + var ret = await AddBatchRoutingRulesAsync(SelectedRouting, stringData); if (ret == 0) { RefreshRulesItems(); @@ -301,7 +314,7 @@ public class RoutingRuleSettingViewModel : MyReactiveObject private async Task AddBatchRoutingRulesAsync(RoutingItem routingItem, string? clipboardData) { var blReplace = false; - if (await _updateView?.Invoke(EViewAction.AddBatchRoutingRulesYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.AddBatchRoutingRulesYesNo) == false) { blReplace = true; } diff --git a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs index 13703ae7..4b248566 100644 --- a/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/RoutingSettingViewModel.cs @@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels; public class RoutingSettingViewModel : MyReactiveObject { + public Interaction ShowYesNoInteraction { get; } = new(); + #region Reactive public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); @@ -26,10 +28,9 @@ public class RoutingSettingViewModel : MyReactiveObject #endregion Reactive - public RoutingSettingViewModel(Func>? updateView) + public RoutingSettingViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; var canEditRemove = this.WhenAnyValue( x => x.SelectedSource, @@ -131,7 +132,8 @@ public class RoutingSettingViewModel : MyReactiveObject return; } } - if (await _updateView?.Invoke(EViewAction.RoutingRuleSettingWindow, item) == true) + var routingRuleSettingViewModel = new RoutingRuleSettingViewModel(item); + if (await AppManager.Instance.WindowDialog.ShowDialogAsync(routingRuleSettingViewModel) == true) { await RefreshRoutingItems(); IsModified = true; @@ -145,7 +147,7 @@ public class RoutingSettingViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.PleaseSelectRules); return; } - if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } diff --git a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs index f4eee499..3ba82c63 100644 --- a/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/StatusBarViewModel.cs @@ -2,9 +2,20 @@ namespace ServiceLib.ViewModels; public class StatusBarViewModel : MyReactiveObject { - private static readonly Lazy _instance = new(() => new(null)); + public Interaction SetClipboardDataInteraction { get; } = new(); + public Interaction PasswordInputInteraction { get; } = new(); + public Interaction DispatcherRefreshIconInteraction { get; } = new(); + public EventChannel SubscriptionsUpdateRequested { get; } = new(); + public EventChannel ShowHideWindowRequested { get; } = new(); + + private static readonly Lazy _instance = new(() => new()); public static StatusBarViewModel Instance => _instance.Value; + public EventChannel SetDefaultServerRequested { get; } = new(); + public EventChannel ReloadRequested { get; } = new(); + public EventChannel AddServerViaScanRequested { get; } = new(); + public EventChannel AddServerViaClipboardRequested { get; } = new(); + #region ObservableCollection public IObservableCollection RoutingItems { get; } = new ObservableCollectionExtended(); @@ -92,7 +103,7 @@ public class StatusBarViewModel : MyReactiveObject #endregion UI - public StatusBarViewModel(Func>? updateView) + public StatusBarViewModel() { _config = AppManager.Instance.Config; SelectedRouting = new(); @@ -140,17 +151,17 @@ public class StatusBarViewModel : MyReactiveObject NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () => { - AppEvents.ShowHideWindowRequested.Publish(null); + ShowHideWindowRequested.Publish(null); await Task.CompletedTask; }); ShowWindowCmd = ReactiveCommand.CreateFromTask(async () => { - AppEvents.ShowHideWindowRequested.Publish(true); + ShowHideWindowRequested.Publish(true); await Task.CompletedTask; }); HideWindowCmd = ReactiveCommand.CreateFromTask(async () => { - AppEvents.ShowHideWindowRequested.Publish(false); + ShowHideWindowRequested.Publish(false); await Task.CompletedTask; }); @@ -193,31 +204,11 @@ public class StatusBarViewModel : MyReactiveObject #region AppEvents - if (updateView != null) - { - InitUpdateView(updateView); - } - AppEvents.DispatcherStatisticsRequested .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); - AppEvents.RoutingsMenuRefreshRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await RefreshRoutingsMenu()); - - AppEvents.TestServerRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await TestServerAvailability()); - - AppEvents.InboundDisplayRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await InboundDisplayStatus()); - AppEvents.SysProxyChangeRequested .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) @@ -238,18 +229,6 @@ public class StatusBarViewModel : MyReactiveObject BlRouting = true; } - public void InitUpdateView(Func>? updateView) - { - _updateView = updateView; - if (_updateView != null) - { - AppEvents.ProfilesRefreshRequested - .AsObservable() - .ObserveOn(RxSchedulers.MainThreadScheduler) - .Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables); - } - } - private async Task CopyProxyCmdToClipboard() { var cmd = Utils.IsWindows() ? "set" : "export"; @@ -264,27 +243,32 @@ public class StatusBarViewModel : MyReactiveObject sb.AppendLine($"{cmd} HTTPS_PROXY={Global.HttpProtocol}{address}"); sb.AppendLine($"{cmd} ALL_PROXY={Global.Socks5Protocol}{address}"); - await _updateView?.Invoke(EViewAction.SetClipboardData, sb.ToString()); + await SetClipboardDataInteraction.Handle(sb.ToString()); } private async Task AddServerViaClipboard() { - AppEvents.AddServerViaClipboardRequested.Publish(); + AddServerViaClipboardRequested.Publish(); await Task.Delay(1000); } private async Task AddServerViaScan() { - AppEvents.AddServerViaScanRequested.Publish(); + AddServerViaScanRequested.Publish(); await Task.Delay(1000); } private async Task UpdateSubscriptionProcess(bool blProxy) { - AppEvents.SubscriptionsUpdateRequested.Publish(blProxy); + SubscriptionsUpdateRequested.Publish(blProxy); await Task.Delay(1000); } + public async Task RefreshServers() + { + await RefreshServersBiz(); + } + private async Task RefreshServersBiz() { await RefreshServersMenu(); @@ -312,8 +296,7 @@ public class StatusBarViewModel : MyReactiveObject { var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, ""); - Servers.Clear(); - if (lstModel.Count > _config.GuiItem.TrayMenuServersLimit) + if (lstModel?.Count > _config.GuiItem.TrayMenuServersLimit) { BlServers = false; return; @@ -332,6 +315,7 @@ public class StatusBarViewModel : MyReactiveObject SelectedServer = item; } } + Servers.Clear(); Servers.AddRange(models); } @@ -349,7 +333,7 @@ public class StatusBarViewModel : MyReactiveObject { return; } - AppEvents.SetDefaultServerRequested.Publish(SelectedServer.ID); + SetDefaultServerRequested.Publish(SelectedServer.ID); } public async Task TestServerAvailability() @@ -411,11 +395,18 @@ public class StatusBarViewModel : MyReactiveObject if (blChange) { - _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); + try + { + await DispatcherRefreshIconInteraction.Handle(Unit.Default); + } + catch (UnhandledInteractionException) + { + // Ignore + } } } - private async Task RefreshRoutingsMenu() + public async Task RefreshRoutingsMenu() { var routings = await AppManager.Instance.RoutingItems(); @@ -446,8 +437,8 @@ public class StatusBarViewModel : MyReactiveObject if (await ConfigHandler.SetDefaultRouting(_config, item) == 0) { NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting); - AppEvents.ReloadRequested.Publish(); - _updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null); + ReloadRequested.Publish(); + await DispatcherRefreshIconInteraction.Handle(Unit.Default); } } @@ -484,8 +475,8 @@ public class StatusBarViewModel : MyReactiveObject } else { - bool? passwordResult = await _updateView?.Invoke(EViewAction.PasswordInput, null); - if (passwordResult == false) + var password = await PasswordInputInteraction.Handle(Unit.Default); + if (password.IsNullOrEmpty()) { _config.TunModeItem.EnableTun = false; return; @@ -494,7 +485,7 @@ public class StatusBarViewModel : MyReactiveObject } await ConfigHandler.SaveConfig(_config); - AppEvents.ReloadRequested.Publish(); + ReloadRequested.Publish(); } private bool AllowEnableTun() @@ -518,7 +509,7 @@ public class StatusBarViewModel : MyReactiveObject #region UI - private async Task InboundDisplayStatus() + public async Task InboundDisplayStatus() { StringBuilder sb = new(); sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}"); diff --git a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs index 344f4ac8..3b91cf76 100644 --- a/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubEditViewModel.cs @@ -1,17 +1,38 @@ namespace ServiceLib.ViewModels; -public class SubEditViewModel : MyReactiveObject +public class SubEditViewModel : MyReactiveObject, ICloseable { + public event EventHandler? RequestClose; + [Reactive] public SubItem SelectedSource { get; set; } + public ReactiveCommand SelectPrevProfileCmd { get; } + public ReactiveCommand SelectNextProfileCmd { get; } public ReactiveCommand SaveCmd { get; } - public SubEditViewModel(SubItem subItem, Func>? updateView) + public SubEditViewModel(SubItem subItem) { _config = AppManager.Instance.Config; - _updateView = updateView; + SelectPrevProfileCmd = ReactiveCommand.CreateFromTask(async () => + { + var profileItem = await SelectProfileAsync(); + if (profileItem != null) + { + SelectedSource?.PrevProfile = profileItem.Remarks; + SelectedSource = JsonUtils.DeepCopy(SelectedSource); + } + }); + SelectNextProfileCmd = ReactiveCommand.CreateFromTask(async () => + { + var profileItem = await SelectProfileAsync(); + if (profileItem != null) + { + SelectedSource?.NextProfile = profileItem.Remarks; + SelectedSource = JsonUtils.DeepCopy(SelectedSource); + } + }); SaveCmd = ReactiveCommand.CreateFromTask(async () => { await SaveSubAsync(); @@ -49,11 +70,24 @@ public class SubEditViewModel : MyReactiveObject if (await ConfigHandler.AddSubItem(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); - _updateView?.Invoke(EViewAction.CloseWindow, null); + RequestClose?.Invoke(this, EventArgs.Empty); } else { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } + + private async Task SelectProfileAsync() + { + var profileSelectViewModel = new ProfilesSelectViewModel(); + profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true); + var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel); + if (result != true) + { + return null; + } + var profileItem = await profileSelectViewModel.GetProfileItem(); + return profileItem; + } } diff --git a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs index ce0f6fa7..e9d1fe59 100644 --- a/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/SubSettingViewModel.cs @@ -2,6 +2,9 @@ namespace ServiceLib.ViewModels; public class SubSettingViewModel : MyReactiveObject { + public Interaction ShowYesNoInteraction { get; } = new(); + public Interaction ShareSubInteraction { get; } = new(); + public IObservableCollection SubItems { get; } = new ObservableCollectionExtended(); [Reactive] @@ -15,10 +18,9 @@ public class SubSettingViewModel : MyReactiveObject public ReactiveCommand SubShareCmd { get; } public bool IsModified { get; set; } - public SubSettingViewModel(Func>? updateView) + public SubSettingViewModel() { _config = AppManager.Instance.Config; - _updateView = updateView; var canEditRemove = this.WhenAnyValue( x => x.SelectedSource, @@ -38,7 +40,7 @@ public class SubSettingViewModel : MyReactiveObject }, canEditRemove); SubShareCmd = ReactiveCommand.CreateFromTask(async () => { - await _updateView?.Invoke(EViewAction.ShareSub, SelectedSource?.Url); + await ShareSubInteraction.Handle(SelectedSource?.Url); }, canEditRemove); _ = Init(); @@ -72,7 +74,8 @@ public class SubSettingViewModel : MyReactiveObject return; } } - if (await _updateView?.Invoke(EViewAction.SubEditWindow, item) == true) + var subEditViewModel = new SubEditViewModel(item); + if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true) { await RefreshSubItems(); IsModified = true; @@ -81,7 +84,7 @@ public class SubSettingViewModel : MyReactiveObject private async Task DeleteSubAsync() { - if (await _updateView?.Invoke(EViewAction.ShowYesNo, null) == false) + if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs index 92f9398c..b53b4959 100644 --- a/v2rayN/v2rayN.Desktop/App.axaml.cs +++ b/v2rayN/v2rayN.Desktop/App.axaml.cs @@ -15,6 +15,9 @@ public partial class App : Application public override void OnFrameworkInitializationCompleted() { + var viewLocator = SimpleViewLocator.Instance; + DataTemplates.Add(viewLocator); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { if (!Design.IsDesignMode) @@ -23,7 +26,9 @@ public partial class App : Application DataContext = StatusBarViewModel.Instance; } - var mainWindow = new MainWindow(); + var mainWindowViewModel = new MainWindowViewModel(); + var mainWindow = (MainWindow)viewLocator.Build(mainWindowViewModel); + mainWindow.ViewModel = mainWindowViewModel; desktop.MainWindow = mainWindow; if (OperatingSystem.IsMacOS()) diff --git a/v2rayN/v2rayN.Desktop/Common/SimpleViewLocator.cs b/v2rayN/v2rayN.Desktop/Common/SimpleViewLocator.cs new file mode 100644 index 00000000..8b4328f5 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Common/SimpleViewLocator.cs @@ -0,0 +1,69 @@ +using Avalonia.Controls.Templates; +using v2rayN.Desktop.ViewModels; +using v2rayN.Desktop.Views; + +namespace v2rayN.Desktop.Common; + +public class SimpleViewLocator : IDataTemplate +{ + private static readonly Lazy _instance = new(() => new SimpleViewLocator()); + + private readonly Dictionary> _locator = new(); + + private SimpleViewLocator() + { + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + RegisterViewFactory(); + } + + public static SimpleViewLocator Instance => _instance.Value; + + public Control Build(object? data) + { + if (data is null) + { + return new TextBlock { Text = "No VM provided" }; + } + + _locator.TryGetValue(data.GetType(), out var factory); + + return factory?.Invoke() ?? new TextBlock { Text = $"VM Not Registered: {data.GetType()}" }; + } + + public bool Match(object? data) + { + return data is MyReactiveObject; + } + + public void RegisterViewFactory(Func factory) where TViewModel : class + { + _locator.Add(typeof(TViewModel), factory); + } + + public void RegisterViewFactory() + where TViewModel : class + where TView : Control, new() + { + _locator.Add(typeof(TViewModel), () => new TView()); + } +} diff --git a/v2rayN/v2rayN.Desktop/Common/UI.cs b/v2rayN/v2rayN.Desktop/Common/UI.cs index 990e3c68..c038f52a 100644 --- a/v2rayN/v2rayN.Desktop/Common/UI.cs +++ b/v2rayN/v2rayN.Desktop/Common/UI.cs @@ -1,4 +1,5 @@ using Avalonia.Platform.Storage; +using v2rayN.Desktop.Manager; using v2rayN.Desktop.Views; namespace v2rayN.Desktop.Common; @@ -7,16 +8,17 @@ internal class UI { private static readonly string caption = Global.AppName; - public static async Task ShowYesNo(Window owner, string msg) + public static async Task ShowYesNo(string msg) { + var owner = WindowDialog.TryGetOwnerWindow(); var box = new MessageBoxDialog(caption, msg); var result = await box.ShowDialog(owner); return result == ButtonResult.Yes ? ButtonResult.Yes : ButtonResult.No; } - public static async Task OpenFileDialog(Window owner, FilePickerFileType? filter) + public static async Task OpenFileDialog(FilePickerFileType? filter) { - var sp = GetStorageProvider(owner); + var sp = GetStorageProvider(); if (sp is null) { return null; @@ -32,9 +34,9 @@ internal class UI return files.FirstOrDefault()?.TryGetLocalPath(); } - public static async Task SaveFileDialog(Window owner, string filter) + public static async Task SaveFileDialog(string filter) { - var sp = GetStorageProvider(owner); + var sp = GetStorageProvider(); if (sp is null) { return null; @@ -48,8 +50,9 @@ internal class UI return files?.TryGetLocalPath(); } - private static IStorageProvider? GetStorageProvider(Window owner) + private static IStorageProvider? GetStorageProvider() { + var owner = WindowDialog.TryGetOwnerWindow(); var topLevel = TopLevel.GetTopLevel(owner); return topLevel?.StorageProvider; } diff --git a/v2rayN/v2rayN.Desktop/DesignData/DesignData.cs b/v2rayN/v2rayN.Desktop/DesignData/DesignData.cs new file mode 100644 index 00000000..3b6f2afa --- /dev/null +++ b/v2rayN/v2rayN.Desktop/DesignData/DesignData.cs @@ -0,0 +1,99 @@ +using ServiceLib.Models.Entities; +using System.Diagnostics; +using v2rayN.Desktop.Manager; +using v2rayN.Desktop.ViewModels; + +namespace v2rayN.Desktop.DesignData; + +/// +/// Provides design-time data for Avalonia XAML previewer. +/// Each inner class lazily initializes with a stub config +/// so that ViewModel constructors don't fail during design-time rendering. +/// +public static class DesignData +{ + // ── Parameterless-constructor ViewModels ─────────────────────────────── + + public static MainWindowViewModel? MainWindow { get; } = SafeCreate(CreateMainWindow); + + public static ProfilesViewModel? Profiles { get; } = SafeCreate(() => new ProfilesViewModel()); + + public static StatusBarViewModel? StatusBar { get; } = SafeCreate(CreateStatusBar); + + public static MsgViewModel? Msg { get; } = SafeCreate(() => new MsgViewModel()); + + public static SubSettingViewModel? SubSetting { get; } = SafeCreate(() => new SubSettingViewModel()); + + public static RoutingSettingViewModel? RoutingSetting { get; } = SafeCreate(() => new RoutingSettingViewModel()); + + public static ClashProxiesViewModel? ClashProxies { get; } = SafeCreate(() => new ClashProxiesViewModel()); + + public static ClashConnectionsViewModel? ClashConnections { get; } = SafeCreate(() => new ClashConnectionsViewModel()); + + public static CheckUpdateViewModel? CheckUpdate { get; } = SafeCreate(() => new CheckUpdateViewModel()); + + public static DNSSettingViewModel? DNSSetting { get; } = SafeCreate(() => new DNSSettingViewModel()); + + public static FullConfigTemplateViewModel? FullConfigTemplate { get; } = SafeCreate(() => new FullConfigTemplateViewModel()); + + public static GlobalHotkeySettingViewModel? GlobalHotkeySetting { get; } = SafeCreate(() => new GlobalHotkeySettingViewModel()); + + public static OptionSettingViewModel? OptionSetting { get; } = SafeCreate(() => new OptionSettingViewModel()); + + public static ProfilesSelectViewModel? ProfilesSelect { get; } = SafeCreate(() => new ProfilesSelectViewModel()); + + public static BackupAndRestoreViewModel? BackupAndRestore { get; } = SafeCreate(() => new BackupAndRestoreViewModel()); + + public static ThemeSettingViewModel? ThemeSetting { get; } = SafeCreate(() => new ThemeSettingViewModel()); + + // ── ViewModels that require constructor parameters ───────────────────── + + public static AddGroupServerViewModel? AddGroupServer { get; } = SafeCreate(() => new AddGroupServerViewModel(new ProfileItem { Remarks = "Design Group", ConfigType = EConfigType.PolicyGroup })); + + public static AddServer2ViewModel? AddServer2 { get; } = SafeCreate(() => new AddServer2ViewModel(new ProfileItem { Remarks = "Design Custom Server", ConfigType = EConfigType.Custom })); + + public static AddServerViewModel? AddServer { get; } = SafeCreate(() => new AddServerViewModel(new ProfileItem { Remarks = "Design VMess Server", ConfigType = EConfigType.VMess, Address = "example.com", Port = 443 })); + + public static RoutingRuleSettingViewModel? RoutingRuleSetting { get; } = SafeCreate(() => new RoutingRuleSettingViewModel(new RoutingItem { Remarks = "Design Routing Rule" })); + + public static RoutingRuleDetailsViewModel? RoutingRuleDetails { get; } = SafeCreate(() => new RoutingRuleDetailsViewModel(new RulesItem { Domain = ["example.com"], OutboundTag = "direct" })); + + public static SubEditViewModel? SubEdit { get; } = SafeCreate(() => new SubEditViewModel(new SubItem { Remarks = "Design Subscription", Url = "https://example.com/sub" })); + + // ── Helper factories ─────────────────────────────────────────────────── + + private static MainWindowViewModel CreateMainWindow() + { + var vm = new MainWindowViewModel { DesignMode = true }; + return vm; + } + + private static StatusBarViewModel CreateStatusBar() + { + var vm = StatusBarViewModel.Instance; + vm.InboundDisplay = "socks:10808"; + vm.InboundLanDisplay = "http:10809"; + vm.RunningServerDisplay = "🚀 Design Server (Active)"; + vm.RunningInfoDisplay = "v2rayN Design Mode"; + vm.SpeedProxyDisplay = "↑ 1.2 MB/s"; + vm.SpeedDirectDisplay = "↓ 5.6 MB/s"; + vm.RoutingItems.Add(new RoutingItem { Remarks = "Default Routing" }); + vm.RoutingItems.Add(new RoutingItem { Remarks = "Global" }); + return vm; + } + + private static T? SafeCreate(Func factory) where T : class + { + try + { + AppManager.Instance.InitApp(); + AppManager.Instance.WindowDialog = new WindowDialog(); + return factory(); + } + catch (Exception ex) + { + Debug.WriteLine($"[DesignData] Failed to create {typeof(T).Name}: {ex}"); + return null; + } + } +} diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs index 558852d2..53b79a33 100644 --- a/v2rayN/v2rayN.Desktop/GlobalUsings.cs +++ b/v2rayN/v2rayN.Desktop/GlobalUsings.cs @@ -3,6 +3,7 @@ global using System.Collections.Generic; global using System.Globalization; global using System.IO; global using System.Linq; +global using System.Reactive; global using System.Reactive.Disposables.Fluent; global using System.Reactive.Linq; global using System.Runtime.Versioning; diff --git a/v2rayN/v2rayN.Desktop/Manager/WindowDialog.cs b/v2rayN/v2rayN.Desktop/Manager/WindowDialog.cs new file mode 100644 index 00000000..4ecea3f9 --- /dev/null +++ b/v2rayN/v2rayN.Desktop/Manager/WindowDialog.cs @@ -0,0 +1,68 @@ +using v2rayN.Desktop.Base; +using v2rayN.Desktop.Common; + +namespace v2rayN.Desktop.Manager; + +public class WindowDialog : IWindowDialog +{ + public async Task ShowDialogAsync(TViewModel vm) + where TViewModel : class + { + var owner = TryGetOwnerWindow(); + + var view = SimpleViewLocator.Instance.Build(vm); + + if (view is not WindowBase window) + { + return false; + } + + window.ViewModel = vm; + + if (vm is ServiceLib.Base.ICloseable closeable) + { + closeable.RequestClose += (_, _) => Dispatch(() => window.Close(true)); + } + + var result = await window.ShowDialog(owner); + return result; + } + + public static Window TryGetOwnerWindow() + { + var desktop = Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime + ?? throw new InvalidOperationException("Application lifetime is not of type IClassicDesktopStyleApplicationLifetime."); + var openWindows = desktop.Windows + .Where(w => w.IsVisible) + .ToArray(); + if (openWindows.Length == 0) + { + return desktop.MainWindow + ?? throw new InvalidOperationException("No open windows and no main window found."); + } + + if (openWindows.Length == 1) + { + return openWindows[0]; + } + + try + { + Window.SortWindowsByZOrder(openWindows); + + var activeTopmost = openWindows.Reverse().FirstOrDefault(w => w.IsActive); + return activeTopmost ?? openWindows[^1]; + } + catch + { + return desktop.Windows.FirstOrDefault(w => w.IsActive) + ?? desktop.MainWindow + ?? openWindows[0]; + } + } + + private static void Dispatch(Action action) + { + Dispatcher.UIThread.InvokeAsync(action); + } +} diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs index a7d1e055..b7e76b1c 100644 --- a/v2rayN/v2rayN.Desktop/Program.cs +++ b/v2rayN/v2rayN.Desktop/Program.cs @@ -1,4 +1,5 @@ using v2rayN.Desktop.Common; +using v2rayN.Desktop.Manager; namespace v2rayN.Desktop; @@ -48,6 +49,8 @@ internal class Program { return false; } + + AppManager.Instance.WindowDialog = new WindowDialog(); return true; } diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml index e50da940..82b15b77 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml @@ -3,6 +3,7 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" @@ -10,6 +11,7 @@ Width="900" Height="700" x:DataType="vms:AddGroupServerViewModel" + Design.DataContext="{x:Static dd:DesignData.AddGroupServer}" ShowInTaskbar="False" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> diff --git a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs index 1a2af744..1d51551a 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/AddGroupServerWindow.axaml.cs @@ -5,11 +5,6 @@ namespace v2rayN.Desktop.Views; public partial class AddGroupServerWindow : WindowBase { public AddGroupServerWindow() - { - InitializeComponent(); - } - - public AddGroupServerWindow(ProfileItem profileItem) { InitializeComponent(); @@ -18,8 +13,6 @@ public partial class AddGroupServerWindow : WindowBase lstChild.SelectionChanged += LstChild_SelectionChanged; tabControl.SelectionChanged += TabControl_SelectionChanged; - ViewModel = new AddGroupServerViewModel(profileItem, UpdateViewHandler); - cmbCoreType.ItemsSource = Global.CoreTypes; cmbPolicyGroupType.ItemsSource = new List { @@ -31,6 +24,42 @@ public partial class AddGroupServerWindow : WindowBase }; cmbFilter.ItemsSource = Global.PolicyGroupDefaultFilterList; + this.WhenActivated(disposables => + { + this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables); + //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.AddCmd, v => v.menuAddChildServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.SelectedSource) + .WhereNotNull() + .Subscribe(InitializeData) + .DisposeWith(disposables); + }); + + // Context menu actions that require custom logic (Add, SelectAll) + menuSelectAllChild.Click += (s, e) => lstChild.SelectAll(); + + // Keyboard shortcuts when focus is within grid + AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel); + lstChild.LoadingRow += LstChild_LoadingRow; + } + + private void InitializeData(ProfileItem profileItem) + { switch (profileItem.ConfigType) { case EConfigType.PolicyGroup: @@ -46,34 +75,6 @@ public partial class AddGroupServerWindow : WindowBase } break; } - - this.WhenActivated(disposables => - { - this.Bind(ViewModel, vm => vm.SelectedSource.Remarks, v => v.txtRemarks.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.CoreType, v => v.cmbCoreType.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.PolicyGroupType, v => v.cmbPolicyGroupType.SelectedValue).DisposeWith(disposables); - //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbSubChildItems.ItemsSource).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSubItem, v => v.cmbSubChildItems.SelectedItem).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Filter, v => v.cmbFilter.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.SelectedChild, v => v.lstChild.SelectedItem).DisposeWith(disposables); - - this.BindCommand(ViewModel, vm => vm.RemoveCmd, v => v.menuRemoveChildServer).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.MoveTopCmd, v => v.menuMoveTop).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.MoveUpCmd, v => v.menuMoveUp).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.MoveDownCmd, v => v.menuMoveDown).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.MoveBottomCmd, v => v.menuMoveBottom).DisposeWith(disposables); - - this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); - }); - - // Context menu actions that require custom logic (Add, SelectAll) - menuAddChildServer.Click += MenuAddChild_Click; - menuSelectAllChild.Click += (s, e) => lstChild.SelectAll(); - - // Keyboard shortcuts when focus is within grid - AddHandler(KeyDownEvent, AddGroupServerWindow_KeyDown, RoutingStrategies.Tunnel); - lstChild.LoadingRow += LstChild_LoadingRow; } private void LstChild_LoadingRow(object? sender, DataGridRowEventArgs e) @@ -81,17 +82,6 @@ public partial class AddGroupServerWindow : WindowBase e.Row.Header = $" {e.Row.Index + 1}"; } - private async Task UpdateViewHandler(EViewAction action, object? obj) - { - switch (action) - { - case EViewAction.CloseWindow: - Close(true); - break; - } - return await Task.FromResult(true); - } - private void Window_Loaded(object? sender, RoutedEventArgs e) { txtRemarks.Focus(); @@ -145,19 +135,6 @@ public partial class AddGroupServerWindow : WindowBase } } - private async void MenuAddChild_Click(object? sender, RoutedEventArgs e) - { - var selectWindow = new ProfilesSelectWindow(); - selectWindow.SetConfigTypeFilter([EConfigType.Custom], exclude: true); - selectWindow.AllowMultiSelect(true); - var result = await selectWindow.ShowDialog(this); - if (result == true) - { - var profiles = await selectWindow.ProfileItems; - ViewModel?.ChildItemsObs.AddRange(profiles); - } - } - private void LstChild_SelectionChanged(object? sender, SelectionChangedEventArgs e) { if (ViewModel != null) diff --git a/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml b/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml index 0c9d89a4..d08e85f6 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml @@ -3,6 +3,7 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:vms="clr-namespace:ServiceLib.ViewModels;assembly=ServiceLib" @@ -10,6 +11,7 @@ Width="700" Height="500" x:DataType="vms:AddServer2ViewModel" + Design.DataContext="{x:Static dd:DesignData.AddServer2}" ShowInTaskbar="False" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> diff --git a/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs index 64260fae..651b0d0f 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/AddServer2Window.axaml.cs @@ -6,17 +6,11 @@ namespace v2rayN.Desktop.Views; public partial class AddServer2Window : WindowBase { public AddServer2Window() - { - InitializeComponent(); - } - - public AddServer2Window(ProfileItem profileItem) { InitializeComponent(); Loaded += Window_Loaded; btnCancel.Click += (s, e) => Close(); - ViewModel = new AddServer2ViewModel(profileItem, UpdateViewHandler); cmbCoreType.ItemsSource = Utils.GetEnumNames().Where(t => t != nameof(ECoreType.v2rayN)).ToList().AppendEmpty(); @@ -31,30 +25,15 @@ public partial class AddServer2Window : WindowBase 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.SaveServerCmd, v => v.btnSave).DisposeWith(disposables); + + ViewModel.BrowseConfigFileInteraction.RegisterHandler(async interaction => + { + var fileName = await UI.OpenFileDialog(null); + interaction.SetOutput(fileName); + }).DisposeWith(disposables); }); } - private async Task UpdateViewHandler(EViewAction action, object? obj) - { - switch (action) - { - case EViewAction.CloseWindow: - Close(true); - break; - - case EViewAction.BrowseServer: - var fileName = await UI.OpenFileDialog(this, null); - if (fileName.IsNullOrEmpty()) - { - return false; - } - ViewModel?.BrowseServer(fileName); - break; - } - - return await Task.FromResult(true); - } - private void Window_Loaded(object? sender, RoutedEventArgs e) { txtRemarks.Focus(); diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index 7f970661..d3d17d9e 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -3,6 +3,7 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" xmlns:views="clr-namespace:v2rayN.Desktop.Views" @@ -11,6 +12,7 @@ Width="900" Height="600" x:DataType="vms:AddServerViewModel" + Design.DataContext="{x:Static dd:DesignData.AddServer}" ShowInTaskbar="False" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs index 15b638c6..ed1f74a9 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml.cs @@ -1,3 +1,4 @@ +using System.Reactive.Disposables; using v2rayN.Desktop.Base; using v2rayN.Desktop.Common; @@ -6,11 +7,6 @@ namespace v2rayN.Desktop.Views; public partial class AddServerWindow : WindowBase { public AddServerWindow() - { - InitializeComponent(); - } - - public AddServerWindow(ProfileItem profileItem) { InitializeComponent(); @@ -22,8 +18,6 @@ public partial class AddServerWindow : WindowBase btnGUID.Click += btnGUID_Click; btnGUID5.Click += btnGUID_Click; - ViewModel = new AddServerViewModel(profileItem, UpdateViewHandler); - cmbCoreType.ItemsSource = Global.CoreTypes.AppendEmpty(); cmbNetwork.ItemsSource = Global.Networks; @@ -40,8 +34,173 @@ public partial class AddServerWindow : WindowBase cmbFingerprint2.ItemsSource = Global.Fingerprints; cmbAlpn.ItemsSource = Global.Alpns; - var lstStreamSecurity = new List { string.Empty, Global.StreamSecurity }; + gridTlsMore.IsVisible = false; + this.WhenActivated(disposables => + { + var configTypeBindings = new SerialDisposable().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.Address, v => v.txtAddress.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.SelectedSource.ConfigType) + .Subscribe(configType => + { + var currentTypeDisposables = new CompositeDisposable(); + configTypeBindings.Disposable = currentTypeDisposables; + + switch (configType) + { + case EConfigType.VMess: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.SelectedValue).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.Shadowsocks: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.SOCKS: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text) + .DisposeWith(disposables); + break; + + case EConfigType.HTTP: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text) + .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.HttpHeadersJson, v => v.txtHttpHeadersJson.Text) + .DisposeWith(disposables); + break; + + case EConfigType.VLESS: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.Trojan: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.Hysteria2: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId7.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.SalamanderPass, v => v.txtPath7.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.Ports, v => v.txtPorts7.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.TUIC: + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl8.SelectedValue).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.WireGuard: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.WgPresharedKey, v => v.txtPreSharedKey9.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.Anytls: + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId11.Text).DisposeWith(currentTypeDisposables); + break; + + case EConfigType.Naive: + this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId12.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity12.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.togNaiveQuic12.IsChecked).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.cmbCongestionControl12.IsEnabled).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl12.SelectedValue).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(currentTypeDisposables); + this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(currentTypeDisposables); + break; + } + }) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostRaw.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathRaw.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.KcpSeed, v => v.txtKcpSeed.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.KcpMtu, v => v.txtKcpMtu.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostWs.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathWs.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostHttpupgrade.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathHttpupgrade.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.XhttpMode, v => v.cmbHeaderTypeXhttp.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostXhttp.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Path, v => v.txtPathXhttp.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.XhttpExtra, v => v.txtExtraXhttp.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.GrpcMode, v => v.cmbHeaderTypeGrpc.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.GrpcAuthority, v => v.txtRequestHostGrpc.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.GrpcServiceName, v => v.txtPathGrpc.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.AllowInsecure, v => v.togAllowInsecure.IsChecked).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); + + //reality + this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.SelectedSource.Finalmask, v => v.txtFinalmask.Text).DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.SelectedSource) + .WhereNotNull() + .Subscribe(InitializeData) + .DisposeWith(disposables); + }); + + } + + private void InitializeData(ProfileItem profileItem) + { + Title = $"{profileItem.ConfigType}"; + var lstStreamSecurity = new List { string.Empty, Global.StreamSecurity }; switch (profileItem.ConfigType) { case EConfigType.VMess: @@ -127,159 +286,7 @@ public partial class AddServerWindow : WindowBase break; } cmbStreamSecurity.ItemsSource = lstStreamSecurity; - - gridTlsMore.IsVisible = false; - - this.WhenActivated(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.Address, v => v.txtAddress.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Port, v => v.txtPort.Text).DisposeWith(disposables); - - switch (profileItem.ConfigType) - { - case EConfigType.VMess: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.AlterId, v => v.txtAlterId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.VmessSecurity, v => v.cmbSecurity.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled.IsChecked).DisposeWith(disposables); - break; - - case EConfigType.Shadowsocks: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId3.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SsMethod, v => v.cmbSecurity3.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled3.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled3.IsChecked).DisposeWith(disposables); - break; - - case EConfigType.SOCKS: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); - break; - - case EConfigType.HTTP: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId4.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtSecurity4.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.HttpHeadersJson, v => v.txtHttpHeadersJson.Text).DisposeWith(disposables); - break; - - case EConfigType.VLESS: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId5.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow5.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.VlessEncryption, v => v.txtSecurity5.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled5.IsChecked).DisposeWith(disposables); - break; - - case EConfigType.Trojan: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId6.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Flow, v => v.cmbFlow6.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.MuxEnabled, v => v.togmuxEnabled6.IsChecked).DisposeWith(disposables); - break; - - case EConfigType.Hysteria2: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SalamanderPass, v => v.txtPath7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Ports, v => v.txtPorts7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.HopInterval, v => v.txtHopInt7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.UpMbps, v => v.txtUpMbps7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.DownMbps, v => v.txtDownMbps7.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Hy2RealmUrl, v => v.txtHy2RealmUrl.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.GeckoMinPacketSize, v => v.txtMinGeckoPacketSize.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.GeckoMaxPacketSize, v => v.txtMaxGeckoPacketSize.Text).DisposeWith(disposables); - break; - - case EConfigType.TUIC: - this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId8.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity8.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl8.SelectedValue).DisposeWith(disposables); - break; - - case EConfigType.WireGuard: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.WgPublicKey, v => v.txtPublicKey9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.WgPresharedKey, v => v.txtPreSharedKey9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.WgReserved, v => v.txtPath9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.WgInterfaceAddress, v => v.txtRequestHost9.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.WgMtu, v => v.txtShortId9.Text).DisposeWith(disposables); - break; - - case EConfigType.Anytls: - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtId11.Text).DisposeWith(disposables); - break; - - case EConfigType.Naive: - this.Bind(ViewModel, vm => vm.SelectedSource.Username, v => v.txtId12.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Password, v => v.txtSecurity12.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.togNaiveQuic12.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.NaiveQuic, v => v.cmbCongestionControl12.IsEnabled).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.CongestionControl, v => v.cmbCongestionControl12.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.InsecureConcurrency, v => v.txtInsecureConcurrency12.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Uot, v => v.togUotEnabled12.IsChecked).DisposeWith(disposables); - break; - } - this.Bind(ViewModel, vm => vm.SelectedSource.Network, v => v.cmbNetwork.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.RawHeaderType, v => v.cmbHeaderTypeRaw.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostRaw.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Path, v => v.txtPathRaw.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.KcpHeaderType, v => v.cmbHeaderTypeKcp.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.KcpSeed, v => v.txtKcpSeed.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.KcpMtu, v => v.txtKcpMtu.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostWs.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Path, v => v.txtPathWs.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostHttpupgrade.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Path, v => v.txtPathHttpupgrade.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.XhttpMode, v => v.cmbHeaderTypeXhttp.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Host, v => v.txtRequestHostXhttp.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Path, v => v.txtPathXhttp.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.XhttpExtra, v => v.txtExtraXhttp.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.GrpcMode, v => v.cmbHeaderTypeGrpc.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.GrpcAuthority, v => v.txtRequestHostGrpc.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.GrpcServiceName, v => v.txtPathGrpc.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.SelectedSource.StreamSecurity, v => v.cmbStreamSecurity.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.AllowInsecure, v => v.togAllowInsecure.IsChecked).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.VerifyPeerCertByName, v => v.txtVerifyPeerCertByName.Text).DisposeWith(disposables); - - //reality - this.Bind(ViewModel, vm => vm.SelectedSource.Sni, v => v.txtSNI2.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint2.SelectedValue).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.PublicKey, v => v.txtPublicKey.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.ShortId, v => v.txtShortId.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.SpiderX, v => v.txtSpiderX.Text).DisposeWith(disposables); - this.Bind(ViewModel, vm => vm.SelectedSource.Mldsa65Verify, v => v.txtMldsa65Verify.Text).DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.SelectedSource.Finalmask, v => v.txtFinalmask.Text).DisposeWith(disposables); - - this.BindCommand(ViewModel, vm => vm.FetchCertCmd, v => v.btnFetchCert).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.FetchCertChainCmd, v => v.btnFetchCertChain).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.SaveCmd, v => v.btnSave).DisposeWith(disposables); - }); - - Title = $"{profileItem.ConfigType}"; - } - - private async Task UpdateViewHandler(EViewAction action, object? obj) - { - switch (action) - { - case EViewAction.CloseWindow: - Close(true); - break; - } - return await Task.FromResult(true); + cmbStreamSecurity.SelectedItem = profileItem.StreamSecurity; } private void Window_Loaded(object? sender, RoutedEventArgs e) diff --git a/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml b/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml index 70d6b9a9..7c42854f 100644 --- a/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/BackupAndRestoreView.axaml @@ -3,10 +3,12 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:dd="clr-namespace:v2rayN.Desktop.DesignData" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib" d:DesignHeight="450" d:DesignWidth="800" + Design.DataContext="{x:Static dd:DesignData.BackupAndRestore}" mc:Ignorable="d">