diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f2dafc..8d4ec68b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,20 @@ ### Changelog +#### Version - TBD +* Fixed Readme opening twice +* Updated Text in the UI to better describe current app behavior +* Added support for resumable downloads after closing the app during downloads (not available for MEGA downloads) +* More and improved existing tooltips +* Fixed being able to click the readme button when there is no readme +* Game Selector Improvements: + * Only games that have modlists are shown now + * Amount of lists for a game is shown + * Now able to filter for game in combination with filtering on only installed modlists +* Game support: + * Added Enderal GOG support (compatibility with existing lists unclear) + #### Version - 3.0.6.2 - 1/28/2023 * Add fallback for DDS compression when installing older lists. This should keep older DDS files from not being compressed without any mipmaps at all. -* #### Version - 3.0.6.1 - 1/28/2023 * Game support: diff --git a/Wabbajack.App.Wpf/App.xaml b/Wabbajack.App.Wpf/App.xaml index fb010729..2d1108c4 100644 --- a/Wabbajack.App.Wpf/App.xaml +++ b/Wabbajack.App.Wpf/App.xaml @@ -5,8 +5,7 @@ xmlns:local="clr-namespace:Wabbajack" xmlns:controls="clr-namespace:Wabbajack.Extensions" ShutdownMode="OnExplicitShutdown" - Startup="OnStartup" - Exit="OnExit"> + Startup="OnStartup"> diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs index e855d5af..7cb399d9 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -194,14 +194,5 @@ namespace Wabbajack return services; } - - private void OnExit(object sender, ExitEventArgs e) - { - using (_host) - { - _host.StopAsync(); - } - base.OnExit(e); - } } } diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs index a625bb77..25cc53e6 100644 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs +++ b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs @@ -6,6 +6,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using DynamicData; @@ -25,9 +26,10 @@ namespace Wabbajack public class ModListGalleryVM : BackNavigatingVM { public MainWindowVM MWVM { get; } - + private readonly SourceCache _modLists = new(x => x.Metadata.NamespacedName); public ReadOnlyObservableCollection _filteredModLists; + public ReadOnlyObservableCollection ModLists => _filteredModLists; private const string ALL_GAME_TYPE = "All"; @@ -44,19 +46,45 @@ namespace Wabbajack [Reactive] public string GameType { get; set; } - public List GameTypeEntries => GetGameTypeEntries(); + public class GameTypeEntry + { + public GameTypeEntry(string humanFriendlyName, int amount) + { + HumanFriendlyName = humanFriendlyName; + Amount = amount; + FormattedName = $"{HumanFriendlyName} ({Amount})"; + } + public string HumanFriendlyName { get; set; } + public int Amount { get; set; } + public string FormattedName { get; set; } + } + + [Reactive] public List GameTypeEntries { get; set; } + private bool _filteringOnGame; + private GameTypeEntry _selectedGameTypeEntry = null; + + public GameTypeEntry SelectedGameTypeEntry + { + get => _selectedGameTypeEntry; + set + { + RaiseAndSetIfChanged(ref _selectedGameTypeEntry, value == null ? GameTypeEntries?.FirstOrDefault(gte => gte.HumanFriendlyName == ALL_GAME_TYPE) : value); + GameType = _selectedGameTypeEntry?.HumanFriendlyName; + } + } private readonly Client _wjClient; private readonly ILogger _logger; private readonly GameLocator _locator; private readonly ModListDownloadMaintainer _maintainer; private readonly SettingsManager _settingsManager; + private readonly CancellationToken _cancellationToken; private FiltersSettings settings { get; set; } = new(); public ICommand ClearFiltersCommand { get; set; } - public ModListGalleryVM(ILogger logger, Client wjClient, - GameLocator locator, SettingsManager settingsManager, ModListDownloadMaintainer maintainer) + public ModListGalleryVM(ILogger logger, Client wjClient, GameLocator locator, + SettingsManager settingsManager, ModListDownloadMaintainer maintainer, CancellationToken cancellationToken) : base(logger) { _wjClient = wjClient; @@ -64,7 +92,8 @@ namespace Wabbajack _locator = locator; _maintainer = maintainer; _settingsManager = settingsManager; - + _cancellationToken = cancellationToken; + ClearFiltersCommand = ReactiveCommand.Create( () => { @@ -72,9 +101,9 @@ namespace Wabbajack ShowNSFW = false; ShowUnofficialLists = false; Search = string.Empty; - GameType = ALL_GAME_TYPE; + SelectedGameTypeEntry = GameTypeEntries.FirstOrDefault(); }); - + BackCommand = ReactiveCommand.Create( () => { @@ -83,13 +112,13 @@ namespace Wabbajack this.WhenActivated(disposables => - { + { LoadModLists().FireAndForget(); LoadSettings().FireAndForget(); Disposable.Create(() => SaveSettings().FireAndForget()) .DisposeWith(disposables); - + var searchTextPredicates = this.ObservableForProperty(vm => vm.Search) .Select(change => change.Value) .StartWith("") @@ -99,7 +128,7 @@ namespace Wabbajack return item => item.Metadata.Title.ContainsCaseInsensitive(txt) || item.Metadata.Description.ContainsCaseInsensitive(txt); }); - + var onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalled) .Select(v => v.Value) .Select>(onlyInstalled => @@ -108,7 +137,7 @@ namespace Wabbajack return item => _locator.IsInstalled(item.Metadata.Game); }) .StartWith(_ => true); - + var showUnofficial = this.ObservableForProperty(vm => vm.ShowUnofficialLists) .Select(v => v.Value) .StartWith(false) @@ -117,21 +146,22 @@ namespace Wabbajack if (unoffical) return x => true; return x => x.Metadata.Official; }); - + var showNSFWFilter = this.ObservableForProperty(vm => vm.ShowNSFW) .Select(v => v.Value) .Select>(showNsfw => { return item => item.Metadata.NSFW == showNsfw; }) .StartWith(item => item.Metadata.NSFW == false); - + var gameFilter = this.ObservableForProperty(vm => vm.GameType) .Select(v => v.Value) .Select>(selected => { + _filteringOnGame = true; if (selected is null or ALL_GAME_TYPE) return _ => true; return item => item.Metadata.Game.MetaData().HumanFriendlyGameName == selected; }) .StartWith(_ => true); - + _modLists.Connect() .ObserveOn(RxApp.MainThreadScheduler) .Filter(searchTextPredicates) @@ -140,7 +170,18 @@ namespace Wabbajack .Filter(showNSFWFilter) .Filter(gameFilter) .Bind(out _filteredModLists) - .Subscribe() + .Subscribe((_) => + { + if (!_filteringOnGame) + { + var previousGameType = GameType; + SelectedGameTypeEntry = null; + GameTypeEntries = new(GetGameTypeEntries()); + var nextEntry = GameTypeEntries.FirstOrDefault(gte => previousGameType == gte.HumanFriendlyName); + SelectedGameTypeEntry = nextEntry != default ? nextEntry : GameTypeEntries.FirstOrDefault(gte => GameType == ALL_GAME_TYPE); + } + _filteringOnGame = false; + }) .DisposeWith(disposables); }); } @@ -174,10 +215,10 @@ namespace Wabbajack private async Task LoadSettings() { using var ll = LoadingLock.WithLoading(); - RxApp.MainThreadScheduler.Schedule(await _settingsManager.Load("modlist_gallery"), + RxApp.MainThreadScheduler.Schedule(await _settingsManager.Load("modlist_gallery"), (_, s) => { - GameType = s.GameType; + SelectedGameTypeEntry = GameTypeEntries?.FirstOrDefault(gte => gte.HumanFriendlyName.Equals(s.GameType)); ShowNSFW = s.ShowNSFW; ShowUnofficialLists = s.ShowUnofficialLists; Search = s.Search; @@ -196,7 +237,7 @@ namespace Wabbajack { e.Clear(); e.AddOrUpdate(modLists.Select(m => - new ModListMetadataVM(_logger, this, m, _maintainer, _wjClient))); + new ModListMetadataVM(_logger, this, m, _maintainer, _wjClient, _cancellationToken))); }); } catch (Exception ex) @@ -207,13 +248,14 @@ namespace Wabbajack ll.Succeed(); } - private List GetGameTypeEntries() + private List GetGameTypeEntries() { - List gameEntries = new List {ALL_GAME_TYPE}; - gameEntries.AddRange(GameRegistry.Games.Values.Select(gameType => gameType.HumanFriendlyGameName)); - gameEntries.Sort(); - return gameEntries; + return ModLists.Select(fm => fm.Metadata) + .GroupBy(m => m.Game) + .Select(g => new GameTypeEntry(g.Key.MetaData().HumanFriendlyGameName, g.Count())) + .OrderBy(gte => gte.HumanFriendlyName) + .Prepend(new GameTypeEntry(ALL_GAME_TYPE, ModLists.Count)) + .ToList(); } - } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs index 0c172dc7..3bd14e44 100644 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs +++ b/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reactive; using System.Reactive.Linq; using System.Reactive.Subjects; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Media.Imaging; @@ -94,15 +95,17 @@ namespace Wabbajack private readonly ILogger _logger; private readonly ModListDownloadMaintainer _maintainer; private readonly Client _wjClient; + private readonly CancellationToken _cancellationToken; public ModListMetadataVM(ILogger logger, ModListGalleryVM parent, ModlistMetadata metadata, - ModListDownloadMaintainer maintainer, Client wjClient) + ModListDownloadMaintainer maintainer, Client wjClient, CancellationToken cancellationToken) { _logger = logger; _parent = parent; _maintainer = maintainer; Metadata = metadata; _wjClient = wjClient; + _cancellationToken = cancellationToken; Location = LauncherUpdater.CommonFolder.Value.Combine("downloaded_mod_lists", Metadata.NamespacedName).WithExtension(Ext.Wabbajack); ModListTagList = new List(); @@ -184,7 +187,7 @@ namespace Wabbajack Status = ModListStatus.Downloading; using var ll = LoadingLock.WithLoading(); - var (progress, task) = _maintainer.DownloadModlist(Metadata); + var (progress, task) = _maintainer.DownloadModlist(Metadata, _cancellationToken); var dispose = progress .BindToStrict(this, vm => vm.ProgressPercent); try diff --git a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs index 507d463a..7f912df0 100644 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs @@ -119,6 +119,7 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM private readonly HttpClient _client; private readonly DownloadDispatcher _downloadDispatcher; private readonly IEnumerable _logins; + private readonly CancellationToken _cancellationToken; public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; [Reactive] @@ -146,7 +147,8 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, IServiceProvider serviceProvider, SystemParametersConstructor parametersConstructor, IGameLocator gameLocator, LogStream loggerProvider, ResourceMonitor resourceMonitor, - Wabbajack.Services.OSIntegrated.Configuration configuration, HttpClient client, DownloadDispatcher dispatcher, IEnumerable logins) : base(logger) + Wabbajack.Services.OSIntegrated.Configuration configuration, HttpClient client, DownloadDispatcher dispatcher, IEnumerable logins, + CancellationToken cancellationToken) : base(logger) { _logger = logger; _configuration = configuration; @@ -160,18 +162,19 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM _client = client; _downloadDispatcher = dispatcher; _logins = logins; - + _cancellationToken = cancellationToken; + Installer = new MO2InstallerVM(this); BackCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView)); BeginCommand = ReactiveCommand.Create(() => BeginInstall().FireAndForget()); - + OpenReadmeCommand = ReactiveCommand.Create(() => { UIUtils.OpenWebsite(new Uri(ModList!.Readme)); - }, LoadingLock.IsNotLoadingObservable); - + }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading, vm => vm.ModList.Readme, (isNotLoading, readme) => isNotLoading && !string.IsNullOrWhiteSpace(readme))); + VisitModListWebsiteCommand = ReactiveCommand.Create(() => { UIUtils.OpenWebsite(ModList!.Website); @@ -312,7 +315,9 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM { var lst = await _settingsManager.Load(LastLoadedModlist); if (lst.FileExists()) - await LoadModlist(lst, null); + { + ModListLocation.TargetPath = lst; + } } private async Task LoadModlist(AbsolutePath path, ModlistMetadata? metadata) @@ -437,8 +442,8 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate, update.StepsProgress.Value); }; - - if (!await installer.Begin(CancellationToken.None)) + + if (!await installer.Begin(_cancellationToken)) { TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error); InstallState = InstallState.Failure; diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs index 3ac56faa..804a14d6 100644 --- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs +++ b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs @@ -243,6 +243,20 @@ namespace Wabbajack return false; } + public void CancelRunningTasks(TimeSpan timeout) + { + var endTime = DateTime.Now.Add(timeout); + var cancellationTokenSource = _serviceProvider.GetRequiredService(); + cancellationTokenSource.Cancel(); + + bool IsInstalling() => Installer.InstallState is InstallState.Installing; + + while (DateTime.Now < endTime && IsInstalling()) + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + } + } + /* public void NavigateTo(ViewModel vm) { diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml index 93497b90..9dc049e9 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml @@ -224,7 +224,7 @@ Margin="30,2" FontSize="20" Style="{StaticResource LargeButtonStyle}" - ToolTip="Open the Discord for this Modlist"> + ToolTip="Open the Discord for the Modlist"> @@ -290,7 +290,7 @@ Margin="30,2" FontSize="20" Style="{StaticResource LargeButtonStyle}" - ToolTip="Open an explicit listing of all actions this modlist will take"> + ToolTip="Open an explicit listing of all archives this modlist contains"> diff --git a/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml b/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml index cd1eb189..5561d7fd 100644 --- a/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml @@ -25,23 +25,27 @@ VerticalAlignment="Center" FontSize="14" Text="Modlist Installation Location" - TextAlignment="Center" /> + TextAlignment="Center" + ToolTip="The directory where the modlist will be installed" /> + PickerVM="{Binding Location}" + ToolTip="The directory where the modlist will be installed" /> + TextAlignment="Center" + ToolTip="The directory where modlist archives will be downloaded to" /> + PickerVM="{Binding DownloadLocation}" + ToolTip="The directory where modlist archives will be downloaded to" /> - diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs index 4875b58b..4cf49089 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs @@ -3,7 +3,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; -using System.Threading.Tasks; +using System.Threading; using System.Windows; using System.Windows.Input; using DynamicData; @@ -55,11 +55,8 @@ namespace Wabbajack Closed += (s, e) => { - Task.Run(async () => - { - await Task.Delay(5000); - Environment.Exit(0); - }); + _mwvm.CancelRunningTasks(TimeSpan.FromSeconds(10)); + Application.Current.Shutdown(); }; MessageBus.Current.Listen() diff --git a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml index 9f14a0d3..415af5e8 100644 --- a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml +++ b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml @@ -91,45 +91,52 @@