ViewModel-First (#9621)

* Interaction

* Remove view action

* ViewModel-first

* MainGirdOrientation Hot Reload

* Fix avalonia preview

* Remove CloseWindowInteraction

* Fix

* Avoid threading issues

* Fix

* ProfilesSelect

* Try fix previewer

* Remove AppEvents.ProfilesRefreshRequested

* Remove AppEvents.SubscriptionsRefreshRequested

* Remove AppEvents.ProxiesReloadRequested

* Remove AppEvents.AdjustMainLvColWidthRequested

* Remove AppEvents.SetDefaultServerRequested

* Remove AppEvents.RoutingsMenuRefreshRequested

* Remove AppEvents.TestServerRequested

* Remove AppEvents.InboundDisplayRequested

* Remove AppEvents.SubscriptionsUpdateRequested

* Remove AppEvents.ShowHideWindowRequested

* Remove AppEvents.ReloadRequested

* Remove AppEvents.AddServerRequested

* Fix
This commit is contained in:
DHR60
2026-07-14 01:05:05 +00:00
committed by GitHub
parent 6f50206606
commit 9664ee380e
115 changed files with 2075 additions and 2190 deletions
@@ -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<ProfileItem> AllProfilePreviewItemsObs { get; } = new ObservableCollectionExtended<ProfileItem>();
//public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> AddCmd { get; }
public ReactiveCommand<Unit, Unit> RemoveCmd { get; }
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@@ -39,15 +41,18 @@ public class AddGroupServerViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddGroupServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? 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
{
@@ -1,7 +1,10 @@
namespace ServiceLib.ViewModels;
public class AddServer2ViewModel : MyReactiveObject
public class AddServer2ViewModel : MyReactiveObject, ICloseable
{
public event EventHandler? RequestClose;
public Interaction<Unit, string?> BrowseConfigFileInteraction { get; } = new();
[Reactive]
public ProfileItem SelectedSource { get; set; }
@@ -13,15 +16,18 @@ public class AddServer2ViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveServerCmd { get; }
public bool IsModified { get; set; }
public AddServer2ViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? 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
{
@@ -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<Unit, Unit> FetchCertChainCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public AddServerViewModel(ProfileItem profileItem, Func<EViewAction, object?, Task<bool>>? updateView)
public AddServerViewModel(ProfileItem profileItem)
{
_config = AppManager.Instance.Config;
_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
{
@@ -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<EViewAction, object?, Task<bool>>? updateView)
public BackupAndRestoreViewModel()
{
_config = AppManager.Instance.Config;
_updateView = updateView;
WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () =>
{
@@ -7,15 +7,16 @@ public class CheckUpdateViewModel : MyReactiveObject
private List<CheckUpdateModel> _lstUpdated = [];
private static readonly string _tag = "CheckUpdateViewModel";
public EventChannel<Unit> ReloadRequested { get; } = new();
public IObservableCollection<CheckUpdateModel> CheckUpdateModels { get; } = new ObservableCollectionExtended<CheckUpdateModel>();
public ReactiveCommand<Unit, Unit> CheckUpdateCmd { get; }
public ReactiveCommand<Unit, Unit> CheckOnlyCmd { get; }
[Reactive] public bool EnableCheckPreReleaseUpdate { get; set; }
public CheckUpdateViewModel(Func<EViewAction, object?, Task<bool>>? 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
{
@@ -16,10 +16,9 @@ public class ClashConnectionsViewModel : MyReactiveObject
[Reactive]
public bool AutoRefresh { get; set; }
public ClashConnectionsViewModel(Func<EViewAction, object?, Task<bool>>? updateView)
public ClashConnectionsViewModel()
{
_config = AppManager.Instance.Config;
_updateView = updateView;
AutoRefresh = _config.ClashUIItem.ConnectionsAutoRefresh;
var canEditRemove = this.WhenAnyValue(
@@ -33,10 +33,9 @@ public class ClashProxiesViewModel : MyReactiveObject
[Reactive]
public bool AutoRefresh { get; set; }
public ClashProxiesViewModel(Func<EViewAction, object?, Task<bool>>? 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();
}
@@ -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<Unit, Unit> ImportDefConfig4V2rayCompatibleCmd { get; }
public ReactiveCommand<Unit, Unit> ImportDefConfig4SingboxCompatibleCmd { get; }
public DNSSettingViewModel(Func<EViewAction, object?, Task<bool>>? 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);
}
}
@@ -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<Unit, Unit> SaveCmd { get; }
#endregion Reactive
public FullConfigTemplateViewModel(Func<EViewAction, object?, Task<bool>>? 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<bool> SaveXrayConfigAsync()
@@ -1,15 +1,16 @@
namespace ServiceLib.ViewModels;
public class GlobalHotkeySettingViewModel : MyReactiveObject
public class GlobalHotkeySettingViewModel : MyReactiveObject, ICloseable
{
public event EventHandler? RequestClose;
private readonly List<KeyEventItem> _globalHotkeys;
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public GlobalHotkeySettingViewModel(Func<EViewAction, object?, Task<bool>>? 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
{
@@ -4,6 +4,21 @@ namespace ServiceLib.ViewModels;
public class MainWindowViewModel : MyReactiveObject
{
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, byte[]?> ScanScreenInteraction { get; } = new();
public Interaction<Unit, string?> BrowseImageFileInteraction { get; } = new();
public Interaction<bool?, Unit> ShowHideWindowInteraction { get; } = new();
public bool DesignMode { get; set; }
public ProfilesViewModel ProfilesViewModel { get; } = new();
public MsgViewModel MsgViewModel { get; } = new();
public ClashProxiesViewModel ClashProxiesViewModel { get; } = new();
public ClashConnectionsViewModel ClashConnectionsViewModel { get; } = new();
public CheckUpdateViewModel CheckUpdateViewModel { get; } = new();
public BackupAndRestoreViewModel BackupAndRestoreViewModel { get; } = new();
public StatusBarViewModel StatusBarViewModel { get; } = StatusBarViewModel.Instance;
#region Menu
//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<EViewAction, object?, Task<bool>>? 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<IObservable<Unit>>
{
ProfilesViewModel.ReloadRequested.AsObservable(),
StatusBarViewModel.ReloadRequested.AsObservable(),
CheckUpdateViewModel.ReloadRequested.AsObservable(),
};
foreach (var reloadRequested in vmReloadRequestedList)
{
reloadRequested
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await Reload());
}
StatusBarViewModel.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
StatusBarViewModel.AddServerViaClipboardRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null));
StatusBarViewModel.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blShow =>
{
await ShowHideWindowInteraction.Handle(blShow);
});
StatusBarViewModel.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async indexId => await ProfilesViewModel.SetDefaultServer(indexId));
StatusBarViewModel.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
_ = Init();
}
@@ -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();
+4 -3
View File
@@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject
{
public Interaction<string, Unit> DispatcherShowMsgInteraction { get; } = new();
private readonly ConcurrentQueue<string> _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<EViewAction, object?, Task<bool>>? 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
{
@@ -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<Unit, Unit> SaveCmd { get; }
public OptionSettingViewModel(Func<EViewAction, object?, Task<bool>>? 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
{
@@ -1,7 +1,10 @@
namespace ServiceLib.ViewModels;
public class ProfilesSelectViewModel : MyReactiveObject
public class ProfilesSelectViewModel : MyReactiveObject, ICloseable
{
public event EventHandler? RequestClose;
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new();
#region private prop
private string _serverFilter = string.Empty;
@@ -12,6 +15,8 @@ public class ProfilesSelectViewModel : MyReactiveObject
#endregion private prop
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
#region ObservableCollection
public IObservableCollection<ProfileItemModel> ProfileItems { get; } = new ObservableCollectionExtended<ProfileItemModel>();
@@ -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<EViewAction, object?, Task<bool>>? 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<Unit, Unit>)
{
}
}
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()
@@ -2,6 +2,16 @@ namespace ServiceLib.ViewModels;
public class ProfilesViewModel : MyReactiveObject
{
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<ProfileItem, bool> SaveFileDialogInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, Unit> ProfilesFocusInteraction { get; } = new();
public Interaction<string, Unit> ShareServerInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshServersBizInteraction { get; } = new();
public Interaction<Unit, Unit> AdjustMainLvColWidthInteraction { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new();
#region private prop
private List<ProfileItem> _lstProfile;
@@ -82,10 +92,9 @@ public class ProfilesViewModel : MyReactiveObject
#region Init
public ProfilesViewModel(Func<EViewAction, object?, Task<bool>>? 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<Unit, Unit>)
{
}
}
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<List<ProfileItem>>(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<Unit, Unit>)
{
}
}
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<List<ProfileItemModel>?> 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;
}
@@ -1,7 +1,9 @@
namespace ServiceLib.ViewModels;
public class RoutingRuleDetailsViewModel : MyReactiveObject
public class RoutingRuleDetailsViewModel : MyReactiveObject, ICloseable
{
public event EventHandler? RequestClose;
public IList<string> ProtocolItems { get; set; }
public IList<string> InboundTagItems { get; set; }
@@ -23,13 +25,17 @@ public class RoutingRuleDetailsViewModel : MyReactiveObject
[Reactive]
public bool AutoSort { get; set; }
public ReactiveCommand<Unit, Unit> SelectProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public RoutingRuleDetailsViewModel(RulesItem rulesItem, Func<EViewAction, object?, Task<bool>>? updateView)
public RoutingRuleDetailsViewModel(RulesItem rulesItem)
{
_config = AppManager.Instance.Config;
_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);
}
}
}
@@ -1,7 +1,13 @@
namespace ServiceLib.ViewModels;
public class RoutingRuleSettingViewModel : MyReactiveObject
public class RoutingRuleSettingViewModel : MyReactiveObject, ICloseable
{
public event EventHandler? RequestClose;
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> ReadTextFromClipboardInteraction { get; } = new();
public Interaction<Unit, string?> BrowseRulesFileInteraction { get; } = new();
private List<RulesItem> _rules;
[Reactive]
@@ -27,10 +33,9 @@ public class RoutingRuleSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public RoutingRuleSettingViewModel(RoutingItem routingItem, Func<EViewAction, object?, Task<bool>>? 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<int> 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;
}
@@ -2,6 +2,8 @@ namespace ServiceLib.ViewModels;
public class RoutingSettingViewModel : MyReactiveObject
{
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
#region Reactive
public IObservableCollection<RoutingItemModel> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItemModel>();
@@ -26,10 +28,9 @@ public class RoutingSettingViewModel : MyReactiveObject
#endregion Reactive
public RoutingSettingViewModel(Func<EViewAction, object?, Task<bool>>? 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;
}
@@ -2,9 +2,20 @@ namespace ServiceLib.ViewModels;
public class StatusBarViewModel : MyReactiveObject
{
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new(null));
public Interaction<string, Unit> SetClipboardDataInteraction { get; } = new();
public Interaction<Unit, string?> PasswordInputInteraction { get; } = new();
public Interaction<Unit, Unit> DispatcherRefreshIconInteraction { get; } = new();
public EventChannel<bool> SubscriptionsUpdateRequested { get; } = new();
public EventChannel<bool?> ShowHideWindowRequested { get; } = new();
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new());
public static StatusBarViewModel Instance => _instance.Value;
public EventChannel<string> SetDefaultServerRequested { get; } = new();
public EventChannel<Unit> ReloadRequested { get; } = new();
public EventChannel<Unit> AddServerViaScanRequested { get; } = new();
public EventChannel<Unit> AddServerViaClipboardRequested { get; } = new();
#region ObservableCollection
public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>();
@@ -92,7 +103,7 @@ public class StatusBarViewModel : MyReactiveObject
#endregion UI
public StatusBarViewModel(Func<EViewAction, object?, Task<bool>>? 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<EViewAction, object?, Task<bool>>? updateView)
{
_updateView = updateView;
if (_updateView != null)
{
AppEvents.ProfilesRefreshRequested
.AsObservable()
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz()); //.DisposeWith(_disposables);
}
}
private async Task CopyProxyCmdToClipboard()
{
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<Unit, Unit>)
{
// 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)}");
@@ -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<Unit, Unit> SelectPrevProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SelectNextProfileCmd { get; }
public ReactiveCommand<Unit, Unit> SaveCmd { get; }
public SubEditViewModel(SubItem subItem, Func<EViewAction, object?, Task<bool>>? updateView)
public SubEditViewModel(SubItem subItem)
{
_config = AppManager.Instance.Config;
_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<ProfileItem?> SelectProfileAsync()
{
var profileSelectViewModel = new ProfilesSelectViewModel();
profileSelectViewModel.SetConfigTypeFilter([EConfigType.Custom], exclude: true);
var result = await AppManager.Instance.WindowDialog.ShowDialogAsync(profileSelectViewModel);
if (result != true)
{
return null;
}
var profileItem = await profileSelectViewModel.GetProfileItem();
return profileItem;
}
}
@@ -2,6 +2,9 @@ namespace ServiceLib.ViewModels;
public class SubSettingViewModel : MyReactiveObject
{
public Interaction<string, bool> ShowYesNoInteraction { get; } = new();
public Interaction<string, Unit> ShareSubInteraction { get; } = new();
public IObservableCollection<SubItem> SubItems { get; } = new ObservableCollectionExtended<SubItem>();
[Reactive]
@@ -15,10 +18,9 @@ public class SubSettingViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SubShareCmd { get; }
public bool IsModified { get; set; }
public SubSettingViewModel(Func<EViewAction, object?, Task<bool>>? 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;
}