public VortexInstallerVM(InstallerVM installerVM) { Parent = installerVM; _TargetGame = installerVM.WhenAny(x => x.ModList.SourceModList.GameType) .ToProperty(this, nameof(TargetGame)); CanInstall = Observable.CombineLatest( this.WhenAny(x => x.TargetGame) .Select(game => VortexCompiler.IsActiveVortexGame(game)), installerVM.WhenAny(x => x.ModListLocation.InError), resultSelector: (isVortexGame, modListErr) => isVortexGame && !modListErr); }
public MO2InstallerVM(InstallerVM installerVM) { Parent = installerVM; Location = new FilePickerVM() { ExistCheckOption = FilePickerVM.CheckOptions.Off, PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Select Installation Directory", }; DownloadLocation = new FilePickerVM() { ExistCheckOption = FilePickerVM.CheckOptions.Off, PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Select a location for MO2 downloads", }; DownloadLocation.AdditionalError = this.WhenAny(x => x.DownloadLocation.TargetPath) .Select(x => Utils.IsDirectoryPathValid(x)); Location.AdditionalError = Observable.CombineLatest( this.WhenAny(x => x.Location.TargetPath), this.WhenAny(x => x.DownloadLocation.TargetPath), resultSelector: (target, download) => (target, download)) .ObserveOn(RxApp.TaskpoolScheduler) .Select(i => MO2Installer.CheckValidInstallPath(i.target, i.download)) .ObserveOnGuiThread(); CanInstall = Observable.CombineLatest( this.WhenAny(x => x.Location.InError), this.WhenAny(x => x.DownloadLocation.InError), installerVM.WhenAny(x => x.ModListLocation.InError), resultSelector: (loc, modlist, download) => { return(!loc && !download && !modlist); }); // Have Installation location updates modify the downloads location if empty this.WhenAny(x => x.Location.TargetPath) .Skip(1) // Don't do it initially .Subscribe(installPath => { if (string.IsNullOrWhiteSpace(DownloadLocation.TargetPath)) { DownloadLocation.TargetPath = Path.Combine(installPath, "downloads"); } }) .DisposeWith(CompositeDisposable); // Load settings _CurrentSettings = installerVM.WhenAny(x => x.ModListLocation.TargetPath) .Select(path => path == null ? null : installerVM.MWVM.Settings.Installer.Mo2ModlistSettings.TryCreate(path)) .ToGuiProperty(this, nameof(CurrentSettings)); this.WhenAny(x => x.CurrentSettings) .Pairwise() .Subscribe(settingsPair => { SaveSettings(settingsPair.Previous); if (settingsPair.Current == null) { return; } Location.TargetPath = settingsPair.Current.InstallationLocation; DownloadLocation.TargetPath = settingsPair.Current.DownloadLocation; AutomaticallyOverwrite = settingsPair.Current.AutomaticallyOverrideExistingInstall; }) .DisposeWith(CompositeDisposable); installerVM.MWVM.Settings.SaveSignal .Subscribe(_ => SaveSettings(CurrentSettings)) .DisposeWith(CompositeDisposable); // Hook onto user interventions, and intercept MO2 specific ones for customization this.WhenAny(x => x.ActiveInstallation) .Select(x => x?.LogMessages ?? Observable.Empty <IStatusMessage>()) .Switch() .Subscribe(x => { switch (x) { case ConfirmUpdateOfExistingInstall c: if (AutomaticallyOverwrite) { c.Confirm(); } break; default: break; } }) .DisposeWith(CompositeDisposable); }
public SlideShow(InstallerVM appState) { Installer = appState; // Wire target slideshow index var intervalSeconds = 10; // Compile all the sources that trigger a slideshow update, any of which trigger a counter update var selectedIndex = Observable.Merge( // If user requests one manually SlideShowNextItemCommand.StartingExecution(), // If the natural timer fires Observable.Merge( // Start with an initial timer Observable.Return(Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))), // but reset timer if user requests one SlideShowNextItemCommand.StartingExecution() .Select(_ => Observable.Interval(TimeSpan.FromSeconds(intervalSeconds)))) // When a new timer comes in, swap to it .Switch() .Unit()) // When filter switch enabled, fire an initial signal .StartWith(Unit.Default) // Only subscribe to slideshow triggers if enabled and installing .FlowSwitch( Observable.CombineLatest( this.WhenAny(x => x.Enable), this.WhenAny(x => x.Installer.Installing), resultSelector: (enabled, installing) => enabled && installing)) // Block spam .Debounce(TimeSpan.FromMilliseconds(250), RxApp.MainThreadScheduler) .Scan( seed: 0, accumulator: (i, _) => i + 1) .Publish() .RefCount(); // Dynamic list changeset of mod VMs to display var modVMs = this.WhenAny(x => x.Installer.ModList) // Whenever modlist changes, grab the list of its slides .Select(modList => { if (modList?.SourceModList?.Archives == null) { return(Observable.Empty <NexusDownloader.State>() .ToObservableChangeSet(x => x.ModID)); } return(modList.SourceModList.Archives .Select(m => m.State) .OfType <NexusDownloader.State>() // Shuffle it .Shuffle(_random) .AsObservableChangeSet(x => x.ModID)); }) // Switch to the new list after every ModList change .Switch() .Transform(nexus => new ModVM(nexus)) .DisposeMany() // Filter out any NSFW slides if we don't want them .AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW)) .Filter(slide => !slide.IsNSFW || ShowNSFW) .RefCount(); // Find target mod to display by combining dynamic list with currently desired index _targetMod = Observable.CombineLatest( modVMs.QueryWhenChanged(), selectedIndex, resultSelector: (query, selected) => { var index = selected % (query.Count == 0 ? 1 : query.Count); return(query.Items.ElementAtOrDefault(index)); }) .StartWith(default(ModVM)) .ToGuiProperty(this, nameof(TargetMod)); // Mark interest and materialize image of target mod _image = this.WhenAny(x => x.TargetMod) // We want to Switch here, not SelectMany, as we want to hotswap to newest target without waiting on old ones .Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage))) .Switch() .ToGuiProperty(this, nameof(Image)); VisitNexusSiteCommand = ReactiveCommand.Create( execute: () => { Process.Start(TargetMod.ModURL); return(Unit.Default); }, canExecute: this.WhenAny(x => x.TargetMod.ModURL) .Select(x => x?.StartsWith("https://") ?? false) .ObserveOnGuiThread()); // Preload upcoming images var list = Observable.CombineLatest( modVMs.QueryWhenChanged(), selectedIndex, resultSelector: (query, selected) => { // Retrieve the mods that should be preloaded var index = selected % (query.Count == 0 ? 1 : query.Count); var amountToTake = Math.Min(query.Count - index, PreloadAmount); return(query.Items.Skip(index).Take(amountToTake).ToObservable()); }) .Select(i => i.ToObservableChangeSet()) .Switch() .Transform(mod => mod.ImageObservable.Subscribe()) .DisposeMany() .AsObservableList(); }
public SlideShow(InstallerVM appState) { this.Installer = appState; // Wire target slideshow index var intervalSeconds = 10; // Compile all the sources that trigger a slideshow update, any of which trigger a counter update var selectedIndex = Observable.Merge( // If user requests one manually this.SlideShowNextItemCommand.StartingExecution(), // If the natural timer fires Observable.Merge( // Start with an initial timer Observable.Return(Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))), // but reset timer if user requests one this.SlideShowNextItemCommand.StartingExecution() .Select(_ => Observable.Interval(TimeSpan.FromSeconds(intervalSeconds)))) // When a new timer comes in, swap to it .Switch() .Unit()) // When filter switch enabled, fire an initial signal .StartWith(Unit.Default) // Only subscribe to slideshow triggers if enabled and installing .FilterSwitch( Observable.CombineLatest( this.WhenAny(x => x.Enable), this.WhenAny(x => x.Installer.Installing), resultSelector: (enabled, installing) => enabled && installing)) // Block spam .Debounce(TimeSpan.FromMilliseconds(250)) .Scan( seed: 0, accumulator: (i, _) => i + 1) .Publish() .RefCount(); // Dynamic list changeset of mod VMs to display var modVMs = this.WhenAny(x => x.Installer.ModList) // Whenever modlist changes, grab the list of its slides .Select(modList => { if (modList == null) { return(Observable.Empty <ModVM>() .ToObservableChangeSet(x => x.ModID)); } return(modList.SourceModList.Archives .Select(m => m.State) .OfType <NexusDownloader.State>() .Select(nexus => new ModVM(nexus)) // Shuffle it .Shuffle(this._random) .AsObservableChangeSet(x => x.ModID)); }) // Switch to the new list after every modlist change .Switch() // Filter out any NSFW slides if we don't want them .AutoRefreshOnObservable(slide => this.WhenAny(x => x.ShowNSFW)) .Filter(slide => !slide.IsNSFW || this.ShowNSFW) .RefCount(); // Find target mod to display by combining dynamic list with currently desired index this._TargetMod = Observable.CombineLatest( modVMs.QueryWhenChanged(), selectedIndex, resultSelector: (query, selected) => query.Items.ElementAtOrDefault(selected % query.Count)) .StartWith(default(ModVM)) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, nameof(this.TargetMod)); // Mark interest and materialize image of target mod this._Image = this.WhenAny(x => x.TargetMod) // We want to Switch here, not SelectMany, as we want to hotswap to newest target without waiting on old ones .Select(x => x?.ImageObservable ?? Observable.Return(default(BitmapImage))) .Switch() .ToProperty(this, nameof(this.Image)); this.VisitNexusSiteCommand = ReactiveCommand.Create( execute: () => Process.Start(this.TargetMod.ModURL), canExecute: this.WhenAny(x => x.TargetMod.ModURL) .Select(x => x?.StartsWith("https://") ?? false) .ObserveOnGuiThread()); // ToDo // Can maybe add "preload" systems to prep upcoming images // This would entail subscribing to modVMs, narrowing it down to Top(X) or Page() somehow. // The result would not be used anywhere, just simply expressing interest in those mods' // images will implicitly cache them // // Page would be really clever to use, but it's not exactly right as its "window" won't follow the current index, // so at the boundary of a page, the next image won't be cached. Need like a Page() /w an offset parameter, or something. }