public ModListGalleryVM(MainWindowVM mainWindowVM) : base(mainWindowVM) { MWVM = mainWindowVM; Observable.Return(Unit.Default) .ObserveOn(RxApp.TaskpoolScheduler) .SelectTask(async _ => { try { Error = null; var list = await ModlistMetadata.LoadFromGithub(); return(list.AsObservableChangeSet(x => x.DownloadMetadata?.Hash ?? $"Fallback{missingHashFallbackCounter++}")); } catch (Exception ex) { Utils.Error(ex); Error = ErrorResponse.Fail(ex); return(Observable.Empty <IChangeSet <ModlistMetadata, string> >()); } }) // Unsubscribe and release when not active .FlowSwitch( this.WhenAny(x => x.IsActive), valueWhenOff: Observable.Return(ChangeSet <ModlistMetadata, string> .Empty)) // Convert to VM and bind to resulting list .Switch() .ObserveOnGuiThread() .Transform(m => new ModListMetadataVM(this, m)) .DisposeMany() .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 = Consts.ModListDownloadFolder.Combine(Metadata.Links.MachineURL + (string)Consts.ModListExtension); ModListTagList = new List <ModListTag>(); Metadata.tags.ForEach(tag => { ModListTagList.Add(new ModListTag(tag)); }); DownloadSizeText = "Download size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives); InstallSizeText = "Installation size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles); IsBroken = metadata.ValidationSummary.HasFailures; //https://www.wabbajack.org/#/modlists/info?machineURL=eldersouls OpenWebsiteCommand = ReactiveCommand.Create(() => Utils.OpenWebsite(new Uri($"https://www.wabbajack.org/#/modlists/info?machineURL={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(Location.Exists); } return(exists); }) .Where(exists => exists) // Do any install page swap over on GUI thread .ObserveOnGuiThread() .Select(_ => { _parent.MWVM.OpenInstaller(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.OpenReadme(); } 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)) .SelectAsync(async _ => { try { return(!IsDownloading && !(await 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)); }
public FilePickerVM(object parentVM = null) { Parent = parentVM; SetTargetPathCommand = ConstructTypicalPickerCommand(); var existsCheckTuple = Observable.CombineLatest( this.WhenAny(x => x.ExistCheckOption), this.WhenAny(x => x.PathType), this.WhenAny(x => x.TargetPath) // Dont want to debounce the initial value, because we know it's null .Skip(1) .Debounce(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler) .StartWith(default(string)), resultSelector: (existsOption, type, path) => (ExistsOption: existsOption, Type: type, Path: path)) .StartWith((ExistsOption: ExistCheckOption, Type: PathType, Path: TargetPath)) .Replay(1) .RefCount(); var doExistsCheck = existsCheckTuple .Select(t => { // Don't do exists type if we don't know what path type we're tracking if (t.Type == PathTypeOptions.Off) { return(false); } switch (t.ExistsOption) { case CheckOptions.Off: return(false); case CheckOptions.IfPathNotEmpty: return(!string.IsNullOrWhiteSpace(t.Path)); case CheckOptions.On: return(true); default: throw new NotImplementedException(); } }) .Replay(1) .RefCount(); _exists = Observable.Interval(TimeSpan.FromSeconds(3), RxApp.TaskpoolScheduler) // Only check exists on timer if desired .FlowSwitch(doExistsCheck) .Unit() // Also check though, when fields change .Merge(this.WhenAny(x => x.PathType).Unit()) .Merge(this.WhenAny(x => x.ExistCheckOption).Unit()) .Merge(this.WhenAny(x => x.TargetPath).Unit()) // Signaled to check, get latest params for actual use .CombineLatest(existsCheckTuple, resultSelector: (_, tuple) => tuple) // Refresh exists .ObserveOn(RxApp.TaskpoolScheduler) .Select(t => { switch (t.ExistsOption) { case CheckOptions.IfPathNotEmpty: if (string.IsNullOrWhiteSpace(t.Path)) { return(false); } break; case CheckOptions.On: break; case CheckOptions.Off: default: return(false); } switch (t.Type) { case PathTypeOptions.Either: return(File.Exists(t.Path) || Directory.Exists(t.Path)); case PathTypeOptions.File: return(File.Exists(t.Path)); case PathTypeOptions.Folder: return(Directory.Exists(t.Path)); case PathTypeOptions.Off: default: return(false); } }) .DistinctUntilChanged() .StartWith(false) .ToGuiProperty(this, nameof(Exists)); var passesFilters = Observable.CombineLatest( this.WhenAny(x => x.TargetPath), this.WhenAny(x => x.PathType), this.WhenAny(x => x.FilterCheckOption), Filters.Connect().QueryWhenChanged(), resultSelector: (target, type, checkOption, query) => { switch (type) { case PathTypeOptions.Either: case PathTypeOptions.File: break; default: return(true); } if (query.Count == 0) { return(true); } switch (checkOption) { case CheckOptions.Off: return(true); case CheckOptions.IfPathNotEmpty: if (string.IsNullOrWhiteSpace(target)) { return(true); } break; case CheckOptions.On: break; default: throw new NotImplementedException(); } try { var extension = Path.GetExtension(target); if (extension == null || !extension.StartsWith(".")) { return(false); } extension = extension.Substring(1); if (!query.Any(filter => filter.Extensions.Any(ext => string.Equals(ext, extension)))) { return(false); } } catch (ArgumentException) { return(false); } return(true); }) .StartWith(true) .Select(passed => { if (passed) { return(ErrorResponse.Success); } return(ErrorResponse.Fail(DoesNotPassFiltersText)); }) .Replay(1) .RefCount(); _errorState = Observable.CombineLatest( Observable.CombineLatest( this.WhenAny(x => x.Exists), doExistsCheck, resultSelector: (exists, doExists) => !doExists || exists) .Select(exists => ErrorResponse.Create(successful: exists, exists ? default(string) : PathDoesNotExistText)), passesFilters, this.WhenAny(x => x.AdditionalError) .Select(x => x ?? Observable.Return <IErrorResponse>(ErrorResponse.Success)) .Switch(), resultSelector: (existCheck, filter, err) => { if (existCheck.Failed) { return(existCheck); } if (filter.Failed) { return(filter); } return(ErrorResponse.Convert(err)); }) .ToGuiProperty(this, nameof(ErrorState)); _inError = this.WhenAny(x => x.ErrorState) .Select(x => !x.Succeeded) .ToGuiProperty(this, nameof(InError)); // Doesn't derive from ErrorState, as we want to bubble non-empty tooltips, // which is slightly different logic _errorTooltip = Observable.CombineLatest( Observable.CombineLatest( this.WhenAny(x => x.Exists), doExistsCheck, resultSelector: (exists, doExists) => !doExists || exists) .Select(exists => exists ? default(string) : PathDoesNotExistText), passesFilters .Select(x => x.Reason), this.WhenAny(x => x.AdditionalError) .Select(x => x ?? Observable.Return <IErrorResponse>(ErrorResponse.Success)) .Switch(), resultSelector: (exists, filters, err) => { if (!string.IsNullOrWhiteSpace(exists)) { return(exists); } if (!string.IsNullOrWhiteSpace(filters)) { return(filters); } return(err?.Reason); }) .ToGuiProperty(this, nameof(ErrorTooltip)); }
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)); }
public MO2CompilerVM(CompilerVM parent) { Parent = parent; ModListLocation = new FilePickerVM() { ExistCheckOption = FilePickerVM.CheckOptions.On, PathType = FilePickerVM.PathTypeOptions.File, PromptTitle = "Select a ModList" }; DownloadLocation = new FilePickerVM() { ExistCheckOption = FilePickerVM.CheckOptions.On, PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Select a downloads location", }; _mo2Folder = this.WhenAny(x => x.ModListLocation.TargetPath) .Select(loc => { try { var profileFolder = Path.GetDirectoryName(loc); return(Path.GetDirectoryName(Path.GetDirectoryName(profileFolder))); } catch (Exception) { return(null); } }) .ToGuiProperty(this, nameof(Mo2Folder)); _moProfile = this.WhenAny(x => x.ModListLocation.TargetPath) .Select(loc => { try { var profileFolder = Path.GetDirectoryName(loc); return(Path.GetFileName(profileFolder)); } catch (Exception) { return(null); } }) .ToGuiProperty(this, nameof(MOProfile)); // Wire missing Mo2Folder to signal error state for ModList Location ModListLocation.AdditionalError = this.WhenAny(x => x.Mo2Folder) .Select <string, IErrorResponse>(moFolder => { if (Directory.Exists(moFolder)) { return(ErrorResponse.Success); } return(ErrorResponse.Fail($"MO2 folder could not be located from the given ModList location.{Environment.NewLine}Make sure your ModList is inside a valid MO2 distribution.")); }); // Load custom ModList settings per MO2 profile _modlistSettings = Observable.CombineLatest( (this).WhenAny(x => x.ModListLocation.ErrorState), (this).WhenAny(x => x.ModListLocation.TargetPath), resultSelector: (state, path) => (State: state, Path: path)) // A short throttle is a quick hack to make the above changes "atomic" .Throttle(TimeSpan.FromMilliseconds(25), RxApp.MainThreadScheduler) .Select(u => { if (u.State.Failed) { return(null); } var modlistSettings = _settings.ModlistSettings.TryCreate(u.Path); return(new ModlistSettingsEditorVM(modlistSettings) { ModListName = MOProfile }); }) // Interject and save old while loading new .Pairwise() .Do(pair => { pair.Previous?.Save(); pair.Current?.Init(); }) .Select(x => x.Current) .ToGuiProperty(this, nameof(ModlistSettings)); CanCompile = Observable.CombineLatest( this.WhenAny(x => x.ModListLocation.InError), this.WhenAny(x => x.DownloadLocation.InError), parent.WhenAny(x => x.OutputLocation.InError), this.WhenAny(x => x.ModlistSettings) .Select(x => x?.InError ?? Observable.Return(false)) .Switch(), resultSelector: (ml, down, output, modlistSettings) => !ml && !down && !output && !modlistSettings) .Publish() .RefCount(); // Load settings _settings = parent.MWVM.Settings.Compiler.MO2Compilation; ModListLocation.TargetPath = _settings.LastCompiledProfileLocation; if (!string.IsNullOrWhiteSpace(_settings.DownloadLocation)) { DownloadLocation.TargetPath = _settings.DownloadLocation; } parent.MWVM.Settings.SaveSignal .Subscribe(_ => Unload()) .DisposeWith(CompositeDisposable); // If Mo2 folder changes and download location is empty, set it for convenience this.WhenAny(x => x.Mo2Folder) .DelayInitial(TimeSpan.FromMilliseconds(100), RxApp.MainThreadScheduler) .Where(x => Directory.Exists(x)) .FlowSwitch( (this).WhenAny(x => x.DownloadLocation.Exists) .Invert()) // A skip is needed to ignore the initial signal when the FilterSwitch turns on .Skip(1) .Subscribe(_ => { DownloadLocation.TargetPath = MO2Compiler.GetTypicalDownloadsFolder(Mo2Folder); }) .DisposeWith(CompositeDisposable); }
public ModListGalleryVM(MainWindowVM mainWindowVM) : base(mainWindowVM) { MWVM = mainWindowVM; ClearFiltersCommand = ReactiveCommand.Create( () => { OnlyInstalled = false; Search = string.Empty; }); 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 ?? $"Fallback{missingHashFallbackCounter++}")); } catch (Exception ex) { Utils.Error(ex); Error = ErrorResponse.Fail(ex); return(Observable.Empty <IChangeSet <ModlistMetadata, string> >()); } }) // Unsubscribe and release when not active .FlowSwitch( this.WhenAny(x => x.IsActive), valueWhenOff: Observable.Return(ChangeSet <ModlistMetadata, string> .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(predicateChanged: 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(predicateChanged: 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)); })) // 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 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; 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); ClearFiltersCommand = ReactiveCommand.Create( () => { OnlyInstalled = false; ShowNSFW = false; ShowUtilityLists = false; Search = string.Empty; GameType = ALL_GAME_TYPE; }); this.WhenAny(x => x.OnlyInstalled) .Subscribe(val => { if (val) { GameType = ALL_GAME_TYPE; } }) .DisposeWith(CompositeDisposable); var sourceList = Observable.Return(Unit.Default) .ObserveOn(RxApp.TaskpoolScheduler) .SelectTask(async _ => { try { Error = null; var list = await ModlistMetadata.LoadFromGithub(); Error = ErrorResponse.Success; return(list .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) || vm.Metadata.tags.Any(t => t.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(this.WhenAny(x => x.ShowUtilityLists) .Select <bool, Func <ModListMetadataVM, bool> >(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 <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()); })) .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 InstallerVM(MainWindowVM mainWindowVM) { if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower()) { MessageBox.Show( "Wabbajack is running inside your Downloads folder. This folder is often highly monitored by antivirus software and these can often " + "conflict with the operations Wabbajack needs to perform. Please move this executable outside of your Downloads folder and then restart the app.", "Cannot run inside Downloads", MessageBoxButton.OK, MessageBoxImage.Error); Environment.Exit(1); } MWVM = mainWindowVM; ModListLocation = new FilePickerVM() { ExistCheckOption = FilePickerVM.CheckOptions.On, PathType = FilePickerVM.PathTypeOptions.File, PromptTitle = "Select a modlist to install" }; // Swap to proper sub VM based on selected type _installer = this.WhenAny(x => x.TargetManager) // Delay so the initial VM swap comes in immediately, subVM comes right after .DelayInitial(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler) .Select <ModManager?, ISubInstallerVM>(type => { switch (type) { case ModManager.MO2: return(new MO2InstallerVM(this)); case ModManager.Vortex: return(new VortexInstallerVM(this)); default: return(null); } }) // Unload old VM .Pairwise() .Do(pair => { pair.Previous?.Unload(); }) .Select(p => p.Current) .ToProperty(this, nameof(Installer)); // Load settings MWVM.Settings.SaveSignal .Subscribe(_ => { MWVM.Settings.Installer.LastInstalledListLocation = ModListLocation.TargetPath; }) .DisposeWith(CompositeDisposable); _modList = this.WhenAny(x => x.ModListLocation.TargetPath) .ObserveOn(RxApp.TaskpoolScheduler) .Select(modListPath => { if (modListPath == null) { return(default(ModListVM)); } if (!File.Exists(modListPath)) { return(default(ModListVM)); } return(new ModListVM(modListPath)); }) .ObserveOnGuiThread() .StartWith(default(ModListVM)) .ToProperty(this, nameof(ModList)); _htmlReport = this.WhenAny(x => x.ModList) .Select(modList => modList?.ReportHTML) .ToProperty(this, nameof(HTMLReport)); _installing = this.WhenAny(x => x.Installer.ActiveInstallation) .Select(i => i != null) .ObserveOnGuiThread() .ToProperty(this, nameof(Installing)); _TargetManager = this.WhenAny(x => x.ModList) .Select(modList => modList?.ModManager) .ToProperty(this, nameof(TargetManager)); // Add additional error check on modlist ModListLocation.AdditionalError = this.WhenAny(x => x.ModList) .Select <ModListVM, IErrorResponse>(modList => { if (modList == null) { return(ErrorResponse.Fail("Modlist path resulted in a null object.")); } if (modList.Error != null) { return(ErrorResponse.Fail("Modlist is corrupt", modList.Error)); } return(ErrorResponse.Success); }); BackCommand = ReactiveCommand.Create( execute: () => { StartedInstallation = false; Completed = null; mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM; }, canExecute: this.WhenAny(x => x.Installing) .Select(x => !x)); _percentCompleted = this.WhenAny(x => x.Installer.ActiveInstallation) .StartWith(default(AInstaller)) .CombineLatest( this.WhenAny(x => x.Completed), (installer, completed) => { if (installer == null) { return(Observable.Return <float>(completed != null ? 1f : 0f)); } return(installer.PercentCompleted.StartWith(0f)); }) .Switch() .Debounce(TimeSpan.FromMilliseconds(25)) .ToProperty(this, nameof(PercentCompleted)); Slideshow = new SlideShow(this); // Set display items to modlist if configuring or complete, // or to the current slideshow data if installing _image = Observable.CombineLatest( this.WhenAny(x => x.ModList.Error), this.WhenAny(x => x.ModList) .Select(x => x?.ImageObservable ?? Observable.Empty <BitmapImage>()) .Switch() .StartWith(WabbajackLogo), this.WhenAny(x => x.Slideshow.Image) .StartWith(default(BitmapImage)), this.WhenAny(x => x.Installing), resultSelector: (err, modList, slideshow, installing) => { if (err != null) { return(WabbajackErrLogo); } var ret = installing ? slideshow : modList; return(ret ?? WabbajackLogo); }) .Select <BitmapImage, ImageSource>(x => x) .ToProperty(this, nameof(Image)); _titleText = Observable.CombineLatest( this.WhenAny(x => x.ModList.Name), this.WhenAny(x => x.Slideshow.TargetMod.ModName) .StartWith(default(string)), this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) .ToProperty(this, nameof(TitleText)); _authorText = Observable.CombineLatest( this.WhenAny(x => x.ModList.Author), this.WhenAny(x => x.Slideshow.TargetMod.ModAuthor) .StartWith(default(string)), this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) .ToProperty(this, nameof(AuthorText)); _description = Observable.CombineLatest( this.WhenAny(x => x.ModList.Description), this.WhenAny(x => x.Slideshow.TargetMod.ModDescription) .StartWith(default(string)), this.WhenAny(x => x.Installing), resultSelector: (modList, mod, installing) => installing ? mod : modList) .ToProperty(this, nameof(Description)); _modListName = Observable.CombineLatest( this.WhenAny(x => x.ModList.Error) .Select(x => x != null), this.WhenAny(x => x.ModList) .Select(x => x?.Name), resultSelector: (err, name) => { if (err) { return("Corrupted Modlist"); } return(name); }) .ToProperty(this, nameof(ModListName)); // Define commands ShowReportCommand = ReactiveCommand.Create(ShowReport); OpenReadmeCommand = ReactiveCommand.Create( execute: () => this.ModList?.OpenReadmeWindow(), canExecute: this.WhenAny(x => x.ModList) .Select(modList => !string.IsNullOrEmpty(modList?.Readme)) .ObserveOnGuiThread()); VisitWebsiteCommand = ReactiveCommand.Create( execute: () => Process.Start(ModList.Website), canExecute: this.WhenAny(x => x.ModList.Website) .Select(x => x?.StartsWith("https://") ?? false) .ObserveOnGuiThread()); _progressTitle = Observable.CombineLatest( this.WhenAny(x => x.Installing), this.WhenAny(x => x.StartedInstallation), resultSelector: (installing, started) => { if (!installing) { return("Configuring"); } return(started ? "Installing" : "Installed"); }) .ToProperty(this, nameof(ProgressTitle)); Dictionary <int, CPUDisplayVM> cpuDisplays = new Dictionary <int, CPUDisplayVM>(); // Compile progress updates and populate ObservableCollection this.WhenAny(x => x.Installer.ActiveInstallation) .SelectMany(c => c?.QueueStatus ?? Observable.Empty <CPUStatus>()) .ObserveOn(RxApp.TaskpoolScheduler) // Attach start times to incoming CPU items .Scan( new CPUDisplayVM(), (_, cpu) => { var ret = cpuDisplays.TryCreate(cpu.ID); ret.AbsorbStatus(cpu); return(ret); }) .ToObservableChangeSet(x => x.Status.ID) .Batch(TimeSpan.FromMilliseconds(250), RxApp.TaskpoolScheduler) .EnsureUniqueChanges() .Filter(i => i.Status.IsWorking && i.Status.ID != WorkQueue.UnassignedCpuId) .ObserveOn(RxApp.MainThreadScheduler) .Sort(SortExpressionComparer <CPUDisplayVM> .Ascending(s => s.StartTime)) .Bind(StatusList) .Subscribe() .DisposeWith(CompositeDisposable); BeginCommand = ReactiveCommand.CreateFromTask( canExecute: this.WhenAny(x => x.Installer.CanInstall) .Switch(), execute: async() => { try { await this.Installer.Install(); Completed = ErrorResponse.Success; try { this.ModList?.OpenReadmeWindow(); } catch (Exception ex) { Utils.Error(ex); } } catch (Exception ex) { while (ex.InnerException != null) { ex = ex.InnerException; } Utils.Log(ex.StackTrace); Utils.Log(ex.ToString()); Utils.Log($"{ex.Message} - Can't continue"); Completed = ErrorResponse.Fail(ex); } }); // When sub installer begins an install, mark state variable BeginCommand.StartingExecution() .Subscribe(_ => { StartedInstallation = true; }) .DisposeWith(CompositeDisposable); // Listen for user interventions, and compile a dynamic list of all unhandled ones var activeInterventions = this.WhenAny(x => x.Installer.ActiveInstallation) .SelectMany(c => c?.LogMessages ?? Observable.Empty <IStatusMessage>()) .WhereCastable <IStatusMessage, IUserIntervention>() .ToObservableChangeSet() .AutoRefresh(i => i.Handled) .Filter(i => !i.Handled) .AsObservableList(); // Find the top intervention /w no CPU ID to be marked as "global" _ActiveGlobalUserIntervention = activeInterventions.Connect() .Filter(x => x.CpuID == WorkQueue.UnassignedCpuId) .QueryWhenChanged(query => query.FirstOrDefault()) .ObserveOnGuiThread() .ToProperty(this, nameof(ActiveGlobalUserIntervention)); CloseWhenCompleteCommand = ReactiveCommand.Create( canExecute: this.WhenAny(x => x.Completed) .Select(x => x != null), execute: () => { MWVM.ShutdownApplication(); }); GoToInstallCommand = ReactiveCommand.Create( canExecute: Observable.CombineLatest( this.WhenAny(x => x.Completed) .Select(x => x != null), this.WhenAny(x => x.Installer.SupportsAfterInstallNavigation), resultSelector: (complete, supports) => complete && supports), execute: () => { Installer.AfterInstallNavigation(); }); }
public CompilerVM(MainWindowVM mainWindowVM) { MWVM = mainWindowVM; OutputLocation = new FilePickerVM() { ExistCheckOption = FilePickerVM.CheckOptions.IfPathNotEmpty, PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Select the folder to place the resulting modlist.wabbajack file", }; // Load settings CompilerSettings settings = MWVM.Settings.Compiler; SelectedCompilerType = settings.LastCompiledModManager; OutputLocation.TargetPath = settings.OutputLocation; MWVM.Settings.SaveSignal .Subscribe(_ => { settings.LastCompiledModManager = SelectedCompilerType; settings.OutputLocation = OutputLocation.TargetPath; }) .DisposeWith(CompositeDisposable); // Swap to proper sub VM based on selected type _compiler = this.WhenAny(x => x.SelectedCompilerType) // Delay so the initial VM swap comes in immediately, subVM comes right after .DelayInitial(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler) .Select <ModManager, ISubCompilerVM>(type => { switch (type) { case ModManager.MO2: return(new MO2CompilerVM(this)); case ModManager.Vortex: return(new VortexCompilerVM(this)); default: return(null); } }) // Unload old VM .Pairwise() .Do(pair => { pair.Previous?.Unload(); }) .Select(p => p.Current) .ToProperty(this, nameof(Compiler)); // Let sub VM determine what settings we're displaying and when _currentModlistSettings = this.WhenAny(x => x.Compiler.ModlistSettings) .ToProperty(this, nameof(CurrentModlistSettings)); _image = this.WhenAny(x => x.CurrentModlistSettings.ImagePath.TargetPath) // Throttle so that it only loads image after any sets of swaps have completed .Throttle(TimeSpan.FromMilliseconds(50), RxApp.MainThreadScheduler) .DistinctUntilChanged() .Select(path => { if (string.IsNullOrWhiteSpace(path)) { return(UIUtils.BitmapImageFromResource("Resources/Wabba_Mouth_No_Text.png")); } if (UIUtils.TryGetBitmapImageFromFile(path, out var image)) { return(image); } return(null); }) .ToProperty(this, nameof(Image)); _compiling = this.WhenAny(x => x.Compiler.ActiveCompilation) .Select(compilation => compilation != null) .ObserveOnGuiThread() .ToProperty(this, nameof(Compiling)); BackCommand = ReactiveCommand.Create( execute: () => { mainWindowVM.ActivePane = mainWindowVM.ModeSelectionVM; StartedCompilation = false; Completed = null; }, canExecute: this.WhenAny(x => x.Compiling) .Select(x => !x)); // Compile progress updates and populate ObservableCollection Dictionary <int, CPUDisplayVM> cpuDisplays = new Dictionary <int, CPUDisplayVM>(); this.WhenAny(x => x.Compiler.ActiveCompilation) .SelectMany(c => c?.QueueStatus ?? Observable.Empty <CPUStatus>()) .ObserveOn(RxApp.TaskpoolScheduler) // Attach start times to incoming CPU items .Scan( new CPUDisplayVM(), (_, cpu) => { var ret = cpuDisplays.TryCreate(cpu.ID); ret.AbsorbStatus(cpu); return(ret); }) .ToObservableChangeSet(x => x.Status.ID) .Batch(TimeSpan.FromMilliseconds(250), RxApp.TaskpoolScheduler) .EnsureUniqueChanges() .Filter(i => i.Status.IsWorking && i.Status.ID != WorkQueue.UnassignedCpuId) .ObserveOn(RxApp.MainThreadScheduler) .Sort(SortExpressionComparer <CPUDisplayVM> .Ascending(s => s.StartTime)) .Bind(StatusList) .Subscribe() .DisposeWith(CompositeDisposable); _percentCompleted = this.WhenAny(x => x.Compiler.ActiveCompilation) .StartWith(default(ACompiler)) .CombineLatest( this.WhenAny(x => x.Completed), (compiler, completed) => { if (compiler == null) { return(Observable.Return <float>(completed != null ? 1f : 0f)); } return(compiler.PercentCompleted.StartWith(0)); }) .Switch() .Debounce(TimeSpan.FromMilliseconds(25)) .ToProperty(this, nameof(PercentCompleted)); BeginCommand = ReactiveCommand.CreateFromTask( canExecute: this.WhenAny(x => x.Compiler.CanCompile) .Switch(), execute: async() => { try { await this.Compiler.Compile(); Completed = ErrorResponse.Success; } catch (Exception ex) { Completed = ErrorResponse.Fail(ex); while (ex.InnerException != null) { ex = ex.InnerException; } Utils.Error(ex, $"Compiler error"); } }); // When sub compiler begins a compile, mark state variable BeginCommand.StartingExecution() .Subscribe(_ => { StartedCompilation = true; }) .DisposeWith(CompositeDisposable); // Listen for user interventions, and compile a dynamic list of all unhandled ones var activeInterventions = this.WhenAny(x => x.Compiler.ActiveCompilation) .SelectMany(c => c?.LogMessages ?? Observable.Empty <IStatusMessage>()) .WhereCastable <IStatusMessage, IUserIntervention>() .ToObservableChangeSet() .AutoRefresh(i => i.Handled) .Filter(i => !i.Handled) .AsObservableList(); // Find the top intervention /w no CPU ID to be marked as "global" _ActiveGlobalUserIntervention = activeInterventions.Connect() .Filter(x => x.CpuID == WorkQueue.UnassignedCpuId) .QueryWhenChanged(query => query.FirstOrDefault()) .ObserveOnGuiThread() .ToProperty(this, nameof(ActiveGlobalUserIntervention)); CloseWhenCompleteCommand = ReactiveCommand.Create( canExecute: this.WhenAny(x => x.Completed) .Select(x => x != null), execute: () => { MWVM.ShutdownApplication(); }); GoToModlistCommand = ReactiveCommand.Create( canExecute: this.WhenAny(x => x.Completed) .Select(x => x != null), execute: () => { if (string.IsNullOrWhiteSpace(OutputLocation.TargetPath)) { Process.Start("explorer.exe", Path.GetDirectoryName(System.Reflection.Assembly.GetEntryAssembly().Location)); } else { Process.Start("explorer.exe", OutputLocation.TargetPath); } }); }