diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs index af61d17c..c8f61860 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -37,6 +37,8 @@ namespace Wabbajack services.AddSingleton(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); return services; diff --git a/Wabbajack.App.Wpf/Messages/NavigateBack.cs b/Wabbajack.App.Wpf/Messages/NavigateBack.cs new file mode 100644 index 00000000..049c67c8 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/NavigateBack.cs @@ -0,0 +1,11 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; + +public class NavigateBack +{ + public static void Send() + { + MessageBus.Current.SendMessage(new NavigateBack()); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/NavigateTo.cs b/Wabbajack.App.Wpf/Messages/NavigateTo.cs new file mode 100644 index 00000000..ac42ceaf --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/NavigateTo.cs @@ -0,0 +1,21 @@ +using ReactiveUI; +using Wabbajack.Lib; + +namespace Wabbajack.Messages; + +public class NavigateTo +{ + + public ViewModel ViewModel { get; } + private NavigateTo(ViewModel vm) + { + ViewModel = vm; + } + + public static void Send(T vm) + where T : ViewModel + { + MessageBus.Current.SendMessage(new NavigateTo(vm)); + } + +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs b/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs new file mode 100644 index 00000000..55263b7b --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs @@ -0,0 +1,29 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; + +public class NavigateToGlobal +{ + public enum ScreenType + { + ModeSelectionView, + ModListGallery, + Installer, + Settings, + Compiler, + ModListContents + } + + public ScreenType Screen { get; } + + private NavigateToGlobal(ScreenType screen) + { + Screen = screen; + } + + public static void Send(ScreenType screen) + { + MessageBus.Current.SendMessage(new NavigateToGlobal(screen)); + } + +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs b/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs index 9c6575c4..8ae8e46b 100644 --- a/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs +++ b/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs @@ -1,5 +1,6 @@ using System; using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using Microsoft.Extensions.Logging; @@ -7,6 +8,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.Messages; namespace Wabbajack { @@ -24,27 +26,30 @@ namespace Wabbajack [Reactive] public ViewModel NavigateBackTarget { get; set; } public ReactiveCommand BackCommand { get; protected set; } - - protected ObservableAsPropertyHelper _IsActive; - public bool IsActive => _IsActive.Value; + + [Reactive] + public bool IsActive { get; set; } public Subject IsBackEnabledSubject { get; } = new Subject(); public IObservable IsBackEnabled { get; } - public BackNavigatingVM(ILogger logger, MainWindowVM mainWindowVM) + public BackNavigatingVM(ILogger logger) { IsBackEnabled = IsBackEnabledSubject.StartWith(true); BackCommand = ReactiveCommand.Create( execute: () => logger.CatchAndLog(() => { - mainWindowVM.NavigateTo(NavigateBackTarget); + NavigateBack.Send(); Unload(); }), canExecute: this.ConstructCanNavigateBack() .ObserveOnGuiThread()); - - _IsActive = this.ConstructIsActive(mainWindowVM) - .ToGuiProperty(this, nameof(IsActive)); + + this.WhenActivated(disposables => + { + IsActive = true; + Disposable.Create(() => IsActive = false).DisposeWith(disposables); + }); } public virtual void Unload() @@ -60,7 +65,7 @@ namespace Wabbajack .CombineLatest(vm.IsBackEnabled) .Select(x => x.First != null && x.Second); } - + public static IObservable ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm) { return mwvm.WhenAny(x => x.ActivePane) diff --git a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs index d7a8f23d..61760ab1 100644 --- a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs @@ -13,6 +13,7 @@ using Microsoft.Extensions.Logging; using Wabbajack.Compiler; using Wabbajack.Lib.Extensions; using Wabbajack.Lib.Interventions; +using Wabbajack.Messages; using Wabbajack.RateLimiter; namespace Wabbajack @@ -64,7 +65,7 @@ namespace Wabbajack private readonly ObservableAsPropertyHelper<(int CurrentCPUs, int DesiredCPUs)> _CurrentCpuCount; public (int CurrentCPUs, int DesiredCPUs) CurrentCpuCount => _CurrentCpuCount.Value; - public CompilerVM(ILogger logger, MainWindowVM mainWindowVM) : base(logger, mainWindowVM) + public CompilerVM(ILogger logger, MainWindowVM mainWindowVM) : base(logger) { MWVM = mainWindowVM; @@ -133,7 +134,7 @@ namespace Wabbajack BackCommand = ReactiveCommand.Create( execute: () => { - mainWindowVM.NavigateTo(mainWindowVM.ModeSelectionVM); + NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); StartedCompilation = false; Completed = null; }, diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs index 8b378ab9..d61662c0 100644 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs +++ b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs @@ -1,9 +1,13 @@ -using System; + + +using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading.Tasks; using System.Windows.Input; using DynamicData; using DynamicData.Binding; @@ -17,6 +21,7 @@ using Wabbajack.DTOs; using Wabbajack.Hashing.xxHash64; using Wabbajack.Lib.Extensions; using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Services.OSIntegrated; using Wabbajack.Services.OSIntegrated.Services; namespace Wabbajack @@ -24,68 +29,48 @@ namespace Wabbajack public class ModListGalleryVM : BackNavigatingVM { public MainWindowVM MWVM { get; } - - public ObservableCollectionExtended ModLists { get; } = new ObservableCollectionExtended(); + + private readonly SourceCache _modLists = new(x => x.Metadata.Links.MachineURL); + public ReadOnlyObservableCollection _filteredModLists; + public ReadOnlyObservableCollection ModLists => _filteredModLists; private const string ALL_GAME_TYPE = "All"; - [Reactive] - public IErrorResponse Error { get; set; } + [Reactive] public IErrorResponse Error { get; set; } - [Reactive] - public string Search { get; set; } + [Reactive] public string Search { get; set; } - [Reactive] - public bool OnlyInstalled { get; set; } + [Reactive] public bool OnlyInstalled { get; set; } - [Reactive] - public bool ShowNSFW { get; set; } + [Reactive] public bool ShowNSFW { get; set; } - [Reactive] - public bool ShowUtilityLists { get; set; } + [Reactive] public bool ShowUtilityLists { get; set; } - [Reactive] - public string GameType { get; set; } + [Reactive] public string GameType { get; set; } - public List GameTypeEntries { get { return GetGameTypeEntries(); } } + public List GameTypeEntries => GetGameTypeEntries(); - private readonly ObservableAsPropertyHelper _Loaded; + private ObservableAsPropertyHelper _Loaded; private readonly Client _wjClient; private readonly ILogger _logger; private readonly GameLocator _locator; + private readonly ModListDownloadMaintainer _maintainer; - private FiltersSettings settings => MWVM.Settings.Filters; + private FiltersSettings settings { get; set; } = new(); public bool Loaded => _Loaded.Value; - public ICommand ClearFiltersCommand { get; } + public ICommand ClearFiltersCommand { get; set; } - public ModListGalleryVM(ILogger logger, MainWindowVM mainWindowVM, Client wjClient, - GameLocator locator, IServiceProvider provider) - : base(logger, mainWindowVM) + public ModListGalleryVM(ILogger logger, Client wjClient, + GameLocator locator, SettingsManager settingsManager, ModListDownloadMaintainer maintainer) + : base(logger) { - MWVM = mainWindowVM; _wjClient = wjClient; _logger = logger; _locator = locator; - - // load persistent filter settings - if (settings.IsPersistent) - { - GameType = !string.IsNullOrEmpty(settings.Game) ? settings.Game : ALL_GAME_TYPE; - ShowNSFW = settings.ShowNSFW; - ShowUtilityLists = settings.ShowUtilityLists; - OnlyInstalled = settings.OnlyInstalled; - Search = settings.Search; - } - else - GameType = ALL_GAME_TYPE; - - // subscribe to save signal - MWVM.Settings.SaveSignal - .Subscribe(_ => UpdateFiltersSettings()) - .DisposeWith(this.CompositeDisposable); - + _maintainer = maintainer; + ClearFiltersCommand = ReactiveCommand.Create( () => { @@ -97,101 +82,16 @@ namespace Wabbajack }); - this.WhenAny(x => x.OnlyInstalled) - .Subscribe(val => - { - if(val) - GameType = ALL_GAME_TYPE; - }) - .DisposeWith(CompositeDisposable); - - var sourceList = Observable.Return(Unit.Default) - .ObserveOn(RxApp.TaskpoolScheduler) - .SelectMany(async _ => - { - try - { - Error = null; - var list = await _wjClient.LoadLists(); - Error = ErrorResponse.Success; - return list - .AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? default); - } - catch (Exception ex) - { - _logger.LogError(ex, "While Loading Lists"); - Error = ErrorResponse.Fail(ex); - return Observable.Empty>(); - } - }) - // Unsubscribe and release when not active - .FlowSwitch( - this.WhenAny(x => x.IsActive), - valueWhenOff: Observable.Return(ChangeSet.Empty)) - .Switch() - .RefCount(); - - _Loaded = sourceList.CollectionCount() - .Select(c => c > 0) - .ToProperty(this, nameof(Loaded)); - - // Convert to VM and bind to resulting list - sourceList - .ObserveOnGuiThread() - .Transform(m => new ModListMetadataVM(provider.GetRequiredService>(),this, m, - provider.GetRequiredService(), provider.GetRequiredService())) - .DisposeMany() - // Filter only installed - .Filter(this.WhenAny(x => x.OnlyInstalled) - .Select>(onlyInstalled => (vm) => - { - if (!onlyInstalled) return true; - if (!GameRegistry.Games.TryGetValue(vm.Metadata.Game, out var gameMeta)) return false; - return _locator.IsInstalled(gameMeta.Game); - })) - // Filter on search box - .Filter(this.WhenAny(x => x.Search) - .Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) - .Select>(search => (vm) => - { - if (string.IsNullOrWhiteSpace(search)) return true; - return vm.Metadata.Title.ContainsCaseInsensitive(search) || vm.Metadata.tags.Any(t => t.ContainsCaseInsensitive(search)); - })) - .Filter(this.WhenAny(x => x.ShowNSFW) - .Select>(showNSFW => vm => - { - if (!vm.Metadata.NSFW) return true; - return vm.Metadata.NSFW && showNSFW; - })) - .Filter(this.WhenAny(x => x.ShowUtilityLists) - .Select>(showUtilityLists => vm => showUtilityLists ? vm.Metadata.UtilityList : !vm.Metadata.UtilityList)) - // Filter by Game - .Filter(this.WhenAny(x => x.GameType) - .Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) - .Select>(GameType => (vm) => - { - if (GameType == ALL_GAME_TYPE) - return true; - if (string.IsNullOrEmpty(GameType)) - return false; - - return GameType == vm.Metadata.Game.GetDescription().ToString(); - - })) - .Bind(ModLists) - .Subscribe() - .DisposeWith(CompositeDisposable); - - // Extra GC when navigating away, just to immediately clean up modlist metadata - this.WhenAny(x => x.IsActive) - .Where(x => !x) - .Skip(1) - .Delay(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler) - .Subscribe(_ => - { - GC.Collect(); - }) - .DisposeWith(CompositeDisposable); + this.WhenActivated(disposables => + { + var _ = LoadModLists(); + + _modLists.Connect() + .ObserveOn(RxApp.MainThreadScheduler) + .Bind(out _filteredModLists) + .Subscribe() + .DisposeWith(disposables); + }); } public override void Unload() @@ -199,9 +99,30 @@ namespace Wabbajack Error = null; } + private async Task LoadModLists() + { + using var ll = LoadingLock.WithLoading(); + try + { + var modLists = await _wjClient.LoadLists(); + _modLists.Edit(e => + { + e.Clear(); + e.AddOrUpdate(modLists.Select(m => + new ModListMetadataVM(_logger, this, m, _maintainer, _wjClient))); + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "While loading lists"); + ll.Fail(); + } + ll.Succeed(); + } + private List GetGameTypeEntries() { - List gameEntries = new List { ALL_GAME_TYPE }; + List gameEntries = new List {ALL_GAME_TYPE}; gameEntries.AddRange(GameRegistry.Games.Values.Select(gameType => gameType.HumanFriendlyGameName)); gameEntries.Sort(); return gameEntries; @@ -216,4 +137,4 @@ namespace Wabbajack settings.OnlyInstalled = OnlyInstalled; } } -} +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs index be5a12f9..969a3a8d 100644 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs +++ b/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs @@ -16,6 +16,7 @@ using Wabbajack.DTOs; using Wabbajack.DTOs.ServerResponses; using Wabbajack.Lib; using Wabbajack.Lib.Extensions; +using Wabbajack.Messages; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Paths; using Wabbajack.Paths.IO; @@ -126,7 +127,7 @@ namespace Wabbajack ArchiveStatus = a.Status, IsFailing = a.Status != ArchiveStatus.InValid })); - _parent.MWVM.NavigateTo(_parent.MWVM.ModListContentsVM.Value); + NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModListContents); } finally { diff --git a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs index bebe24ca..6715d3c2 100644 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs @@ -15,6 +15,7 @@ using Microsoft.WindowsAPICodePack.Shell; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Lib.Extensions; using Wabbajack.Lib.Interventions; +using Wabbajack.Messages; using Wabbajack.RateLimiter; using Wabbajack.View_Models; using Wabbajack.Paths.IO; @@ -98,7 +99,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM public ReactiveCommand GoToInstallCommand { get; } public ReactiveCommand BeginCommand { get; } - public InstallerVM(ILogger logger, MainWindowVM mainWindowVM, IServiceProvider serviceProvider) : base(logger, mainWindowVM) + public InstallerVM(ILogger logger, MainWindowVM mainWindowVM, IServiceProvider serviceProvider) : base(logger) { _logger = logger; @@ -162,10 +163,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM MWVM.Settings.Installer.LastInstalledListLocation = ModListLocation.TargetPath; }) .DisposeWith(CompositeDisposable); - - _IsActive = this.ConstructIsActive(MWVM) - .ToGuiProperty(this, nameof(IsActive)); - + // Active path represents the path to currently have loaded // If we're not actively showing, then "unload" the active path var activePath = Observable.CombineLatest( @@ -239,7 +237,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM { StartedInstallation = false; Completed = null; - mainWindowVM.NavigateTo(mainWindowVM.ModeSelectionVM); + NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); }, canExecute: Observable.CombineLatest( this.WhenAny(x => x.Installing) diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs index 6e0975ca..6a17ae4e 100644 --- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs +++ b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs @@ -3,6 +3,7 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using System; using System.Diagnostics; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; using System.Threading.Tasks; @@ -14,6 +15,7 @@ using Wabbajack.Common; using Wabbajack.Downloaders.GameFile; using Wabbajack.Lib; using Wabbajack.Lib.Interventions; +using Wabbajack.Messages; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Paths; using Wabbajack.View_Models; @@ -38,7 +40,7 @@ namespace Wabbajack public readonly Lazy Compiler; public readonly Lazy Installer; public readonly Lazy SettingsPane; - public readonly Lazy Gallery; + public readonly ModListGalleryVM Gallery; public readonly ModeSelectionVM ModeSelectionVM; public readonly Lazy ModListContentsVM; public readonly UserInterventionHandlers UserInterventionHandlers; @@ -55,7 +57,7 @@ namespace Wabbajack public bool UpdateAvailable { get; private set; } public MainWindowVM(ILogger logger, MainSettings settings, Client wjClient, - IServiceProvider serviceProvider) + IServiceProvider serviceProvider, ModeSelectionVM modeSelectionVM, ModListGalleryVM modListGalleryVM) { _logger = logger; _wjClient = wjClient; @@ -64,12 +66,15 @@ namespace Wabbajack Installer = new Lazy(() => new InstallerVM(serviceProvider.GetRequiredService>(), this, serviceProvider)); Compiler = new Lazy(() => new CompilerVM(serviceProvider.GetRequiredService>(), this)); SettingsPane = new Lazy(() => new SettingsVM(serviceProvider.GetRequiredService>(), this, serviceProvider)); - Gallery = new Lazy(() => new ModListGalleryVM(serviceProvider.GetRequiredService>(), this, - serviceProvider.GetRequiredService(), serviceProvider.GetRequiredService(), serviceProvider)); - ModeSelectionVM = new ModeSelectionVM(this); + Gallery = modListGalleryVM; + ModeSelectionVM = modeSelectionVM; ModListContentsVM = new Lazy(() => new ModListContentsVM(serviceProvider.GetRequiredService>(), this)); UserInterventionHandlers = new UserInterventionHandlers(serviceProvider.GetRequiredService>(), this); + MessageBus.Current.Listen() + .Subscribe(m => HandleNavigateTo(m.Screen)) + .DisposeWith(CompositeDisposable); + // Set up logging /* TODO Utils.LogMessages @@ -116,12 +121,12 @@ namespace Wabbajack if (IsStartingFromModlist(out var path)) { Installer.Value.ModListLocation.TargetPath = path; - NavigateTo(Installer.Value); + NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); } else { // Start on mode selection - NavigateTo(ModeSelectionVM); + NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); } try @@ -147,7 +152,19 @@ namespace Wabbajack OpenSettingsCommand = ReactiveCommand.Create( canExecute: this.WhenAny(x => x.ActivePane) .Select(active => !SettingsPane.IsValueCreated || !object.ReferenceEquals(active, SettingsPane.Value)), - execute: () => NavigateTo(SettingsPane.Value)); + execute: () => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Settings)); + } + + private void HandleNavigateTo(NavigateToGlobal.ScreenType s) + { + ActivePane = s switch + { + NavigateToGlobal.ScreenType.ModeSelectionView => ModeSelectionVM, + NavigateToGlobal.ScreenType.ModListGallery => Gallery, + NavigateToGlobal.ScreenType.Installer => Installer.Value, + NavigateToGlobal.ScreenType.Settings => SettingsPane.Value, + _ => ActivePane + }; } private static bool IsStartingFromModlist(out AbsolutePath modlistPath) @@ -171,21 +188,23 @@ namespace Wabbajack if (path == default) return; var installer = Installer.Value; Settings.Installer.LastInstalledListLocation = path; - NavigateTo(installer); + NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); installer.ModListLocation.TargetPath = path; } + /* public void NavigateTo(ViewModel vm) { ActivePane = vm; - } + }*/ + /* public void NavigateTo(T vm) where T : ViewModel, IBackNavigatingVM { vm.NavigateBackTarget = ActivePane; ActivePane = vm; - } + }*/ public async Task ShutdownApplication() { diff --git a/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs b/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs index cae3993b..558f6777 100644 --- a/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs +++ b/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs @@ -31,7 +31,7 @@ namespace Wabbajack.View_Models private static readonly Regex NameMatcher = new(@"(?<=\.)[^\.]+(?=\+State)", RegexOptions.Compiled); private readonly ILogger _logger; - public ModListContentsVM(ILogger logger, MainWindowVM mwvm) : base(logger, mwvm) + public ModListContentsVM(ILogger logger, MainWindowVM mwvm) : base(logger) { _logger = logger; _mwvm = mwvm; diff --git a/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs b/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs index a7077bb4..ce4fd331 100644 --- a/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs +++ b/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using System.Windows.Input; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.Messages; using Wabbajack.Paths.IO; namespace Wabbajack @@ -20,23 +21,23 @@ namespace Wabbajack public ICommand CompileCommand { get; } public ReactiveCommand UpdateCommand { get; } - public ModeSelectionVM(MainWindowVM mainVM) + public ModeSelectionVM() { - _mainVM = mainVM; - InstallCommand = ReactiveCommand.Create( execute: () => { + /* TODO var path = mainVM.Settings.Installer.LastInstalledListLocation; if (path == default || !path.FileExists()) { path = UIUtils.OpenFileDialog($"*{Ext.Wabbajack}|*{Ext.Wabbajack}"); } _mainVM.OpenInstaller(path); + */ }); - CompileCommand = ReactiveCommand.Create(() => mainVM.NavigateTo(mainVM.Compiler.Value)); - BrowseCommand = ReactiveCommand.Create(() => mainVM.NavigateTo(mainVM.Gallery.Value)); + CompileCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Compiler)); + BrowseCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModListGallery)); } } } diff --git a/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs b/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs index ae0d769e..8c7a14b9 100644 --- a/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs +++ b/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs @@ -34,7 +34,7 @@ namespace Wabbajack.View_Models.Settings private readonly Client _wjClient; private IObservable IsUploading { get; } - public AuthorFilesVM(ILogger logger, WabbajackApiTokenProvider token, Client wjClient, SettingsVM vm) : base(logger, vm.MWVM) + public AuthorFilesVM(ILogger logger, WabbajackApiTokenProvider token, Client wjClient, SettingsVM vm) : base(logger) { _token = token; _wjClient = wjClient; diff --git a/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs b/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs index 70f2cb40..ab6cc2a1 100644 --- a/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs @@ -20,7 +20,7 @@ namespace Wabbajack public List Downloaders { get; } public LoginManagerVM(ILogger logger, SettingsVM settingsVM) - : base(logger, settingsVM.MWVM) + : base(logger) { /* Downloaders = DownloadDispatcher.Downloaders diff --git a/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs b/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs index 50e63593..af1ddd54 100644 --- a/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs +++ b/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs @@ -28,7 +28,7 @@ namespace Wabbajack public ICommand OpenTerminalCommand { get; } public SettingsVM(ILogger logger, MainWindowVM mainWindowVM, IServiceProvider provider) - : base(logger, mainWindowVM) + : base(logger) { MWVM = mainWindowVM; Login = new LoginManagerVM(provider.GetRequiredService>(), this); diff --git a/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs b/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs index 1699569a..2d7b2ad9 100644 --- a/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs +++ b/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs @@ -7,6 +7,7 @@ using ReactiveUI; using Wabbajack.Common; using Wabbajack.Lib; using Wabbajack.Lib.Interventions; +using Wabbajack.Messages; namespace Wabbajack { @@ -28,11 +29,11 @@ namespace Wabbajack var cancel = new CancellationTokenSource(); var oldPane = MainWindow.ActivePane; using var vm = await WebBrowserVM.GetNew(_logger); - MainWindow.NavigateTo(vm); + NavigateTo.Send(vm); vm.BackCommand = ReactiveCommand.Create(() => { cancel.Cancel(); - MainWindow.NavigateTo(oldPane); + NavigateTo.Send(oldPane); intervention.Cancel(); }); @@ -54,7 +55,7 @@ namespace Wabbajack wait.Dispose(); } - MainWindow.NavigateTo(oldPane); + NavigateTo.Send(oldPane); } public async Task Handle(IStatusMessage msg) diff --git a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs index fc142828..cc22413c 100644 --- a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs @@ -19,19 +19,19 @@ namespace Wabbajack this.WhenAny(x => x.ViewModel.ModLists) .BindToStrict(this, x => x.ModListGalleryControl.ItemsSource) .DisposeWith(dispose); - Observable.CombineLatest( - this.WhenAny(x => x.ViewModel.Error), - this.WhenAny(x => x.ViewModel.Loaded), - resultSelector: (err, loaded) => - { - if (!err?.Succeeded ?? false) return true; - return !loaded; - }) - .DistinctUntilChanged() + + this.WhenAny(x => x.ViewModel.LoadingLock.IsLoading) .Select(x => x ? Visibility.Visible : Visibility.Collapsed) .StartWith(Visibility.Collapsed) - .BindToStrict(this, x => x.LoadingRing.Visibility) + .BindTo(this, x => x.LoadingRing.Visibility) .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.LoadingLock.ErrorState) + .Select(e => (e?.Succeeded ?? true) ? Visibility.Collapsed : Visibility.Visible) + .StartWith(Visibility.Collapsed) + .BindToStrict(this, x => x.ErrorIcon.Visibility) + .DisposeWith(dispose); + Observable.CombineLatest( this.WhenAny(x => x.ViewModel.ModLists.Count) .Select(x => x > 0), @@ -45,11 +45,7 @@ namespace Wabbajack .StartWith(Visibility.Collapsed) .BindToStrict(this, x => x.NoneFound.Visibility) .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.Error) - .Select(e => (e?.Succeeded ?? true) ? Visibility.Collapsed : Visibility.Visible) - .StartWith(Visibility.Collapsed) - .BindToStrict(this, x => x.ErrorIcon.Visibility) - .DisposeWith(dispose); + this.BindStrict(ViewModel, vm => vm.Search, x => x.SearchBox.Text) .DisposeWith(dispose); diff --git a/Wabbajack.App.Wpf/Wabbajack.csproj b/Wabbajack.App.Wpf/Wabbajack.csproj index 09e69ee9..8eb9a28f 100644 --- a/Wabbajack.App.Wpf/Wabbajack.csproj +++ b/Wabbajack.App.Wpf/Wabbajack.csproj @@ -60,7 +60,7 @@ NU1701 - + NU1701 @@ -78,9 +78,9 @@ - - - + + + @@ -104,4 +104,8 @@ + + + + diff --git a/Wabbajack.App/Controls/BrowseItemViewModel.cs b/Wabbajack.App/Controls/BrowseItemViewModel.cs index dfa9d9b4..9a1218a2 100644 --- a/Wabbajack.App/Controls/BrowseItemViewModel.cs +++ b/Wabbajack.App/Controls/BrowseItemViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Net.Http; using System.Reactive; diff --git a/Wabbajack.App.Wpf/Error States/ErrorResponse.cs b/Wabbajack.Lib/Error States/ErrorResponse.cs similarity index 95% rename from Wabbajack.App.Wpf/Error States/ErrorResponse.cs rename to Wabbajack.Lib/Error States/ErrorResponse.cs index 2aedfe1b..871e5507 100644 --- a/Wabbajack.App.Wpf/Error States/ErrorResponse.cs +++ b/Wabbajack.Lib/Error States/ErrorResponse.cs @@ -4,8 +4,8 @@ namespace Wabbajack { public struct ErrorResponse : IErrorResponse { - public readonly static ErrorResponse Success = Succeed(); - public readonly static ErrorResponse Failure = new ErrorResponse(); + public static readonly ErrorResponse Success = Succeed(); + public static readonly ErrorResponse Failure = new ErrorResponse(); public bool Succeeded { get; } public Exception? Exception { get; } diff --git a/Wabbajack.App.Wpf/Error States/GetResponse.cs b/Wabbajack.Lib/Error States/GetResponse.cs similarity index 100% rename from Wabbajack.App.Wpf/Error States/GetResponse.cs rename to Wabbajack.Lib/Error States/GetResponse.cs diff --git a/Wabbajack.Lib/LoadingLock.cs b/Wabbajack.Lib/LoadingLock.cs new file mode 100644 index 00000000..c09ce188 --- /dev/null +++ b/Wabbajack.Lib/LoadingLock.cs @@ -0,0 +1,74 @@ +using System; +using System.Reactive.Disposables; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace Wabbajack.Models; + +public class LoadingLock : ReactiveObject, IDisposable +{ + private readonly CompositeDisposable _disposable; + + public ErrorResponse? ErrorState { get; set; } + + public LoadingLock() + { + _disposable = new CompositeDisposable(); + + this.WhenAnyValue(vm => vm.LoadLevel) + .Subscribe(v => IsLoading = v > 0) + .DisposeWith(_disposable); + } + + [Reactive] public int LoadLevel { get; private set; } + + [Reactive] public bool IsLoading { get; private set; } + + public void Dispose() + { + GC.SuppressFinalize(this); + _disposable.Dispose(); + } + + public LockContext WithLoading() + { + RxApp.MainThreadScheduler.Schedule(0, (_, _) => { LoadLevel++; + return Disposable.Empty; + }); + return new LockContext(this); + } + + public class LockContext : IDisposable + { + private readonly LoadingLock _parent; + private bool _disposed; + + public LockContext(LoadingLock parent) + { + _parent = parent; + _disposed = false; + } + + public void Succeed() + { + _parent.ErrorState = ErrorResponse.Success; + Dispose(); + } + + public void Fail() + { + _parent.ErrorState = ErrorResponse.Failure; + Dispose(); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + RxApp.MainThreadScheduler.Schedule(0, (_, _) => { _parent.LoadLevel--; + return Disposable.Empty; + }); + } + } +} \ No newline at end of file diff --git a/Wabbajack.Lib/ViewModel.cs b/Wabbajack.Lib/ViewModel.cs index 719a7ccc..e480a994 100644 --- a/Wabbajack.Lib/ViewModel.cs +++ b/Wabbajack.Lib/ViewModel.cs @@ -4,15 +4,18 @@ using System; using System.Collections.Generic; using System.Reactive.Disposables; using System.Runtime.CompilerServices; +using Wabbajack.Models; namespace Wabbajack.Lib { - public class ViewModel : ReactiveObject, IDisposable + public class ViewModel : ReactiveObject, IDisposable, IActivatableViewModel { - private readonly Lazy _compositeDisposable = new Lazy(); + private readonly Lazy _compositeDisposable = new(); [JsonIgnore] public CompositeDisposable CompositeDisposable => _compositeDisposable.Value; + [JsonIgnore] public LoadingLock LoadingLock { get; } = new(); + public virtual void Dispose() { if (_compositeDisposable.IsValueCreated) @@ -30,5 +33,7 @@ namespace Wabbajack.Lib item = newItem; this.RaisePropertyChanged(propertyName); } + + public ViewModelActivator Activator { get; } = new(); } } diff --git a/Wabbajack.Lib/Wabbajack.Lib.csproj b/Wabbajack.Lib/Wabbajack.Lib.csproj index 10d0e524..ded77bbf 100644 --- a/Wabbajack.Lib/Wabbajack.Lib.csproj +++ b/Wabbajack.Lib/Wabbajack.Lib.csproj @@ -35,10 +35,10 @@ 0.50.0 - 14.1.1 + 16.2.6 - 14.1.1 + 16.2.6 0.28.3