Beispiel #1
0
        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);
        }
Beispiel #2
0
        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));
        }
Beispiel #3
0
        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));
        }
Beispiel #4
0
        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));
        }
Beispiel #5
0
        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);
        }
Beispiel #6
0
        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);
        }
Beispiel #8
0
        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();
            });
        }
Beispiel #9
0
        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);
                }
            });
        }