public ModListGalleryVM(MainWindowVM mainWindowVM) : base(mainWindowVM) { MWVM = mainWindowVM; // load persistent filter settings if (settings.IsPersistent) { GameType = !string.IsNullOrEmpty(settings.Game) ? settings.Game : ALL_GAME_TYPE; ShowNSFW = settings.ShowNSFW; OnlyInstalled = settings.OnlyInstalled; Search = settings.Search; } else { GameType = ALL_GAME_TYPE; } // subscribe to save signal MWVM.Settings.SaveSignal .Subscribe(_ => UpdateFiltersSettings()) .DisposeWith(this.CompositeDisposable); ClearFiltersCommand = ReactiveCommand.Create( () => { OnlyInstalled = false; ShowNSFW = false; Search = string.Empty; GameType = ALL_GAME_TYPE; }); this.WhenAny(x => x.OnlyInstalled) .Subscribe(val => { if (val) { GameType = ALL_GAME_TYPE; } }) .DisposeWith(CompositeDisposable); var random = new Random(); var sourceList = Observable.Return(Unit.Default) .ObserveOn(RxApp.TaskpoolScheduler) .SelectTask(async _ => { try { Error = null; var list = await ModlistMetadata.LoadFromGithub(); Error = ErrorResponse.Success; return(list // Sort randomly initially, just to give each list a fair shake .Shuffle(random) .AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? Hash.Empty)); } catch (Exception ex) { Utils.Error(ex); Error = ErrorResponse.Fail(ex); return(Observable.Empty <IChangeSet <ModlistMetadata, Hash> >()); } }) // Unsubscribe and release when not active .FlowSwitch( this.WhenAny(x => x.IsActive), valueWhenOff: Observable.Return(ChangeSet <ModlistMetadata, Hash> .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(this, m)) .DisposeMany() // Filter only installed .Filter(this.WhenAny(x => x.OnlyInstalled) .Select <bool, Func <ModListMetadataVM, bool> >(onlyInstalled => (vm) => { if (!onlyInstalled) { return(true); } if (!GameRegistry.Games.TryGetValue(vm.Metadata.Game, out var gameMeta)) { return(false); } return(gameMeta.IsInstalled); })) // Filter on search box .Filter(this.WhenAny(x => x.Search) .Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) .Select <string, Func <ModListMetadataVM, bool> >(search => (vm) => { if (string.IsNullOrWhiteSpace(search)) { return(true); } return(vm.Metadata.Title.ContainsCaseInsensitive(search)); })) .Filter(this.WhenAny(x => x.ShowNSFW) .Select <bool, Func <ModListMetadataVM, bool> >(showNSFW => vm => { if (!vm.Metadata.NSFW) { return(true); } return(vm.Metadata.NSFW && showNSFW); })) // Filter by Game .Filter(this.WhenAny(x => x.GameType) .Debounce(TimeSpan.FromMilliseconds(150), RxApp.MainThreadScheduler) .Select <string, Func <ModListMetadataVM, bool> >(GameType => (vm) => { if (GameType == ALL_GAME_TYPE) { return(true); } if (string.IsNullOrEmpty(GameType)) { return(false); } return(GameType == vm.Metadata.Game.GetDescription <Game>().ToString()); })) .Filter(this.WhenAny(x => x.ShowNSFW) .Select <bool, Func <ModListMetadataVM, bool> >(showNSFW => vm => { if (!vm.Metadata.NSFW) { return(true); } return(vm.Metadata.NSFW && showNSFW); })) // Put broken lists at bottom .Sort(Comparer <ModListMetadataVM> .Create((a, b) => a.IsBroken.CompareTo(b.IsBroken))) .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); }
public ModListMetadataVM(ModListGalleryVM parent, ModlistMetadata metadata) { _parent = parent; Metadata = metadata; Location = Path.Combine(Consts.ModListDownloadFolder, Metadata.Links.MachineURL + ExtensionManager.Extension); IsBroken = metadata.ValidationSummary.HasFailures; OpenWebsiteCommand = ReactiveCommand.Create(() => Process.Start($"https://www.wabbajack.org/modlist/{Metadata.Links.MachineURL}")); ExecuteCommand = ReactiveCommand.CreateFromObservable <Unit, Unit>( canExecute: this.WhenAny(x => x.IsBroken).Select(x => !x), execute: (unit) => Observable.Return(unit) .WithLatestFrom( this.WhenAny(x => x.Exists), (_, e) => e) // Do any download work on background thread .ObserveOn(RxApp.TaskpoolScheduler) .SelectTask(async(exists) => { if (!exists) { try { var success = await Download(); if (!success) { Error = ErrorResponse.Fail("Download was marked unsuccessful"); return(false); } } catch (Exception ex) { Error = ErrorResponse.Fail(ex); return(false); } // Return an updated check on exists return(File.Exists(Location)); } return(exists); }) .Where(exists => exists) // Do any install page swap over on GUI thread .ObserveOnGuiThread() .Select(_ => { _parent.MWVM.OpenInstaller(Path.GetFullPath(Location)); // Wait for modlist member to be filled, then open its readme return(_parent.MWVM.Installer.Value.WhenAny(x => x.ModList) .NotNull() .Take(1) .Do(modList => { try { modList.OpenReadmeWindow(); } catch (Exception ex) { Utils.Error(ex); } })); }) .Switch() .Unit()); _Exists = Observable.Interval(TimeSpan.FromSeconds(0.5)) .Unit() .StartWith(Unit.Default) .FlowSwitch(_parent.WhenAny(x => x.IsActive)) .Select(_ => { try { return(!metadata.NeedsDownload(Location)); } catch (Exception) { return(true); } }) .ToGuiProperty(this, nameof(Exists)); var imageObs = Observable.Return(Metadata.Links.ImageUri) .DownloadBitmapImage((ex) => Utils.Log($"Error downloading modlist image {Metadata.Title}")); _Image = imageObs .ToGuiProperty(this, nameof(Image)); _LoadingImage = imageObs .Select(x => false) .StartWith(true) .ToGuiProperty(this, nameof(LoadingImage)); }