diff --git a/.gitignore b/.gitignore index cf1eb9bc8..7ee716998 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ obj *.user .\packages\* /VERSION.txt -*.opencover.xml \ No newline at end of file +*.opencover.xml diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs index b796692de..3775699ee 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -26,176 +26,180 @@ using Wabbajack.Util; using Ext = Wabbajack.Common.Ext; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for App.xaml +/// +public partial class App { - /// - /// Interaction logic for App.xaml - /// - public partial class App - { - private IHost _host; + private IHost _host; - private void OnStartup(object sender, StartupEventArgs e) + private void OnStartup(object sender, StartupEventArgs e) + { + if (IsAdmin()) { - if (IsAdmin()) + var messageBox = MessageBox.Show("Don't run Wabbajack as Admin!", "Error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK, MessageBoxOptions.DefaultDesktopOnly); + if (messageBox == MessageBoxResult.OK) { - var messageBox = MessageBox.Show("Don't run Wabbajack as Admin!", "Error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK, MessageBoxOptions.DefaultDesktopOnly); - if (messageBox == MessageBoxResult.OK) - { - Environment.Exit(1); - } - else - { - Environment.Exit(1); - } + Environment.Exit(1); + } + else + { + Environment.Exit(1); } + } - RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher); - _host = Host.CreateDefaultBuilder(Array.Empty()) - .ConfigureLogging(AddLogging) - .ConfigureServices((host, services) => - { - ConfigureServices(services); - }) - .Build(); + RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher); + _host = Host.CreateDefaultBuilder(Array.Empty()) + .ConfigureLogging(AddLogging) + .ConfigureServices((host, services) => + { + ConfigureServices(services); + }) + .Build(); - var args = e.Args; + var args = e.Args; - RxApp.MainThreadScheduler.Schedule(0, (_, _) => + RxApp.MainThreadScheduler.Schedule(0, (_, _) => + { + if (args.Length == 1) { - if (args.Length == 1) - { - var arg = args[0].ToAbsolutePath(); - if (arg.FileExists() && arg.Extension == Ext.Wabbajack) - { - var mainWindow = _host.Services.GetRequiredService(); - mainWindow!.Show(); - return Disposable.Empty; - } - } else if (args.Length > 0) - { - var builder = _host.Services.GetRequiredService(); - builder.Run(e.Args).ContinueWith(async x => - { - Environment.Exit(await x); - }); - return Disposable.Empty; - } - else + var arg = args[0].ToAbsolutePath(); + if (arg.FileExists() && arg.Extension == Ext.Wabbajack) { var mainWindow = _host.Services.GetRequiredService(); mainWindow!.Show(); return Disposable.Empty; } - + } else if (args.Length > 0) + { + var builder = _host.Services.GetRequiredService(); + builder.Run(e.Args).ContinueWith(async x => + { + Environment.Exit(await x); + }); return Disposable.Empty; - }); - } + } + else + { + var mainWindow = _host.Services.GetRequiredService(); + mainWindow!.Show(); + return Disposable.Empty; + } - private static bool IsAdmin() - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; + return Disposable.Empty; + }); + } - try - { - var identity = WindowsIdentity.GetCurrent(); - var owner = identity.Owner; - if (owner is not null) return owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); + private static bool IsAdmin() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; + + try + { + var identity = WindowsIdentity.GetCurrent(); + var owner = identity.Owner; + if (owner is not null) return owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); - var principle = new WindowsPrincipal(identity); - return principle.IsInRole(WindowsBuiltInRole.Administrator); + var principle = new WindowsPrincipal(identity); + return principle.IsInRole(WindowsBuiltInRole.Administrator); - } - catch (Exception) - { - return false; - } } - - private void AddLogging(ILoggingBuilder loggingBuilder) + catch (Exception) { - var config = new NLog.Config.LoggingConfiguration(); + return false; + } + } - var logFolder = KnownFolders.LauncherAwarePath.Combine("logs"); - if (!logFolder.DirectoryExists()) - logFolder.CreateDirectory(); + private void AddLogging(ILoggingBuilder loggingBuilder) + { + var config = new NLog.Config.LoggingConfiguration(); - var fileTarget = new FileTarget("file") - { - FileName = logFolder.Combine("Wabbajack.current.log").ToString(), - ArchiveFileName = logFolder.Combine("Wabbajack.{##}.log").ToString(), - ArchiveOldFileOnStartup = true, - MaxArchiveFiles = 10, - Layout = "${processtime} [${level:uppercase=true}] (${logger}) ${message:withexception=true}", - Header = "############ Wabbajack log file - ${longdate} ############" - }; + var logFolder = KnownFolders.LauncherAwarePath.Combine("logs"); + if (!logFolder.DirectoryExists()) + logFolder.CreateDirectory(); - var consoleTarget = new ConsoleTarget("console"); + var fileTarget = new FileTarget("file") + { + FileName = logFolder.Combine("Wabbajack.current.log").ToString(), + ArchiveFileName = logFolder.Combine("Wabbajack.{##}.log").ToString(), + ArchiveOldFileOnStartup = true, + MaxArchiveFiles = 10, + Layout = "${processtime} [${level:uppercase=true}] (${logger}) ${message:withexception=true}", + Header = "############ Wabbajack log file - ${longdate} ############" + }; - var uiTarget = new LogStream - { - Name = "ui", - Layout = "${message:withexception=false}", - }; + var consoleTarget = new ConsoleTarget("console"); + + var uiTarget = new LogStream + { + Name = "ui", + Layout = "${message:withexception=false}", + }; - loggingBuilder.Services.AddSingleton(uiTarget); + loggingBuilder.Services.AddSingleton(uiTarget); - config.AddRuleForAllLevels(fileTarget); - config.AddRuleForAllLevels(consoleTarget); - config.AddRuleForAllLevels(uiTarget); + config.AddRuleForAllLevels(fileTarget); + config.AddRuleForAllLevels(consoleTarget); + config.AddRuleForAllLevels(uiTarget); - loggingBuilder.ClearProviders(); - loggingBuilder.SetMinimumLevel(LogLevel.Information); - loggingBuilder.AddNLog(config); - } + loggingBuilder.ClearProviders(); + loggingBuilder.SetMinimumLevel(LogLevel.Information); + loggingBuilder.AddNLog(config); + } - private static IServiceCollection ConfigureServices(IServiceCollection services) - { - services.AddOSIntegrated(); - - // Orc.FileAssociation - services.AddSingleton(new ApplicationRegistrationService()); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // Login Handlers - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // Login Managers - - //Disabled LL because it is currently not used and broken due to the way LL butchers their API - //services.AddAllSingleton(); - services.AddAllSingleton(); - //Disabled VP due to frequent login issues & because the only file that really got downloaded there has a mirror - //services.AddAllSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Verbs - services.AddSingleton(); - services.AddCLIVerbs(); - - return services; - } + private static IServiceCollection ConfigureServices(IServiceCollection services) + { + services.AddOSIntegrated(); + + // Orc.FileAssociation + services.AddSingleton(new ApplicationRegistrationService()); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Login Handlers + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Login Managers + + //Disabled LL because it is currently not used and broken due to the way LL butchers their API + //services.AddAllSingleton(); + services.AddAllSingleton(); + //Disabled VP due to frequent login issues & because the only file that really got downloaded there has a mirror + //services.AddAllSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Verbs + services.AddSingleton(); + services.AddCLIVerbs(); + + return services; } } diff --git a/Wabbajack.App.Wpf/Consts.cs b/Wabbajack.App.Wpf/Consts.cs index 8f1ada392..e8b55d4c0 100644 --- a/Wabbajack.App.Wpf/Consts.cs +++ b/Wabbajack.App.Wpf/Consts.cs @@ -9,6 +9,11 @@ public static class Consts public static RelativePath MO2IniName = "ModOrganizer.ini".ToRelativePath(); public static string AppName = "Wabbajack"; public static Uri WabbajackBuildServerUri => new("https://build.wabbajack.org"); + public static Uri WabbajackModlistWizardUri => new("https://wizard.wabbajack.org"); + public static Uri WabbajackGithubUri => new("https://github.com/wabbajack-tools/wabbajack"); + public static Uri WabbajackDiscordUri => new("https://discord.gg/wabbajack"); + public static Uri WabbajackPatreonUri => new("https://www.patreon.com/user?u=11907933"); + public static Uri WabbajackWikiUri => new("https://wiki.wabbajack.org"); public static Version CurrentMinimumWabbajackVersion { get; set; } = Version.Parse("2.3.0.0"); public static bool UseNetworkWorkaroundMode { get; set; } = false; public static AbsolutePath CefCacheLocation { get; } = KnownFolders.WabbajackAppLocal.Combine("Cef"); @@ -18,4 +23,14 @@ public static class Consts public static byte SettingsVersion = 0; public static RelativePath NativeSettingsJson = "native_settings.json".ToRelativePath(); + public const string AllSavedCompilerSettingsPaths = "compiler_settings_paths"; + + // Info - TODO, make rich document? + public const string FileManagerInfo = @" +Your modlist will contain lots of files and Wabbajack needs to know where all those files came from to compile a modlist installer. Most of these should be mods that are sourced from the downloads folder. But you might have folders you do **not** want to ship with the modlist, or folders or config files that are generated and can be inlined into the .wabbajack installer. Here is where these files or folders are managed. + +Find more information on the Wabbajack wiki! + +https://wiki.wabbajack.org/modlist_author_documentation/Compilation.html +"; } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Converters/AbsolutePathToStringConverter.cs b/Wabbajack.App.Wpf/Converters/AbsolutePathToStringConverter.cs index 753bf0451..89acc813b 100644 --- a/Wabbajack.App.Wpf/Converters/AbsolutePathToStringConverter.cs +++ b/Wabbajack.App.Wpf/Converters/AbsolutePathToStringConverter.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Windows.Data; using ReactiveUI; -using Wabbajack.Common; using Wabbajack.Paths; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/Converters/CommandConverter.cs b/Wabbajack.App.Wpf/Converters/CommandConverter.cs index 2cee9ae30..da9cc8e69 100644 --- a/Wabbajack.App.Wpf/Converters/CommandConverter.cs +++ b/Wabbajack.App.Wpf/Converters/CommandConverter.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Input; using ReactiveUI; diff --git a/Wabbajack.App.Wpf/Converters/ConverterRegistration.cs b/Wabbajack.App.Wpf/Converters/ConverterRegistration.cs index 2c961991f..cc5ef5a42 100644 --- a/Wabbajack.App.Wpf/Converters/ConverterRegistration.cs +++ b/Wabbajack.App.Wpf/Converters/ConverterRegistration.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using ReactiveUI; +using ReactiveUI; using Splat; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/Converters/IntDownCastConverter.cs b/Wabbajack.App.Wpf/Converters/IntDownCastConverter.cs index ee8f93269..77812d0a1 100644 --- a/Wabbajack.App.Wpf/Converters/IntDownCastConverter.cs +++ b/Wabbajack.App.Wpf/Converters/IntDownCastConverter.cs @@ -1,8 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows.Input; using ReactiveUI; diff --git a/Wabbajack.App.Wpf/Converters/IsNexusArchiveConverter.cs b/Wabbajack.App.Wpf/Converters/IsNexusArchiveConverter.cs new file mode 100644 index 000000000..948f9dfa5 --- /dev/null +++ b/Wabbajack.App.Wpf/Converters/IsNexusArchiveConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; + +namespace Wabbajack +{ + public class IsNexusArchiveConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) return false; + return value is Archive a && a.State.GetType() == typeof(Nexus); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Wabbajack.App.Wpf/Converters/IsTypeVisibilityConverter.cs b/Wabbajack.App.Wpf/Converters/IsTypeVisibilityConverter.cs index b54d5995b..7b228b286 100644 --- a/Wabbajack.App.Wpf/Converters/IsTypeVisibilityConverter.cs +++ b/Wabbajack.App.Wpf/Converters/IsTypeVisibilityConverter.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Windows; using System.Windows.Data; diff --git a/Wabbajack.App.Wpf/Converters/NexusArchiveStateConverter.cs b/Wabbajack.App.Wpf/Converters/NexusArchiveStateConverter.cs new file mode 100644 index 000000000..f25acf9e6 --- /dev/null +++ b/Wabbajack.App.Wpf/Converters/NexusArchiveStateConverter.cs @@ -0,0 +1,27 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using Wabbajack.Common; +using Wabbajack.DTOs.DownloadStates; + +namespace Wabbajack +{ + public class NexusArchiveStateConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if(value is Nexus nexus) + { + var nexusType = value.GetType(); + var nexusProperty = nexusType.GetProperty(parameter.ToString()); + return nexusProperty.GetValue(nexus); + } + return ""; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Wabbajack.App.Wpf/Converters/PercentToDoubleConverter.cs b/Wabbajack.App.Wpf/Converters/PercentToDoubleConverter.cs index 2eb47d55f..daf3992f0 100644 --- a/Wabbajack.App.Wpf/Converters/PercentToDoubleConverter.cs +++ b/Wabbajack.App.Wpf/Converters/PercentToDoubleConverter.cs @@ -1,11 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Input; using ReactiveUI; -using Wabbajack.Common; using Wabbajack.RateLimiter; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/Converters/WidthHeightRectConverter.cs b/Wabbajack.App.Wpf/Converters/WidthHeightRectConverter.cs new file mode 100644 index 000000000..4c8655966 --- /dev/null +++ b/Wabbajack.App.Wpf/Converters/WidthHeightRectConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Wabbajack +{ + public class WidthHeightRectConverter : IMultiValueConverter + { + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + double rectWidth = 0; + double rectHeight = 0; + if (values[0] is not null && double.TryParse(values[0].ToString(), out var width)) + rectWidth = width; + else return null; + if (values[1] is not null && double.TryParse(values[1].ToString(), out var height)) + rectHeight = height; + else return null; + return new Rect(0, 0, rectWidth, rectHeight); + } + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + => throw new NotImplementedException(); + } +} diff --git a/Wabbajack.App.Wpf/Extensions/DynamicDataExt.cs b/Wabbajack.App.Wpf/Extensions/DynamicDataExt.cs index 41561fe76..b36e2e88a 100644 --- a/Wabbajack.App.Wpf/Extensions/DynamicDataExt.cs +++ b/Wabbajack.App.Wpf/Extensions/DynamicDataExt.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; using DynamicData; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/Extensions/IViewForExt.cs b/Wabbajack.App.Wpf/Extensions/IViewForExt.cs index 659187755..fde2fca7c 100644 --- a/Wabbajack.App.Wpf/Extensions/IViewForExt.cs +++ b/Wabbajack.App.Wpf/Extensions/IViewForExt.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; using ReactiveUI; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/Interventions/AErrorMessage.cs b/Wabbajack.App.Wpf/Interventions/AErrorMessage.cs index 94105a0fe..73cd65654 100644 --- a/Wabbajack.App.Wpf/Interventions/AErrorMessage.cs +++ b/Wabbajack.App.Wpf/Interventions/AErrorMessage.cs @@ -1,12 +1,11 @@ using System; -namespace Wabbajack.Interventions +namespace Wabbajack.Interventions; + +public abstract class AErrorMessage : Exception, IException { - public abstract class AErrorMessage : Exception, IException - { - public DateTime Timestamp { get; } = DateTime.Now; - public abstract string ShortDescription { get; } - public abstract string ExtendedDescription { get; } - Exception IException.Exception => this; - } + public DateTime Timestamp { get; } = DateTime.Now; + public abstract string ShortDescription { get; } + public abstract string ExtendedDescription { get; } + Exception IException.Exception => this; } diff --git a/Wabbajack.App.Wpf/Interventions/AUserIntervention.cs b/Wabbajack.App.Wpf/Interventions/AUserIntervention.cs index f8fd944e2..2da28b651 100644 --- a/Wabbajack.App.Wpf/Interventions/AUserIntervention.cs +++ b/Wabbajack.App.Wpf/Interventions/AUserIntervention.cs @@ -1,37 +1,30 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Windows.Input; using ReactiveUI; -using Wabbajack.Common; using Wabbajack.DTOs.Interventions; -using Wabbajack.Interventions; -namespace Wabbajack +namespace Wabbajack; + +public abstract class AUserIntervention : ReactiveObject, IUserIntervention { - public abstract class AUserIntervention : ReactiveObject, IUserIntervention - { - public DateTime Timestamp { get; } = DateTime.Now; - public abstract string ShortDescription { get; } - public abstract string ExtendedDescription { get; } + public DateTime Timestamp { get; } = DateTime.Now; + public abstract string ShortDescription { get; } + public abstract string ExtendedDescription { get; } - private bool _handled; - public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); } - public CancellationToken Token { get; } - public void SetException(Exception exception) - { - throw new NotImplementedException(); - } + private bool _handled; + public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); } + public CancellationToken Token { get; } + public void SetException(Exception exception) + { + throw new NotImplementedException(); + } - public abstract void Cancel(); - public ICommand CancelCommand { get; } + public abstract void Cancel(); + public ICommand CancelCommand { get; } - public AUserIntervention() - { - CancelCommand = ReactiveCommand.Create(() => Cancel()); - } + public AUserIntervention() + { + CancelCommand = ReactiveCommand.Create(() => Cancel()); } } diff --git a/Wabbajack.App.Wpf/Interventions/ConfirmationIntervention.cs b/Wabbajack.App.Wpf/Interventions/ConfirmationIntervention.cs index f0ce10670..0827b9ca4 100644 --- a/Wabbajack.App.Wpf/Interventions/ConfirmationIntervention.cs +++ b/Wabbajack.App.Wpf/Interventions/ConfirmationIntervention.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Threading.Tasks; using System.Windows.Input; using ReactiveUI; diff --git a/Wabbajack.App.Wpf/Interventions/IError.cs b/Wabbajack.App.Wpf/Interventions/IError.cs index 15c0c443f..f88de312b 100644 --- a/Wabbajack.App.Wpf/Interventions/IError.cs +++ b/Wabbajack.App.Wpf/Interventions/IError.cs @@ -1,6 +1,5 @@ -namespace Wabbajack.Interventions +namespace Wabbajack.Interventions; + +public interface IError : IStatusMessage { - public interface IError : IStatusMessage - { - } } diff --git a/Wabbajack.App.Wpf/Interventions/IException.cs b/Wabbajack.App.Wpf/Interventions/IException.cs index 85d0d2705..2fbee5a5e 100644 --- a/Wabbajack.App.Wpf/Interventions/IException.cs +++ b/Wabbajack.App.Wpf/Interventions/IException.cs @@ -1,9 +1,8 @@ using System; -namespace Wabbajack.Interventions +namespace Wabbajack.Interventions; + +public interface IException : IError { - public interface IException : IError - { - Exception Exception { get; } - } + Exception Exception { get; } } diff --git a/Wabbajack.App.Wpf/Interventions/IStatusMessage.cs b/Wabbajack.App.Wpf/Interventions/IStatusMessage.cs index 7d01ad50d..2dba5b6a7 100644 --- a/Wabbajack.App.Wpf/Interventions/IStatusMessage.cs +++ b/Wabbajack.App.Wpf/Interventions/IStatusMessage.cs @@ -1,11 +1,10 @@ using System; -namespace Wabbajack.Interventions +namespace Wabbajack.Interventions; + +public interface IStatusMessage { - public interface IStatusMessage - { - DateTime Timestamp { get; } - string ShortDescription { get; } - string ExtendedDescription { get; } - } + DateTime Timestamp { get; } + string ShortDescription { get; } + string ExtendedDescription { get; } } diff --git a/Wabbajack.App.Wpf/Interventions/UserInterventionHandler.cs b/Wabbajack.App.Wpf/Interventions/UserInterventionHandler.cs index 549ae093d..c1f2643f4 100644 --- a/Wabbajack.App.Wpf/Interventions/UserInterventionHandler.cs +++ b/Wabbajack.App.Wpf/Interventions/UserInterventionHandler.cs @@ -1,6 +1,4 @@ using System; -using System.Reactive.Disposables; -using System.Windows.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ReactiveUI; @@ -10,12 +8,12 @@ namespace Wabbajack.Interventions; -public class UserIntreventionHandler : IUserInterventionHandler +public class UserInterventionHandler : IUserInterventionHandler { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - public UserIntreventionHandler(ILogger logger, IServiceProvider serviceProvider) + public UserInterventionHandler(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; _serviceProvider = serviceProvider; diff --git a/Wabbajack.App.Wpf/LauncherUpdater.cs b/Wabbajack.App.Wpf/LauncherUpdater.cs index 96d3fd6be..738e30b8a 100644 --- a/Wabbajack.App.Wpf/LauncherUpdater.cs +++ b/Wabbajack.App.Wpf/LauncherUpdater.cs @@ -2,11 +2,9 @@ using System.Diagnostics; using System.Linq; using System.Net.Http; -using System.Net.Http.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.VisualBasic.CompilerServices; using Newtonsoft.Json; using Wabbajack.Common; using Wabbajack.Downloaders; @@ -14,160 +12,157 @@ using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Networking.Http; -using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Paths; using Wabbajack.Paths.IO; -using Wabbajack.RateLimiter; -namespace Wabbajack +namespace Wabbajack; + +public class LauncherUpdater { - public class LauncherUpdater + private readonly ILogger _logger; + private readonly HttpClient _client; + private readonly Client _wjclient; + private readonly DTOSerializer _dtos; + + private readonly DownloadDispatcher _downloader; + + private static Uri GITHUB_REPO_RELEASES = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases"); + + public LauncherUpdater(ILogger logger, HttpClient client, Client wjclient, DTOSerializer dtos, + DownloadDispatcher downloader) { - private readonly ILogger _logger; - private readonly HttpClient _client; - private readonly Client _wjclient; - private readonly DTOSerializer _dtos; + _logger = logger; + _client = client; + _wjclient = wjclient; + _dtos = dtos; + _downloader = downloader; + } - private readonly DownloadDispatcher _downloader; - private static Uri GITHUB_REPO_RELEASES = new("https://api.github.com/repos/wabbajack-tools/wabbajack/releases"); + public static Lazy CommonFolder = new (() => + { + var entryPoint = KnownFolders.EntryPoint; - public LauncherUpdater(ILogger logger, HttpClient client, Client wjclient, DTOSerializer dtos, - DownloadDispatcher downloader) + // If we're not in a folder that looks like a version, abort + if (!Version.TryParse(entryPoint.FileName.ToString(), out var version)) { - _logger = logger; - _client = client; - _wjclient = wjclient; - _dtos = dtos; - _downloader = downloader; + return entryPoint; } - - public static Lazy CommonFolder = new (() => + // If we're not in a folder that has Wabbajack.exe in the parent folder, abort + if (!entryPoint.Parent.Combine(Consts.AppName).WithExtension(new Extension(".exe")).FileExists()) { - var entryPoint = KnownFolders.EntryPoint; - - // If we're not in a folder that looks like a version, abort - if (!Version.TryParse(entryPoint.FileName.ToString(), out var version)) - { - return entryPoint; - } + return entryPoint; + } - // If we're not in a folder that has Wabbajack.exe in the parent folder, abort - if (!entryPoint.Parent.Combine(Consts.AppName).WithExtension(new Extension(".exe")).FileExists()) - { - return entryPoint; - } + return entryPoint.Parent; + }); - return entryPoint.Parent; - }); + public async Task Run() + { - public async Task Run() + if (CommonFolder.Value == KnownFolders.EntryPoint) { + _logger.LogInformation("Outside of standard install folder, not updating"); + return; + } - if (CommonFolder.Value == KnownFolders.EntryPoint) - { - _logger.LogInformation("Outside of standard install folder, not updating"); - return; - } + var version = Version.Parse(KnownFolders.EntryPoint.FileName.ToString()); - var version = Version.Parse(KnownFolders.EntryPoint.FileName.ToString()); + var oldVersions = CommonFolder.Value + .EnumerateDirectories() + .Select(f => Version.TryParse(f.FileName.ToString(), out var ver) ? (ver, f) : default) + .Where(f => f != default) + .Where(f => f.ver < version) + .Select(f => f!) + .OrderByDescending(f => f) + .Skip(2) + .ToArray(); - var oldVersions = CommonFolder.Value - .EnumerateDirectories() - .Select(f => Version.TryParse(f.FileName.ToString(), out var ver) ? (ver, f) : default) - .Where(f => f != default) - .Where(f => f.ver < version) - .Select(f => f!) - .OrderByDescending(f => f) - .Skip(2) - .ToArray(); + foreach (var (_, path) in oldVersions) + { + _logger.LogInformation("Deleting old Wabbajack version at: {Path}", path); + path.DeleteDirectory(); + } - foreach (var (_, path) in oldVersions) + var release = (await GetReleases()) + .Select(release => Version.TryParse(release.Tag, out version) ? (version, release) : default) + .Where(r => r != default) + .OrderByDescending(r => r.version) + .Select(r => { - _logger.LogInformation("Deleting old Wabbajack version at: {Path}", path); - path.DeleteDirectory(); - } + var (version, release) = r; + var asset = release.Assets.FirstOrDefault(a => a.Name == "Wabbajack.exe"); + return asset != default ? (version, release, asset) : default; + }) + .FirstOrDefault(); - var release = (await GetReleases()) - .Select(release => Version.TryParse(release.Tag, out version) ? (version, release) : default) - .Where(r => r != default) - .OrderByDescending(r => r.version) - .Select(r => - { - var (version, release) = r; - var asset = release.Assets.FirstOrDefault(a => a.Name == "Wabbajack.exe"); - return asset != default ? (version, release, asset) : default; - }) - .FirstOrDefault(); + var launcherFolder = KnownFolders.EntryPoint.Parent; + var exePath = launcherFolder.Combine("Wabbajack.exe"); - var launcherFolder = KnownFolders.EntryPoint.Parent; - var exePath = launcherFolder.Combine("Wabbajack.exe"); + var launcherVersion = FileVersionInfo.GetVersionInfo(exePath.ToString()); - var launcherVersion = FileVersionInfo.GetVersionInfo(exePath.ToString()); + if (release != default && release.version > Version.Parse(launcherVersion.FileVersion!)) + { + _logger.LogInformation("Updating Launcher from {OldVersion} to {NewVersion}", launcherVersion.FileVersion, release.version); + var tempPath = launcherFolder.Combine("Wabbajack.exe.temp"); - if (release != default && release.version > Version.Parse(launcherVersion.FileVersion!)) + await _downloader.Download(new Archive { - _logger.LogInformation("Updating Launcher from {OldVersion} to {NewVersion}", launcherVersion.FileVersion, release.version); - var tempPath = launcherFolder.Combine("Wabbajack.exe.temp"); - - await _downloader.Download(new Archive - { - State = new Http {Url = release.asset.BrowserDownloadUrl!}, - Name = release.asset.Name, - Size = release.asset.Size - }, tempPath, CancellationToken.None); - - if (tempPath.Size() != release.asset.Size) - { - _logger.LogInformation( - "Downloaded launcher did not match expected size: {DownloadedSize} expected {ExpectedSize}", tempPath.Size(), release.asset.Size); - return; - } - - if (exePath.FileExists()) - exePath.Delete(); - await tempPath.MoveToAsync(exePath, true, CancellationToken.None); - - _logger.LogInformation("Finished updating wabbajack"); - await _wjclient.SendMetric("updated_launcher", $"{launcherVersion.FileVersion} -> {release.version}"); + State = new Http {Url = release.asset.BrowserDownloadUrl!}, + Name = release.asset.Name, + Size = release.asset.Size + }, tempPath, CancellationToken.None); + + if (tempPath.Size() != release.asset.Size) + { + _logger.LogInformation( + "Downloaded launcher did not match expected size: {DownloadedSize} expected {ExpectedSize}", tempPath.Size(), release.asset.Size); + return; } - } - private async Task GetReleases() - { - _logger.LogInformation("Getting new Wabbajack version list"); - var msg = MakeMessage(GITHUB_REPO_RELEASES); - return await _client.GetJsonFromSendAsync(msg, _dtos.Options); - } + if (exePath.FileExists()) + exePath.Delete(); + await tempPath.MoveToAsync(exePath, true, CancellationToken.None); - private HttpRequestMessage MakeMessage(Uri uri) - { - var msg = new HttpRequestMessage(HttpMethod.Get, uri); - msg.AddChromeAgent(); - return msg; + _logger.LogInformation("Finished updating wabbajack"); + await _wjclient.SendMetric("updated_launcher", $"{launcherVersion.FileVersion} -> {release.version}"); } + } + private async Task GetReleases() + { + _logger.LogInformation("Getting new Wabbajack version list"); + var msg = MakeMessage(GITHUB_REPO_RELEASES); + return await _client.GetJsonFromSendAsync(msg, _dtos.Options); + } - class Release - { - [JsonProperty("tag_name")] public string Tag { get; set; } = ""; + private HttpRequestMessage MakeMessage(Uri uri) + { + var msg = new HttpRequestMessage(HttpMethod.Get, uri); + msg.AddChromeAgent(); + return msg; + } - [JsonProperty("assets")] public Asset[] Assets { get; set; } = Array.Empty(); - } + class Release + { + [JsonProperty("tag_name")] public string Tag { get; set; } = ""; - class Asset - { - [JsonProperty("browser_download_url")] - public Uri? BrowserDownloadUrl { get; set; } + [JsonProperty("assets")] public Asset[] Assets { get; set; } = Array.Empty(); - [JsonProperty("name")] public string Name { get; set; } = ""; + } - [JsonProperty("size")] public long Size { get; set; } = 0; - } + class Asset + { + [JsonProperty("browser_download_url")] + public Uri? BrowserDownloadUrl { get; set; } + + [JsonProperty("name")] public string Name { get; set; } = ""; + + [JsonProperty("size")] public long Size { get; set; } = 0; } } diff --git a/Wabbajack.App.Wpf/LoginManagers/INeedsLogin.cs b/Wabbajack.App.Wpf/LoginManagers/INeedsLogin.cs index aaed7797f..9e6cea24a 100644 --- a/Wabbajack.App.Wpf/LoginManagers/INeedsLogin.cs +++ b/Wabbajack.App.Wpf/LoginManagers/INeedsLogin.cs @@ -1,9 +1,6 @@ - using System; -using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Media; -using ReactiveUI; using Wabbajack.Downloaders.Interfaces; namespace Wabbajack.LoginManagers; diff --git a/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs index 8e982af75..cffe1050b 100644 --- a/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs @@ -1,13 +1,8 @@ using System; -using System.Drawing; using System.Reactive.Linq; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; -using System.Windows.Threading; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ReactiveUI; @@ -15,7 +10,6 @@ using Wabbajack.Common; using Wabbajack.Downloaders.IPS4OAuth2Downloader; using Wabbajack.DTOs.Logins; -using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.UserIntervention; diff --git a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs index 27ff83543..ca7437c5c 100644 --- a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs @@ -39,7 +39,7 @@ public NexusLoginManager(ILogger logger, ITokenProvider await RefreshTokenState()); + Task.Run(RefreshTokenState); ClearLogin = ReactiveCommand.CreateFromTask(async () => { diff --git a/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs index 62a13e260..ab2260342 100644 --- a/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs @@ -1,9 +1,5 @@ using System; -using System.Drawing; using System.Reactive.Linq; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -14,7 +10,6 @@ using Wabbajack.Common; using Wabbajack.Downloaders.IPS4OAuth2Downloader; using Wabbajack.DTOs.Logins; -using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.UserIntervention; diff --git a/Wabbajack.App.Wpf/MarkupExtensions/EnumMarkupConverter.cs b/Wabbajack.App.Wpf/MarkupExtensions/EnumMarkupConverter.cs new file mode 100644 index 000000000..f9514f994 --- /dev/null +++ b/Wabbajack.App.Wpf/MarkupExtensions/EnumMarkupConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Windows.Markup; + +namespace Wabbajack; + +public class EnumToItemsSource : MarkupExtension +{ + private readonly Type _type; + + public EnumToItemsSource(Type type) + { + _type = type; + } + public static string GetEnumDescription(Enum value) + { + FieldInfo fi = value.GetType().GetField(value.ToString()); + + DescriptionAttribute[] attributes = fi.GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; + + if (attributes != null && attributes.Any()) + { + return attributes.First().Description; + } + + return value.ToString(); + } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + return Enum.GetValues(_type) + .Cast() + .Select(e => + { + return new + { + Value = e, + DisplayName = GetEnumDescription((Enum)e) + }; + }); + } +} diff --git a/Wabbajack.App.Wpf/Messages/ALoginMessage.cs b/Wabbajack.App.Wpf/Messages/ALoginMessage.cs index 921cf97ba..5ce947184 100644 --- a/Wabbajack.App.Wpf/Messages/ALoginMessage.cs +++ b/Wabbajack.App.Wpf/Messages/ALoginMessage.cs @@ -1,7 +1,6 @@ using System; using System.Threading; using System.Threading.Tasks; -using ReactiveUI; using Wabbajack.DTOs.Interventions; namespace Wabbajack.Messages; diff --git a/Wabbajack.App.Wpf/Messages/HideNavigation.cs b/Wabbajack.App.Wpf/Messages/HideNavigation.cs new file mode 100644 index 000000000..b96bf8a6b --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/HideNavigation.cs @@ -0,0 +1,16 @@ +using ReactiveUI; +using Wabbajack.Compiler; + +namespace Wabbajack.Messages; + +public class HideNavigation +{ + public HideNavigation() + { + } + + public static void Send() + { + MessageBus.Current.SendMessage(new HideNavigation()); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/LoadCompilerSettings.cs b/Wabbajack.App.Wpf/Messages/LoadCompilerSettings.cs new file mode 100644 index 000000000..b255f85e7 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/LoadCompilerSettings.cs @@ -0,0 +1,18 @@ +using ReactiveUI; +using Wabbajack.Compiler; + +namespace Wabbajack.Messages; + +public class LoadCompilerSettings +{ + public CompilerSettings CompilerSettings { get; set; } + public LoadCompilerSettings(CompilerSettings cs) + { + CompilerSettings = cs; + } + + public static void Send(CompilerSettings cs) + { + MessageBus.Current.SendMessage(new LoadCompilerSettings(cs)); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/LoadInfoScreen.cs b/Wabbajack.App.Wpf/Messages/LoadInfoScreen.cs new file mode 100644 index 000000000..b59bd4d22 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/LoadInfoScreen.cs @@ -0,0 +1,18 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; +public class LoadInfoScreen +{ + public string Info { get; set; } + public ViewModel NavigateBackTarget { get; set; } + public LoadInfoScreen(string info, ViewModel navigateBackTarget) + { + Info = info; + NavigateBackTarget = navigateBackTarget; + } + public static void Send(string info, ViewModel navigateBackTarget) + { + NavigateToGlobal.Send(ScreenType.Info); + MessageBus.Current.SendMessage(new LoadInfoScreen(info, navigateBackTarget)); + } +} diff --git a/Wabbajack.App.Wpf/Messages/LoadLastLoadedModlist.cs b/Wabbajack.App.Wpf/Messages/LoadLastLoadedModlist.cs index 5b2fcb42a..9aac4ceed 100644 --- a/Wabbajack.App.Wpf/Messages/LoadLastLoadedModlist.cs +++ b/Wabbajack.App.Wpf/Messages/LoadLastLoadedModlist.cs @@ -1,4 +1,3 @@ - using ReactiveUI; namespace Wabbajack.Messages; diff --git a/Wabbajack.App.Wpf/Messages/LoadModlistForDetails.cs b/Wabbajack.App.Wpf/Messages/LoadModlistForDetails.cs new file mode 100644 index 000000000..7b20340ee --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/LoadModlistForDetails.cs @@ -0,0 +1,19 @@ +using ReactiveUI; +using Wabbajack.DTOs; + +namespace Wabbajack.Messages; + +public class LoadModlistForDetails +{ + public BaseModListMetadataVM MetadataVM { get; } + + public LoadModlistForDetails(BaseModListMetadataVM metadata) + { + MetadataVM = metadata; + } + + public static void Send(BaseModListMetadataVM metadataVM) + { + MessageBus.Current.SendMessage(new LoadModlistForDetails(metadataVM)); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/NavigateTo.cs b/Wabbajack.App.Wpf/Messages/NavigateTo.cs index f9eea96f9..cd0e58905 100644 --- a/Wabbajack.App.Wpf/Messages/NavigateTo.cs +++ b/Wabbajack.App.Wpf/Messages/NavigateTo.cs @@ -1,5 +1,4 @@ using ReactiveUI; -using Wabbajack; namespace Wabbajack.Messages; diff --git a/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs b/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs index ca0bafe6f..636b71464 100644 --- a/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs +++ b/Wabbajack.App.Wpf/Messages/NavigateToGlobal.cs @@ -2,18 +2,21 @@ namespace Wabbajack.Messages; +public enum ScreenType +{ + Home, + ModListGallery, + Installer, + Settings, + CompilerHome, + CompilerMain, + ModListDetails, + WebBrowser, + Info +} + public class NavigateToGlobal { - public enum ScreenType - { - ModeSelectionView, - ModListGallery, - Installer, - Settings, - Compiler, - ModListContents, - WebBrowser - } public ScreenType Screen { get; } diff --git a/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs b/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs new file mode 100644 index 000000000..a49212321 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs @@ -0,0 +1,26 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; + +public enum FloatingScreenType +{ + None, + ModListDetails, +} + +public class ShowFloatingWindow +{ + + public FloatingScreenType Screen { get; } + + private ShowFloatingWindow(FloatingScreenType screen) + { + Screen = screen; + } + + public static void Send(FloatingScreenType screen) + { + MessageBus.Current.SendMessage(new ShowFloatingWindow(screen)); + } + +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/ShowNavigation.cs b/Wabbajack.App.Wpf/Messages/ShowNavigation.cs new file mode 100644 index 000000000..df1148b4a --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ShowNavigation.cs @@ -0,0 +1,16 @@ +using ReactiveUI; +using Wabbajack.Compiler; + +namespace Wabbajack.Messages; + +public class ShowNavigation +{ + public ShowNavigation() + { + } + + public static void Send() + { + MessageBus.Current.SendMessage(new ShowNavigation()); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Models/LogStream.cs b/Wabbajack.App.Wpf/Models/LogStream.cs index 5a997c017..44f05964a 100644 --- a/Wabbajack.App.Wpf/Models/LogStream.cs +++ b/Wabbajack.App.Wpf/Models/LogStream.cs @@ -1,18 +1,13 @@ using System; using System.Collections.ObjectModel; +using System.Globalization; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; -using System.Text; -using System.Windows.Data; using DynamicData; -using DynamicData.Binding; -using Microsoft.Extensions.Logging; using NLog; using NLog.Targets; using ReactiveUI; -using Wabbajack.Extensions; -using LogLevel = NLog.LogLevel; namespace Wabbajack.Models; @@ -66,8 +61,9 @@ public interface ILogMessage long MessageId { get; } string ShortMessage { get; } - DateTime TimeStamp { get; } string LongMessage { get; } + DateTime TimeStamp { get; } + LogLevel Level { get; } } private record LogMessage(LogEventInfo info) : ILogMessage @@ -75,7 +71,8 @@ private record LogMessage(LogEventInfo info) : ILogMessage public long MessageId => info.SequenceID; public string ShortMessage => info.FormattedMessage; public DateTime TimeStamp => info.TimeStamp; - public string LongMessage => info.FormattedMessage; + public LogLevel Level => info.Level; + public string LongMessage => $"[{TimeStamp.ToString("HH:mm:ss")} {info.Level.ToString().ToUpper()}] {info.FormattedMessage}"; } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Models/ResourceMonitor.cs b/Wabbajack.App.Wpf/Models/ResourceMonitor.cs index 8b7bf8831..5296661b9 100644 --- a/Wabbajack.App.Wpf/Models/ResourceMonitor.cs +++ b/Wabbajack.App.Wpf/Models/ResourceMonitor.cs @@ -14,7 +14,7 @@ namespace Wabbajack.Models; public class ResourceMonitor : IDisposable { - private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(250); + private readonly TimeSpan _pollInterval = TimeSpan.FromMilliseconds(1000); private readonly IResource[] _resources; diff --git a/Wabbajack.App.Wpf/Resources/Fonts/Gabarito-VariableFont_wght-BF651cdf1f55e6c.ttf b/Wabbajack.App.Wpf/Resources/Fonts/Gabarito-VariableFont_wght-BF651cdf1f55e6c.ttf new file mode 100644 index 000000000..81d33a6b6 Binary files /dev/null and b/Wabbajack.App.Wpf/Resources/Fonts/Gabarito-VariableFont_wght-BF651cdf1f55e6c.ttf differ diff --git a/Wabbajack.App.Wpf/Resources/libwebp_x64.dll b/Wabbajack.App.Wpf/Resources/libwebp_x64.dll new file mode 100644 index 000000000..0b2bd2c13 Binary files /dev/null and b/Wabbajack.App.Wpf/Resources/libwebp_x64.dll differ diff --git a/Wabbajack.App.Wpf/Resources/libwebp_x86.dll b/Wabbajack.App.Wpf/Resources/libwebp_x86.dll new file mode 100644 index 000000000..62094675e Binary files /dev/null and b/Wabbajack.App.Wpf/Resources/libwebp_x86.dll differ diff --git a/Wabbajack.App.Wpf/Settings.cs b/Wabbajack.App.Wpf/Settings.cs index 4ad1517c3..1094667e1 100644 --- a/Wabbajack.App.Wpf/Settings.cs +++ b/Wabbajack.App.Wpf/Settings.cs @@ -4,51 +4,58 @@ using Wabbajack.RateLimiter; using Wabbajack.Util; -namespace Wabbajack +namespace Wabbajack; + +[JsonName("Mo2ModListInstallerSettings")] +public class Mo2ModlistInstallationSettings { - [JsonName("Mo2ModListInstallerSettings")] - public class Mo2ModlistInstallationSettings - { - public AbsolutePath InstallationLocation { get; set; } - public AbsolutePath DownloadLocation { get; set; } - public bool AutomaticallyOverrideExistingInstall { get; set; } - } + public AbsolutePath InstallationLocation { get; set; } + public AbsolutePath DownloadLocation { get; set; } + public bool AutomaticallyOverrideExistingInstall { get; set; } +} - public class PerformanceSettings : ViewModel - { - private readonly Configuration.MainSettings _settings; - private readonly int _defaultMaximumMemoryPerDownloadThreadMb; +public class PerformanceSettings : ViewModel +{ + private readonly Configuration.MainSettings _settings; + private readonly int _defaultMaximumMemoryPerDownloadThreadMb; - public PerformanceSettings(Configuration.MainSettings settings, IResource downloadResources, SystemParametersConstructor systemParams) - { - var p = systemParams.Create(); + public PerformanceSettings(Configuration.MainSettings settings, IResource downloadResources, SystemParametersConstructor systemParams) + { + var p = systemParams.Create(); - _settings = settings; - // Split half of available memory among download threads - _defaultMaximumMemoryPerDownloadThreadMb = (int)(p.SystemMemorySize / downloadResources.MaxTasks / 1024 / 1024) / 2; - _maximumMemoryPerDownloadThreadMb = settings.PerformanceSettings.MaximumMemoryPerDownloadThreadMb; + _settings = settings; + // Split half of available memory among download threads + _defaultMaximumMemoryPerDownloadThreadMb = (int)(p.SystemMemorySize / downloadResources.MaxTasks / 1024 / 1024) / 2; + _maximumMemoryPerDownloadThreadMb = settings.PerformanceSettings.MaximumMemoryPerDownloadThreadMb; - if (MaximumMemoryPerDownloadThreadMb < 0) - { - ResetMaximumMemoryPerDownloadThreadMb(); - } + if (MaximumMemoryPerDownloadThreadMb < 0) + { + ResetMaximumMemoryPerDownloadThreadMb(); } + } - private int _maximumMemoryPerDownloadThreadMb; + private int _maximumMemoryPerDownloadThreadMb; - public int MaximumMemoryPerDownloadThreadMb + public int MaximumMemoryPerDownloadThreadMb + { + get => _maximumMemoryPerDownloadThreadMb; + set { - get => _maximumMemoryPerDownloadThreadMb; - set - { - RaiseAndSetIfChanged(ref _maximumMemoryPerDownloadThreadMb, value); - _settings.PerformanceSettings.MaximumMemoryPerDownloadThreadMb = value; - } + RaiseAndSetIfChanged(ref _maximumMemoryPerDownloadThreadMb, value); + _settings.PerformanceSettings.MaximumMemoryPerDownloadThreadMb = value; } + } - public void ResetMaximumMemoryPerDownloadThreadMb() - { - MaximumMemoryPerDownloadThreadMb = _defaultMaximumMemoryPerDownloadThreadMb; - } + public void ResetMaximumMemoryPerDownloadThreadMb() + { + MaximumMemoryPerDownloadThreadMb = _defaultMaximumMemoryPerDownloadThreadMb; } } +public class GalleryFilterSettings +{ + public string GameType { get; set; } + public bool IncludeNSFW { get; set; } + public bool IncludeUnofficial { get; set; } + public bool OnlyInstalled { get; set; } + public string Search { get; set; } +} diff --git a/Wabbajack.App.Wpf/StatusMessages/CriticalFailureIntervention.cs b/Wabbajack.App.Wpf/StatusMessages/CriticalFailureIntervention.cs index 618776efa..97f4254f6 100644 --- a/Wabbajack.App.Wpf/StatusMessages/CriticalFailureIntervention.cs +++ b/Wabbajack.App.Wpf/StatusMessages/CriticalFailureIntervention.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Wabbajack.Common; using Wabbajack.Interventions; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/StatusMessages/YesNoIntervention.cs b/Wabbajack.App.Wpf/StatusMessages/YesNoIntervention.cs index a8e59eb6b..ba523ff2d 100644 --- a/Wabbajack.App.Wpf/StatusMessages/YesNoIntervention.cs +++ b/Wabbajack.App.Wpf/StatusMessages/YesNoIntervention.cs @@ -1,15 +1,12 @@ -using Wabbajack.Common; +namespace Wabbajack; -namespace Wabbajack +public class YesNoIntervention : ConfirmationIntervention { - public class YesNoIntervention : ConfirmationIntervention + public YesNoIntervention(string description, string title) { - public YesNoIntervention(string description, string title) - { - ExtendedDescription = description; - ShortDescription = title; - } - public override string ShortDescription { get; } - public override string ExtendedDescription { get; } + ExtendedDescription = description; + ShortDescription = title; } + public override string ShortDescription { get; } + public override string ExtendedDescription { get; } } diff --git a/Wabbajack.App.Wpf/Themes/Styles.xaml b/Wabbajack.App.Wpf/Themes/Styles.xaml index 88495b482..fbec718fd 100644 --- a/Wabbajack.App.Wpf/Themes/Styles.xaml +++ b/Wabbajack.App.Wpf/Themes/Styles.xaml @@ -8,8 +8,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:options="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options" xmlns:sys="clr-namespace:System;assembly=mscorlib" + xmlns:wj="clr-namespace:Wabbajack" + xmlns:ic="clr-namespace:FluentIcons.WPF;assembly=FluentIcons.WPF" + xmlns:generic="http://schemas.sdl.com/xaml" + xmlns:math="http://hexinnovation.com/math" xmlns:controls="http://schemas.sdl.com/xaml" mc:Ignorable="d"> + pack://application:,,,/Resources/Fonts/#Gabarito + @@ -19,44 +25,63 @@ - + + + + - #121212 - #222222 - #272727 - #424242 - #323232 + #222531 + #2A2B41 + #3c3652 + #4e4571 + #4e4571 + #222531 #424242 - #323232 - #666666 - #362675 + #4e4571 + #514c6b - #EFEFEF - #CCCCCC + #E5E5E8 + #40FFFFFF - #BDBDBD + #3b3c50 + + #D9BBF9 #525252 #ffc400 - #e83a40 - #52b545 + #5e2c2b + #5fad56 #967400 - #BB86FC - #00BB86FC - #3700B3 + #D8BAF8 + + + #303141 + + #383750 + #3f3c57 + #46425F + #81739d + #2d2e45 + #5f6071 + + #313146 + + + #8866ad + #514c6b #270080 #1b0059 - #03DAC6 - #0e8f83 + #3C3652 + #363952 #095952 #042421 #cef0ed #8cede5 #00ffe7 - #C7FC86 - #8eb55e - #4b6130 + #4e4571 + #3C3652 + #2A2B41 #abf74d #868CFC #F686FC @@ -64,15 +89,15 @@ #FCBB86 - #FF3700B3 + #FF222531 - #CC868CFC + #CCD8BAF8 - #99868CFC + #99D8BAF8 - #66868CFC + #66D8BAF8 - #33868CFC + #33D8BAF8 + Color="{StaticResource Primary}" /> + + + 16 + 12 - - + @@ -114,10 +145,19 @@ + + + + + + + + + @@ -137,6 +177,9 @@ + + + @@ -146,42 +189,56 @@ - + - - + + - - + + - + - + - - + + - - + + + - - + + + + + + + + + + + + + + - - - + + + - + + - + @@ -191,13 +248,13 @@ - - + + - + - - + + @@ -209,16 +266,232 @@ - - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + M-0.7,5.2 L-2.2,6.7 3.6,12.6 9.5,6.7 8,5.2 3.6,9.6 z M-2.2,10.9 L-0.7,12.4 3.7,8 8,12.4 9.5,10.9 3.7,5 z M1.0E-41,4.2 L0,2.1 2.5,4.5 6.7,4.4E-47 6.7,2.3 2.5,6.7 z @@ -231,24 +504,24 @@ M-0,6 L-0,8 8,8 8,-0 6,-0 6,6 z M5,-0 L9,5 1,5 z - @@ -258,7 +531,7 @@ + + --> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + x:Name="Border" + Background="{TemplateBinding Background}" + BorderBrush="{TemplateBinding BorderBrush}" + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="8"> + + + + + + + + + + + + + + + + + + + + + + + - + + + + + @@ -1299,7 +1695,7 @@ - + + + + - - + + @@ -1333,33 +1738,82 @@ + + + - - + + - + + + + + + + - - @@ -1889,14 +2346,14 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - CornerRadius="6"> + CornerRadius="4"> + CornerRadius="4" /> + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs index 9373e42a1..f02d1dcdc 100644 --- a/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs @@ -1,9 +1,6 @@ using System.Net.Http; -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Wabbajack.DTOs.Logins; -using Wabbajack.Models; -using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; diff --git a/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs index 7bf069bf6..95af22e63 100644 --- a/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs @@ -1,26 +1,18 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Net.Http; -using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Web; -using Fizzler.Systems.HtmlAgilityPack; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Wabbajack.DTOs.Logins; using Wabbajack.DTOs.OAuth; -using Wabbajack.Messages; -using Wabbajack.Models; -using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Services.OSIntegrated; -using Cookie = Wabbajack.DTOs.Logins.Cookie; namespace Wabbajack.UserIntervention; diff --git a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs index a54ac5449..cf04482d2 100644 --- a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs @@ -6,15 +6,9 @@ using System.Threading; using System.Threading.Tasks; using System.Web; -using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; -using ReactiveUI; using Wabbajack.Common; -using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Logins; -using Wabbajack.Messages; -using Wabbajack.Models; -using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; diff --git a/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs index b41e736cf..fa42e877b 100644 --- a/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs @@ -1,8 +1,6 @@ using System.Net.Http; using Microsoft.Extensions.Logging; using Wabbajack.DTOs.Logins; -using Wabbajack.Models; -using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; diff --git a/Wabbajack.App.Wpf/Util/AsyncLazy.cs b/Wabbajack.App.Wpf/Util/AsyncLazy.cs index 69488c282..3a0a206a4 100644 --- a/Wabbajack.App.Wpf/Util/AsyncLazy.cs +++ b/Wabbajack.App.Wpf/Util/AsyncLazy.cs @@ -1,7 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; namespace Wabbajack diff --git a/Wabbajack.App.Wpf/Util/DriveHelper.cs b/Wabbajack.App.Wpf/Util/DriveHelper.cs new file mode 100644 index 000000000..aff2add7e --- /dev/null +++ b/Wabbajack.App.Wpf/Util/DriveHelper.cs @@ -0,0 +1,402 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management; + +namespace Wabbajack; +public static class DriveHelper +{ + private static Dictionary _diskCache = new Dictionary(); + private static Dictionary _partCache = new Dictionary(); + + /// + /// All the physical disks by disk number + /// + public static Dictionary PhysicalDisks + { + get + { + if (_diskCache.Count == 0) + _diskCache = GetPhysicalDisks(); + return _diskCache; + } + } + + /// + /// All the physical disks by partition (drive letter) + /// + public static Dictionary Partitions + { + get + { + if (_partCache.Count == 0) + _partCache = GetPartitions(); + return _partCache; + } + } + + public static void ReloadPhysicalDisks() + { + if (_diskCache.Count > 0) + _diskCache.Clear(); + _diskCache = GetPhysicalDisks(); + } + + public static MediaType GetMediaTypeForPath(string path) + { + var root = Path.GetPathRoot(path); + if (string.IsNullOrEmpty(root)) return MediaType.Unspecified; + return Partitions[root[0]].MediaType; + } + + public static DriveInfo? GetPreferredInstallationDrive(long modlistSize) + { + return DriveInfo.GetDrives() + .Where(d => d.IsReady && d.DriveType == DriveType.Fixed) + .OrderByDescending(d => d.AvailableFreeSpace > modlistSize) + .ThenByDescending(d => Partitions[d.RootDirectory.Name[0]].MediaType == MediaType.SSD) + .ThenByDescending(d => d.AvailableFreeSpace) + .FirstOrDefault(); + } + + [DebuggerHidden] + private static Dictionary GetPhysicalDisks() + { + try + { + var disks = new Dictionary(); + var scope = new ManagementScope(@"\\localhost\ROOT\Microsoft\Windows\Storage"); + var query = new ObjectQuery("SELECT * FROM MSFT_PhysicalDisk"); + using var searcher = new ManagementObjectSearcher(scope, query); + var dObj = searcher.Get(); + foreach (ManagementObject diskobj in dObj) + { + var dis = new PhysicalDisk(); + try + { + dis.SupportedUsages = (ushort[])diskobj["SupportedUsages"]; + } + catch (Exception) + { + dis.SupportedUsages = null; + } + try + { + dis.CannotPoolReason = (ushort[])diskobj["CannotPoolReason"]; + } + catch (Exception) + { + dis.CannotPoolReason = null; + } + try + { + dis.OperationalStatus = (ushort[])diskobj["OperationalStatus"]; + } + catch (Exception) + { + dis.OperationalStatus = null; + } + try + { + dis.OperationalDetails = (string[])diskobj["OperationalDetails"]; + } + catch (Exception) + { + dis.OperationalDetails = null; + } + try + { + dis.UniqueIdFormat = (ushort)diskobj["UniqueIdFormat"]; + } + catch (Exception) + { + dis.UniqueIdFormat = 0; + } + try + { + dis.DeviceId = diskobj["DeviceId"].ToString(); + } + catch (Exception) + { + dis.DeviceId = "NA"; + } + try + { + dis.FriendlyName = (string)diskobj["FriendlyName"]; + } + catch (Exception) + { + dis.FriendlyName = "?"; + } + try + { + dis.HealthStatus = (ushort)diskobj["HealthStatus"]; + } + catch (Exception) + { + dis.HealthStatus = 0; + } + try + { + dis.PhysicalLocation = (string)diskobj["PhysicalLocation"]; + } + catch (Exception) + { + dis.PhysicalLocation = "?"; + } + try + { + dis.VirtualDiskFootprint = (ushort)diskobj["VirtualDiskFootprint"]; + } + catch (Exception) + { + dis.VirtualDiskFootprint = 0; + } + try + { + dis.Usage = (ushort)diskobj["Usage"]; + } + catch (Exception) + { + dis.Usage = 0; + } + try + { + dis.Description = (string)diskobj["Description"]; + } + catch (Exception) + { + dis.Description = "?"; + } + try + { + dis.PartNumber = (string)diskobj["PartNumber"]; + } + catch (Exception) + { + dis.PartNumber = "?"; + } + try + { + dis.FirmwareVersion = (string)diskobj["FirmwareVersion"]; + } + catch (Exception) + { + dis.FirmwareVersion = "?"; + } + try + { + dis.SoftwareVersion = (string)diskobj["SoftwareVersion"]; + } + catch (Exception) + { + dis.SoftwareVersion = "?"; + } + try + { + dis.Size = (ulong)diskobj["SoftwareVersion"]; + } + catch (Exception) + { + dis.Size = 0; + } + try + { + dis.AllocatedSize = (ulong)diskobj["AllocatedSize"]; + } + catch (Exception) + { + dis.AllocatedSize = 0; + } + try + { + dis.BusType = (ushort)diskobj["BusType"]; + } + catch (Exception) + { + dis.BusType = 0; + } + try + { + dis.IsWriteCacheEnabled = (bool)diskobj["IsWriteCacheEnabled"]; + } + catch (Exception) + { + dis.IsWriteCacheEnabled = false; + } + try + { + dis.IsPowerProtected = (bool)diskobj["IsPowerProtected"]; + } + catch (Exception) + { + dis.IsPowerProtected = false; + } + try + { + dis.PhysicalSectorSize = (ulong)diskobj["PhysicalSectorSize"]; + } + catch (Exception) + { + dis.PhysicalSectorSize = 0; + } + try + { + dis.LogicalSectorSize = (ulong)diskobj["LogicalSectorSize"]; + } + catch (Exception) + { + dis.LogicalSectorSize = 0; + } + try + { + dis.SpindleSpeed = (uint)diskobj["SpindleSpeed"]; + } + catch (Exception) + { + dis.SpindleSpeed = 0; + } + try + { + dis.IsIndicationEnabled = (bool)diskobj["IsIndicationEnabled"]; + } + catch (Exception) + { + dis.IsIndicationEnabled = false; + } + try + { + dis.EnclosureNumber = (ushort)diskobj["EnclosureNumber"]; + } + catch (Exception) + { + dis.EnclosureNumber = 0; + } + try + { + dis.SlotNumber = (ushort)diskobj["SlotNumber"]; + } + catch (Exception) + { + dis.SlotNumber = 0; + } + try + { + dis.CanPool = (bool)diskobj["CanPool"]; + } + catch (Exception) + { + dis.CanPool = false; + } + try + { + dis.OtherCannotPoolReasonDescription = (string)diskobj["OtherCannotPoolReasonDescription"]; + } + catch (Exception) + { + dis.OtherCannotPoolReasonDescription = "?"; + } + try + { + dis.IsPartial = (bool)diskobj["IsPartial"]; + } + catch (Exception) + { + dis.IsPartial = false; + } + try + { + dis.MediaType = (MediaType)diskobj["MediaType"]; + } + catch (Exception) + { + dis.MediaType = 0; + } + disks.Add(dis.DeviceId, dis); + } + return disks; + } + catch(Exception ex) + { + return new Dictionary(); + } + } + + [DebuggerHidden] + private static Dictionary GetPartitions() + { + var partitions = new Dictionary(); + try + { + var scope = new ManagementScope(@"\\.\root\Microsoft\Windows\Storage"); + scope.Connect(); + + using var partitionSearcher = new ManagementObjectSearcher($"SELECT DiskNumber, DriveLetter FROM MSFT_Partition"); + partitionSearcher.Scope = scope; + + var queryResult = partitionSearcher.Get(); + if (queryResult.Count <= 0) return new Dictionary(); + + foreach (var partition in queryResult) + { + var diskNumber = partition["DiskNumber"].ToString(); + var driveLetter = partition["DriveLetter"].ToString()[0]; + + partitions[driveLetter] = PhysicalDisks[diskNumber]; + } + + return partitions; + } + catch(Exception) + { + return partitions; + } + } +} + +/// +/// Documentation: https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/msft-physicaldisk +/// +public class PhysicalDisk +{ + public ulong AllocatedSize; + public ushort BusType; + public ushort[] CannotPoolReason; + public bool CanPool; + public string Description; + public string DeviceId; + public ushort EnclosureNumber; + public string FirmwareVersion; + public string FriendlyName; + public ushort HealthStatus; + public bool IsIndicationEnabled; + public bool IsPartial; + public bool IsPowerProtected; + public bool IsWriteCacheEnabled; + public ulong LogicalSectorSize; + public MediaType MediaType; + public string[] OperationalDetails; + public ushort[] OperationalStatus; + public string OtherCannotPoolReasonDescription; + public string PartNumber; + public string PhysicalLocation; + public ulong PhysicalSectorSize; + public ulong Size; + public ushort SlotNumber; + public string SoftwareVersion; + public uint SpindleSpeed; + public ushort[] SupportedUsages; + public ushort UniqueIdFormat; + public ushort Usage; + public ushort VirtualDiskFootprint; +} + +public enum MediaType : ushort +{ + Unspecified = 0, + HDD = 3, + SSD = 4, + SCM = 5 +} diff --git a/Wabbajack.App.Wpf/Util/FilePickerVM.cs b/Wabbajack.App.Wpf/Util/FilePickerVM.cs index 6197e5eb2..43c0c059a 100644 --- a/Wabbajack.App.Wpf/Util/FilePickerVM.cs +++ b/Wabbajack.App.Wpf/Util/FilePickerVM.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Input; -using Wabbajack; using Wabbajack.Extensions; using Wabbajack.Paths; using Wabbajack.Paths.IO; diff --git a/Wabbajack.App.Wpf/Util/ImageCacheManager.cs b/Wabbajack.App.Wpf/Util/ImageCacheManager.cs new file mode 100644 index 000000000..85a2af5d6 --- /dev/null +++ b/Wabbajack.App.Wpf/Util/ImageCacheManager.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using DynamicData.Kernel; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using static System.Text.Encoding; +using Convert = System.Convert; + +namespace Wabbajack; + +public class ImageCacheManager +{ + private readonly TimeSpan _pollInterval = TimeSpan.FromMinutes(1); + private readonly Services.OSIntegrated.Configuration _configuration; + private readonly ILogger _logger; + + private AbsolutePath _imageCachePath; + private ConcurrentDictionary _cachedImages { get; } = new(); + + private async Task SaveImage(Hash hash, MemoryStream ms) + { + var path = _imageCachePath.Combine(hash.ToHex()); + await using var fs = new FileStream(path.ToString(), FileMode.Create, FileAccess.Write); + ms.WriteTo(fs); + } + private async Task<(bool, MemoryStream)> LoadImage(Hash hash) + { + MemoryStream imageStream = null; + var path = _imageCachePath.Combine(hash.ToHex()); + if (!path.FileExists()) + { + return (false, imageStream); + } + + imageStream = new MemoryStream(); + await using var fs = new FileStream(path.ToString(), FileMode.Open, FileAccess.Read); + await fs.CopyToAsync(imageStream); + return (true, imageStream); + } + + public ImageCacheManager(ILogger logger, Services.OSIntegrated.Configuration configuration) + { + _logger = logger; + _configuration = configuration; + _imageCachePath = _configuration.ImageCacheLocation; + _imageCachePath.CreateDirectory(); + + RxApp.TaskpoolScheduler.ScheduleRecurringAction(_pollInterval, () => + { + foreach (var (hash, cachedImage) in _cachedImages) + { + if (!cachedImage.IsExpired()) continue; + + try + { + _cachedImages.TryRemove(hash, out _); + File.Delete(_configuration.ImageCacheLocation.Combine(hash).ToString()); + } + catch (Exception ex) + { + _logger.LogError("Failed to delete cached image {b64}", hash); + } + } + }); + + } + + public async Task Add(string url, BitmapImage img) + { + var hash = await UTF8.GetBytes(url).Hash(); + if (!_cachedImages.TryAdd(hash, new CachedImage(img))) return false; + + await SaveImage(hash, (MemoryStream)img.StreamSource); + return true; + + } + + public async Task<(bool, BitmapImage)> Get(string url) + { + var hash = await UTF8.GetBytes(url).Hash(); + // Try to load the image from memory + if (_cachedImages.TryGetValue(hash, out var cachedImage)) return (true, cachedImage.Image); + + // Try to load the image from disk + var (success, imageStream) = await LoadImage(hash); + if (!success) return (false, null); + + var img = UIUtils.BitmapImageFromStream(imageStream); + _cachedImages.TryAdd(hash, new CachedImage(img)); + await imageStream.DisposeAsync(); + return (true, img); + + } +} + +public class CachedImage(BitmapImage image) +{ + private readonly DateTime _cachedAt = DateTime.Now; + private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(5); + + public BitmapImage Image { get; } = image; + + public bool IsExpired() => _cachedAt - DateTime.Now > _cacheDuration; +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs b/Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs index db5153b25..07baa0485 100644 --- a/Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs +++ b/Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs @@ -1,16 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; using Microsoft.Extensions.Logging; using PInvoke; using Silk.NET.Core.Native; using Silk.NET.DXGI; -using Wabbajack.Common; using Wabbajack.Installer; -using Wabbajack; using static PInvoke.User32; using UnmanagedType = System.Runtime.InteropServices.UnmanagedType; diff --git a/Wabbajack.App.Wpf/Util/UIUtils.cs b/Wabbajack.App.Wpf/Util/UIUtils.cs index b4fc10ac8..ee44970cc 100644 --- a/Wabbajack.App.Wpf/Util/UIUtils.cs +++ b/Wabbajack.App.Wpf/Util/UIUtils.cs @@ -1,60 +1,61 @@ -using DynamicData; -using DynamicData.Binding; -using Microsoft.WindowsAPICodePack.Dialogs; -using ReactiveUI; +using ReactiveUI; using System; using System.Diagnostics; +using System.Drawing.Imaging; using System.IO; using System.Net.Http; using System.Reactive.Linq; -using System.Reflection; using System.Text; -using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Media.Imaging; -using Wabbajack.Common; using Wabbajack.Hashing.xxHash64; using Wabbajack.Extensions; using Wabbajack.Models; using Wabbajack.Paths; using Wabbajack.Paths.IO; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using Wabbajack.DTOs; +using Exception = System.Exception; +using SharpImage = SixLabors.ImageSharp.Image; -namespace Wabbajack +namespace Wabbajack; + +public static class UIUtils { - public static class UIUtils - { - public static BitmapImage BitmapImageFromResource(string name) => BitmapImageFromStream(System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/" + name)).Stream); + public static BitmapImage BitmapImageFromResource(string name) => BitmapImageFromStream(System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Wabbajack;component/" + name)).Stream); - public static BitmapImage BitmapImageFromStream(Stream stream) - { - var img = new BitmapImage(); - img.BeginInit(); - img.CacheOption = BitmapCacheOption.OnLoad; - img.StreamSource = stream; - img.EndInit(); - img.Freeze(); - return img; - } + public static BitmapImage BitmapImageFromStream(Stream stream) + { + var img = new BitmapImage(); + img.BeginInit(); + img.CacheOption = BitmapCacheOption.OnLoad; + img.StreamSource = stream; + img.EndInit(); + img.Freeze(); + return img; + } - public static bool TryGetBitmapImageFromFile(AbsolutePath path, out BitmapImage bitmapImage) + public static bool TryGetBitmapImageFromFile(AbsolutePath path, out BitmapImage bitmapImage) + { + try { - try - { - if (!path.FileExists()) - { - bitmapImage = default; - return false; - } - bitmapImage = new BitmapImage(new Uri(path.ToString(), UriKind.RelativeOrAbsolute)); - return true; - } - catch (Exception) + if (!path.FileExists()) { bitmapImage = default; return false; } + bitmapImage = new BitmapImage(new Uri(path.ToString(), UriKind.RelativeOrAbsolute)); + return true; } + catch (Exception) + { + bitmapImage = default; + return false; + } + } + public static void OpenWebsite(Uri url) { @@ -63,7 +64,7 @@ public static void OpenWebsite(Uri url) CreateNoWindow = true, }); } - + public static void OpenFolder(AbsolutePath path) { string folderPath = path.ToString(); @@ -80,111 +81,80 @@ public static void OpenFolder(AbsolutePath path) }); } - public static AbsolutePath OpenFileDialog(string filter, string initialDirectory = null) - { - OpenFileDialog ofd = new OpenFileDialog(); - ofd.Filter = filter; - ofd.InitialDirectory = initialDirectory; - if (ofd.ShowDialog() == DialogResult.OK) - return (AbsolutePath)ofd.FileName; - return default; - } - public static IObservable DownloadBitmapImage(this IObservable obs, Action exceptionHandler, - LoadingLock loadingLock) - { - return obs - .ObserveOn(RxApp.TaskpoolScheduler) - .SelectTask(async url => + public static AbsolutePath OpenFileDialog(string filter, string initialDirectory = null) + { + OpenFileDialog ofd = new OpenFileDialog(); + ofd.Filter = filter; + ofd.InitialDirectory = initialDirectory; + if (ofd.ShowDialog() == DialogResult.OK) + return (AbsolutePath)ofd.FileName; + return default; + } + + public static IObservable DownloadBitmapImage(this IObservable obs, Action exceptionHandler, + LoadingLock loadingLock, HttpClient client, ImageCacheManager icm) + { + return obs + .ObserveOn(RxApp.TaskpoolScheduler) + .SelectTask(async url => + { + using var ll = loadingLock.WithLoading(); + try { - var ll = loadingLock.WithLoading(); - try - { - var (found, mstream) = await FindCachedImage(url); - if (found) return (ll, mstream); - - var ret = new MemoryStream(); - using (var client = new HttpClient()) - await using (var stream = await client.GetStreamAsync(url)) - { - await stream.CopyToAsync(ret); - } + var (cached, cachedImg) = await icm.Get(url); + if (cached) return cachedImg; - ret.Seek(0, SeekOrigin.Begin); + await using var stream = await client.GetStreamAsync(url); - await WriteCachedImage(url, ret.ToArray()); - return (ll, ret); - } - catch (Exception ex) + using var pngStream = new MemoryStream(); + using (var sharpImg = await SharpImage.LoadAsync(stream)) { - exceptionHandler(ex); - return (ll, default); + await sharpImg.SaveAsPngAsync(pngStream); } - }) - .Select(x => + + var img = BitmapImageFromStream(pngStream); + await icm.Add(url, img); + return img; + } + catch (Exception ex) { - var (ll, memStream) = x; - if (memStream == null) return default; - try - { - return BitmapImageFromStream(memStream); - } - catch (Exception ex) - { - exceptionHandler(ex); - return default; - } - finally - { - ll.Dispose(); - memStream.Dispose(); - } - }) - .ObserveOnGuiThread(); - } + exceptionHandler(ex); + return default; + } + }) + .ObserveOnGuiThread(); + } - private static async Task WriteCachedImage(string url, byte[] data) + /// + /// Format bytes to a greater unit + /// + /// number of bytes + /// + public static string FormatBytes(long bytes) + { + string[] Suffix = { "B", "KB", "MB", "GB", "TB" }; + int i; + double dblSByte = bytes; + for (i = 0; i < Suffix.Length && bytes >= 1024; i++, bytes /= 1024) { - var folder = KnownFolders.WabbajackAppLocal.Combine("ModListImages"); - if (!folder.DirectoryExists()) folder.CreateDirectory(); - - var path = folder.Combine((await Encoding.UTF8.GetBytes(url).Hash()).ToHex()); - await path.WriteAllBytesAsync(data); + dblSByte = bytes / 1024.0; } - private static async Task<(bool Found, MemoryStream data)> FindCachedImage(string uri) - { - var folder = KnownFolders.WabbajackAppLocal.Combine("ModListImages"); - if (!folder.DirectoryExists()) folder.CreateDirectory(); - - var path = folder.Combine((await Encoding.UTF8.GetBytes(uri).Hash()).ToHex()); - return path.FileExists() ? (true, new MemoryStream(await path.ReadAllBytesAsync())) : (false, default); - } + return String.Format("{0:0.##} {1}", dblSByte, Suffix[i]); + } - /// - /// Format bytes to a greater unit - /// - /// number of bytes - /// - public static string FormatBytes(long bytes) + public static void OpenFile(AbsolutePath file) + { + Process.Start(new ProcessStartInfo("cmd.exe", $"/c start \"\" \"{file}\"") { - string[] Suffix = { "B", "KB", "MB", "GB", "TB" }; - int i; - double dblSByte = bytes; - for (i = 0; i < Suffix.Length && bytes >= 1024; i++, bytes /= 1024) - { - dblSByte = bytes / 1024.0; - } - - return String.Format("{0:0.##} {1}", dblSByte, Suffix[i]); - } + CreateNoWindow = true, + }); + } - public static void OpenFile(AbsolutePath file) - { - Process.Start(new ProcessStartInfo("cmd.exe", $"/c start \"\" \"{file}\"") - { - CreateNoWindow = true, - }); - } + public static string GetSmallImageUri(ModlistMetadata metadata) + { + var fileName = metadata.Links.MachineURL + "_small.webp"; + return $"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/refs/heads/master/reports/{metadata.RepositoryName}/{fileName}"; } -} +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs b/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs deleted file mode 100644 index f60641049..000000000 --- a/Wabbajack.App.Wpf/View Models/BackNavigatingVM.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Common; -using Wabbajack; -using Wabbajack.Messages; - -namespace Wabbajack -{ - public interface IBackNavigatingVM : IReactiveObject - { - ViewModel NavigateBackTarget { get; set; } - ReactiveCommand BackCommand { get; } - - Subject IsBackEnabledSubject { get; } - IObservable IsBackEnabled { get; } - } - - public class BackNavigatingVM : ViewModel, IBackNavigatingVM - { - [Reactive] - public ViewModel NavigateBackTarget { get; set; } - public ReactiveCommand BackCommand { get; protected set; } - - [Reactive] - public bool IsActive { get; set; } - - public Subject IsBackEnabledSubject { get; } = new Subject(); - public IObservable IsBackEnabled { get; } - - public BackNavigatingVM(ILogger logger) - { - IsBackEnabled = IsBackEnabledSubject.StartWith(true); - BackCommand = ReactiveCommand.Create( - execute: () => logger.CatchAndLog(() => - { - NavigateBack.Send(); - Unload(); - }), - canExecute: this.ConstructCanNavigateBack() - .ObserveOnGuiThread()); - - this.WhenActivated(disposables => - { - IsActive = true; - Disposable.Create(() => IsActive = false).DisposeWith(disposables); - }); - } - - public virtual void Unload() - { - } - } - - public static class IBackNavigatingVMExt - { - public static IObservable ConstructCanNavigateBack(this IBackNavigatingVM vm) - { - return vm.WhenAny(x => x.NavigateBackTarget) - .CombineLatest(vm.IsBackEnabled) - .Select(x => x.First != null && x.Second); - } - - public static IObservable ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm) - { - return mwvm.WhenAny(x => x.ActivePane) - .Select(x => object.ReferenceEquals(vm, x)); - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/CPUDisplayVM.cs b/Wabbajack.App.Wpf/View Models/CPUDisplayVM.cs deleted file mode 100644 index 87371bc53..000000000 --- a/Wabbajack.App.Wpf/View Models/CPUDisplayVM.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using ReactiveUI.Fody.Helpers; -using Wabbajack; -using Wabbajack.RateLimiter; - -namespace Wabbajack -{ - public class CPUDisplayVM : ViewModel - { - [Reactive] - public ulong ID { get; set; } - [Reactive] - public DateTime StartTime { get; set; } - [Reactive] - public bool IsWorking { get; set; } - [Reactive] - public string Msg { get; set; } - [Reactive] - public Percent ProgressPercent { get; set; } - - public CPUDisplayVM() - { - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs deleted file mode 100644 index f514aea9f..000000000 --- a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs +++ /dev/null @@ -1,515 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Reactive; -using Microsoft.Extensions.Logging; -using Wabbajack.Messages; -using ReactiveUI; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Media; -using DynamicData; -using Microsoft.WindowsAPICodePack.Dialogs; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Common; -using Wabbajack.Compiler; -using Wabbajack.Downloaders; -using Wabbajack.DTOs; -using Wabbajack.DTOs.DownloadStates; -using Wabbajack.DTOs.JsonConverters; -using Wabbajack.Extensions; -using Wabbajack.Installer; -using Wabbajack.LoginManagers; -using Wabbajack.Models; -using Wabbajack.Networking.WabbajackClientApi; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; -using Wabbajack.RateLimiter; -using Wabbajack.Services.OSIntegrated; - -namespace Wabbajack -{ - public enum CompilerState - { - Configuration, - Compiling, - Completed, - Errored - } - - public class CompilerVM : BackNavigatingVM, ICpuStatusVM - { - private const string LastSavedCompilerSettings = "last-saved-compiler-settings"; - private readonly DTOSerializer _dtos; - private readonly SettingsManager _settingsManager; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly ResourceMonitor _resourceMonitor; - private readonly CompilerSettingsInferencer _inferencer; - private readonly IEnumerable _logins; - private readonly DownloadDispatcher _downloadDispatcher; - private readonly Client _wjClient; - private AsyncLock _waitForLoginLock = new (); - - [Reactive] public string StatusText { get; set; } - [Reactive] public Percent StatusProgress { get; set; } - - [Reactive] public CompilerState State { get; set; } - - [Reactive] public MO2CompilerVM SubCompilerVM { get; set; } - - // Paths - public FilePickerVM ModlistLocation { get; } - public FilePickerVM DownloadLocation { get; } - public FilePickerVM OutputLocation { get; } - - // Modlist Settings - - [Reactive] public string ModListName { get; set; } - [Reactive] public string Version { get; set; } - [Reactive] public string Author { get; set; } - [Reactive] public string Description { get; set; } - public FilePickerVM ModListImagePath { get; } = new(); - [Reactive] public ImageSource ModListImage { get; set; } - [Reactive] public string Website { get; set; } - [Reactive] public string Readme { get; set; } - [Reactive] public bool IsNSFW { get; set; } - [Reactive] public bool PublishUpdate { get; set; } - [Reactive] public string MachineUrl { get; set; } - [Reactive] public Game BaseGame { get; set; } - [Reactive] public string SelectedProfile { get; set; } - [Reactive] public AbsolutePath GamePath { get; set; } - [Reactive] public bool IsMO2Compilation { get; set; } - - [Reactive] public RelativePath[] AlwaysEnabled { get; set; } = Array.Empty(); - [Reactive] public RelativePath[] NoMatchInclude { get; set; } = Array.Empty(); - [Reactive] public RelativePath[] Include { get; set; } = Array.Empty(); - [Reactive] public RelativePath[] Ignore { get; set; } = Array.Empty(); - - [Reactive] public string[] OtherProfiles { get; set; } = Array.Empty(); - - [Reactive] public AbsolutePath Source { get; set; } - - public AbsolutePath SettingsOutputLocation => Source.Combine(ModListName).WithExtension(Ext.CompilerSettings); - - - public ReactiveCommand ExecuteCommand { get; } - public ReactiveCommand ReInferSettingsCommand { get; set; } - - public LogStream LoggerProvider { get; } - public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; - - [Reactive] public ErrorResponse ErrorState { get; private set; } - - public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, - IServiceProvider serviceProvider, LogStream loggerProvider, ResourceMonitor resourceMonitor, - CompilerSettingsInferencer inferencer, Client wjClient, IEnumerable logins, DownloadDispatcher downloadDispatcher) : base(logger) - { - _logger = logger; - _dtos = dtos; - _settingsManager = settingsManager; - _serviceProvider = serviceProvider; - LoggerProvider = loggerProvider; - _resourceMonitor = resourceMonitor; - _inferencer = inferencer; - _wjClient = wjClient; - _logins = logins; - _downloadDispatcher = downloadDispatcher; - - StatusText = "Compiler Settings"; - StatusProgress = Percent.Zero; - - BackCommand = - ReactiveCommand.CreateFromTask(async () => - { - await SaveSettingsFile(); - NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); - }); - - SubCompilerVM = new MO2CompilerVM(this); - - ExecuteCommand = ReactiveCommand.CreateFromTask(async () => await StartCompilation()); - ReInferSettingsCommand = ReactiveCommand.CreateFromTask(async () => await ReInferSettings(), - this.WhenAnyValue(vm => vm.Source) - .ObserveOnGuiThread() - .Select(v => v != default) - .CombineLatest(this.WhenAnyValue(vm => vm.ModListName) - .ObserveOnGuiThread() - .Select(p => !string.IsNullOrWhiteSpace(p))) - .Select(v => v.First && v.Second)); - - ModlistLocation = new FilePickerVM - { - ExistCheckOption = FilePickerVM.CheckOptions.On, - PathType = FilePickerVM.PathTypeOptions.File, - PromptTitle = "Select a config file or a modlist.txt file" - }; - - DownloadLocation = new FilePickerVM - { - ExistCheckOption = FilePickerVM.CheckOptions.On, - PathType = FilePickerVM.PathTypeOptions.Folder, - PromptTitle = "Location where the downloads for this list are stored" - }; - - OutputLocation = new FilePickerVM - { - ExistCheckOption = FilePickerVM.CheckOptions.Off, - PathType = FilePickerVM.PathTypeOptions.Folder, - PromptTitle = "Location where the compiled modlist will be stored" - }; - - ModlistLocation.Filters.AddRange(new[] - { - new CommonFileDialogFilter("MO2 Modlist", "*" + Ext.Txt), - new CommonFileDialogFilter("Compiler Settings File", "*" + Ext.CompilerSettings) - }); - - - this.WhenActivated(disposables => - { - State = CompilerState.Configuration; - Disposable.Empty.DisposeWith(disposables); - - ModlistLocation.WhenAnyValue(vm => vm.TargetPath) - .Subscribe(p => InferModListFromLocation(p).FireAndForget()) - .DisposeWith(disposables); - - - this.WhenAnyValue(x => x.DownloadLocation.TargetPath) - .CombineLatest(this.WhenAnyValue(x => x.ModlistLocation.TargetPath), - this.WhenAnyValue(x => x.OutputLocation.TargetPath), - this.WhenAnyValue(x => x.DownloadLocation.ErrorState), - this.WhenAnyValue(x => x.ModlistLocation.ErrorState), - this.WhenAnyValue(x => x.OutputLocation.ErrorState), - this.WhenAnyValue(x => x.ModListName), - this.WhenAnyValue(x => x.Version)) - .Select(_ => Validate()) - .BindToStrict(this, vm => vm.ErrorState) - .DisposeWith(disposables); - - LoadLastSavedSettings().FireAndForget(); - }); - } - - - private async Task ReInferSettings() - { - var newSettings = await _inferencer.InferModListFromLocation( - Source.Combine("profiles", SelectedProfile, "modlist.txt")); - - if (newSettings == null) - { - _logger.LogError("Cannot infer settings"); - return; - } - - Include = newSettings.Include; - Ignore = newSettings.Ignore; - AlwaysEnabled = newSettings.AlwaysEnabled; - NoMatchInclude = newSettings.NoMatchInclude; - OtherProfiles = newSettings.AdditionalProfiles; - } - - private ErrorResponse Validate() - { - var errors = new List(); - errors.Add(DownloadLocation.ErrorState); - errors.Add(ModlistLocation.ErrorState); - errors.Add(OutputLocation.ErrorState); - return ErrorResponse.Combine(errors); - } - - private async Task InferModListFromLocation(AbsolutePath path) - { - using var _ = LoadingLock.WithLoading(); - - CompilerSettings settings; - if (path == default) return; - if (path.FileName.Extension == Ext.CompilerSettings) - { - await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read); - settings = (await _dtos.DeserializeAsync(fs))!; - } - else if (path.FileName == "modlist.txt".ToRelativePath()) - { - settings = await _inferencer.InferModListFromLocation(path); - if (settings == null) return; - } - else - { - return; - } - - BaseGame = settings.Game; - ModListName = settings.ModListName; - Version = settings.Version?.ToString() ?? ""; - Author = settings.ModListAuthor; - Description = settings.Description; - ModListImagePath.TargetPath = settings.ModListImage; - Website = settings.ModListWebsite?.ToString() ?? ""; - Readme = settings.ModListReadme?.ToString() ?? ""; - IsNSFW = settings.ModlistIsNSFW; - - Source = settings.Source; - DownloadLocation.TargetPath = settings.Downloads; - if (settings.OutputFile.Extension == Ext.Wabbajack) - settings.OutputFile = settings.OutputFile.Parent; - OutputLocation.TargetPath = settings.OutputFile; - SelectedProfile = settings.Profile; - PublishUpdate = settings.PublishUpdate; - MachineUrl = settings.MachineUrl; - OtherProfiles = settings.AdditionalProfiles; - AlwaysEnabled = settings.AlwaysEnabled; - NoMatchInclude = settings.NoMatchInclude; - Include = settings.Include; - Ignore = settings.Ignore; - if (path.FileName == "modlist.txt".ToRelativePath()) - { - await SaveSettingsFile(); - await LoadLastSavedSettings(); - } - } - - - private async Task StartCompilation() - { - var tsk = Task.Run(async () => - { - try - { - await SaveSettingsFile(); - var token = CancellationToken.None; - State = CompilerState.Compiling; - - foreach (var downloader in await _downloadDispatcher.AllDownloaders([new Nexus()])) - { - _logger.LogInformation("Preparing {Name}", downloader.GetType().Name); - if (await downloader.Prepare()) - continue; - - var manager = _logins - .FirstOrDefault(l => l.LoginFor() == downloader.GetType()); - if (manager == null) - { - _logger.LogError("Cannot install, could not prepare {Name} for downloading", - downloader.GetType().Name); - throw new Exception($"No way to prepare {downloader}"); - } - - RxApp.MainThreadScheduler.Schedule(manager, (_, _) => - { - manager.TriggerLogin.Execute(null); - return Disposable.Empty; - }); - - while (true) - { - if (await downloader.Prepare()) - break; - await Task.Delay(1000); - } - } - - var mo2Settings = GetSettings(); - mo2Settings.UseGamePaths = true; - if (mo2Settings.OutputFile.DirectoryExists()) - mo2Settings.OutputFile = mo2Settings.OutputFile.Combine(mo2Settings.ModListName.ToRelativePath() - .WithExtension(Ext.Wabbajack)); - - if (PublishUpdate && !await RunPreflightChecks(token)) - { - State = CompilerState.Errored; - return; - } - - var compiler = MO2Compiler.Create(_serviceProvider, mo2Settings); - - var events = Observable.FromEventPattern(h => compiler.OnStatusUpdate += h, - h => compiler.OnStatusUpdate -= h) - .ObserveOnGuiThread() - .Debounce(TimeSpan.FromSeconds(0.5)) - .Subscribe(update => - { - var s = update.EventArgs; - StatusText = $"[Step {s.CurrentStep}] {s.StatusText}"; - StatusProgress = s.StepProgress; - }); - - - try - { - var result = await compiler.Begin(token); - if (!result) - throw new Exception("Compilation Failed"); - } - finally - { - events.Dispose(); - } - - if (PublishUpdate) - { - _logger.LogInformation("Publishing List"); - var downloadMetadata = _dtos.Deserialize( - await mo2Settings.OutputFile.WithExtension(Ext.Meta).WithExtension(Ext.Json) - .ReadAllTextAsync())!; - await _wjClient.PublishModlist(MachineUrl, System.Version.Parse(Version), - mo2Settings.OutputFile, downloadMetadata); - } - - _logger.LogInformation("Compiler Finished"); - - RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => - { - StatusText = "Compilation Completed"; - StatusProgress = Percent.Zero; - State = CompilerState.Completed; - return Disposable.Empty; - }); - } - catch (Exception ex) - { - RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => - { - StatusText = "Compilation Failed"; - StatusProgress = Percent.Zero; - - State = CompilerState.Errored; - _logger.LogInformation(ex, "Failed Compilation : {Message}", ex.Message); - return Disposable.Empty; - }); - } - }); - - await tsk; - } - - private async Task RunPreflightChecks(CancellationToken token) - { - var lists = await _wjClient.GetMyModlists(token); - if (!lists.Any(x => x.Equals(MachineUrl, StringComparison.InvariantCultureIgnoreCase))) - { - _logger.LogError("Preflight Check failed, list {MachineUrl} not found in any repository", MachineUrl); - return false; - } - - if (!System.Version.TryParse(Version, out var v)) - { - _logger.LogError("Bad Version Number {Version}", Version); - return false; - } - - return true; - } - - private async Task SaveSettingsFile() - { - if (Source == default) return; - await using var st = SettingsOutputLocation.Open(FileMode.Create, FileAccess.Write, FileShare.None); - await JsonSerializer.SerializeAsync(st, GetSettings(), _dtos.Options); - - await _settingsManager.Save(LastSavedCompilerSettings, SettingsOutputLocation); - } - - private async Task LoadLastSavedSettings() - { - var lastPath = await _settingsManager.Load(LastSavedCompilerSettings); - if (lastPath == default || !lastPath.FileExists() || - lastPath.FileName.Extension != Ext.CompilerSettings) return; - ModlistLocation.TargetPath = lastPath; - } - - - private CompilerSettings GetSettings() - { - System.Version.TryParse(Version, out var pversion); - Uri.TryCreate(Website, UriKind.Absolute, out var websiteUri); - - return new CompilerSettings - { - ModListName = ModListName, - ModListAuthor = Author, - Version = pversion ?? new Version(), - Description = Description, - ModListReadme = Readme, - ModListImage = ModListImagePath.TargetPath, - ModlistIsNSFW = IsNSFW, - ModListWebsite = websiteUri ?? new Uri("http://www.wabbajack.org"), - Downloads = DownloadLocation.TargetPath, - Source = Source, - Game = BaseGame, - PublishUpdate = PublishUpdate, - MachineUrl = MachineUrl, - Profile = SelectedProfile, - UseGamePaths = true, - OutputFile = OutputLocation.TargetPath, - AlwaysEnabled = AlwaysEnabled, - AdditionalProfiles = OtherProfiles, - NoMatchInclude = NoMatchInclude, - Include = Include, - Ignore = Ignore - }; - } - - #region ListOps - - public void AddOtherProfile(string profile) - { - OtherProfiles = (OtherProfiles ?? Array.Empty()).Append(profile).Distinct().ToArray(); - } - - public void RemoveProfile(string profile) - { - OtherProfiles = OtherProfiles.Where(p => p != profile).ToArray(); - } - - public void AddAlwaysEnabled(RelativePath path) - { - AlwaysEnabled = (AlwaysEnabled ?? Array.Empty()).Append(path).Distinct().ToArray(); - } - - public void RemoveAlwaysEnabled(RelativePath path) - { - AlwaysEnabled = AlwaysEnabled.Where(p => p != path).ToArray(); - } - - public void AddNoMatchInclude(RelativePath path) - { - NoMatchInclude = (NoMatchInclude ?? Array.Empty()).Append(path).Distinct().ToArray(); - } - - public void RemoveNoMatchInclude(RelativePath path) - { - NoMatchInclude = NoMatchInclude.Where(p => p != path).ToArray(); - } - - public void AddInclude(RelativePath path) - { - Include = (Include ?? Array.Empty()).Append(path).Distinct().ToArray(); - } - - public void RemoveInclude(RelativePath path) - { - Include = Include.Where(p => p != path).ToArray(); - } - - - public void AddIgnore(RelativePath path) - { - Ignore = (Ignore ?? Array.Empty()).Append(path).Distinct().ToArray(); - } - - public void RemoveIgnore(RelativePath path) - { - Ignore = Ignore.Where(p => p != path).ToArray(); - } - - #endregion - } -} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/Compilers/MO2CompilerVM.cs b/Wabbajack.App.Wpf/View Models/Compilers/MO2CompilerVM.cs deleted file mode 100644 index b9f708ae0..000000000 --- a/Wabbajack.App.Wpf/View Models/Compilers/MO2CompilerVM.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Microsoft.WindowsAPICodePack.Dialogs; -using ReactiveUI.Fody.Helpers; -using System; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading.Tasks; -using DynamicData; -using Wabbajack.Common; -using Wabbajack.Compiler; -using Wabbajack.DTOs; -using Wabbajack.DTOs.GitHub; -using Wabbajack; -using Wabbajack.Extensions; -using Wabbajack.Paths.IO; -using Consts = Wabbajack.Consts; - -namespace Wabbajack -{ - public class MO2CompilerVM : ViewModel - { - public CompilerVM Parent { get; } - - public FilePickerVM DownloadLocation { get; } - - public FilePickerVM ModListLocation { get; } - - [Reactive] - public ACompiler ActiveCompilation { get; private set; } - - [Reactive] - public object StatusTracker { get; private set; } - - public void Unload() - { - throw new NotImplementedException(); - } - - public IObservable CanCompile { get; } - public Task> Compile() - { - throw new NotImplementedException(); - } - - public MO2CompilerVM(CompilerVM parent) - { - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs b/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs deleted file mode 100644 index 48045dcf9..000000000 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListGalleryVM.cs +++ /dev/null @@ -1,261 +0,0 @@ - - -using System; -using System.Collections.Generic; -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; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Common; -using Wabbajack.Downloaders.GameFile; -using Wabbajack.DTOs; -using Wabbajack.Messages; -using Wabbajack.Networking.WabbajackClientApi; -using Wabbajack.Services.OSIntegrated; -using Wabbajack.Services.OSIntegrated.Services; - -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"; - - [Reactive] public IErrorResponse Error { get; set; } - - [Reactive] public string Search { get; set; } - - [Reactive] public bool OnlyInstalled { get; set; } - - [Reactive] public bool ShowNSFW { get; set; } - - [Reactive] public bool ShowUnofficialLists { get; set; } - - [Reactive] public string GameType { get; set; } - - 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; - - public ICommand ClearFiltersCommand { get; set; } - - public ModListGalleryVM(ILogger logger, Client wjClient, GameLocator locator, - SettingsManager settingsManager, ModListDownloadMaintainer maintainer, CancellationToken cancellationToken) - : base(logger) - { - _wjClient = wjClient; - _logger = logger; - _locator = locator; - _maintainer = maintainer; - _settingsManager = settingsManager; - _cancellationToken = cancellationToken; - - ClearFiltersCommand = ReactiveCommand.Create( - () => - { - OnlyInstalled = false; - ShowNSFW = false; - ShowUnofficialLists = false; - Search = string.Empty; - SelectedGameTypeEntry = GameTypeEntries.FirstOrDefault(); - }); - - BackCommand = ReactiveCommand.Create( - () => - { - NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); - }); - - - 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(Search) - .Select>(txt => - { - if (string.IsNullOrWhiteSpace(txt)) return _ => true; - 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 => - { - if (onlyInstalled == false) return _ => true; - return item => _locator.IsInstalled(item.Metadata.Game); - }) - .StartWith(_ => true); - - var showUnofficial = this.ObservableForProperty(vm => vm.ShowUnofficialLists) - .Select(v => v.Value) - .StartWith(false) - .Select>(unoffical => - { - 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) - .Filter(onlyInstalledGamesFilter) - .Filter(showUnofficial) - .Filter(showNSFWFilter) - .Filter(gameFilter) - .Bind(out _filteredModLists) - .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); - }); - } - - private class FilterSettings - { - public string GameType { get; set; } - public bool ShowNSFW { get; set; } - public bool ShowUnofficialLists { get; set; } - public bool OnlyInstalled { get; set; } - public string Search { get; set; } - } - - public override void Unload() - { - Error = null; - } - - private async Task SaveSettings() - { - await _settingsManager.Save("modlist_gallery", new FilterSettings - { - GameType = GameType, - ShowNSFW = ShowNSFW, - ShowUnofficialLists = ShowUnofficialLists, - Search = Search, - OnlyInstalled = OnlyInstalled, - }); - } - - private async Task LoadSettings() - { - using var ll = LoadingLock.WithLoading(); - RxApp.MainThreadScheduler.Schedule(await _settingsManager.Load("modlist_gallery"), - (_, s) => - { - SelectedGameTypeEntry = GameTypeEntries?.FirstOrDefault(gte => gte.HumanFriendlyName.Equals(s.GameType)); - ShowNSFW = s.ShowNSFW; - ShowUnofficialLists = s.ShowUnofficialLists; - Search = s.Search; - OnlyInstalled = s.OnlyInstalled; - return Disposable.Empty; - }); - } - - private async Task LoadModLists() - { - using var ll = LoadingLock.WithLoading(); - try - { - var modLists = await _wjClient.LoadLists(); - var modlistSummaries = await _wjClient.GetListStatuses(); - _modLists.Edit(e => - { - e.Clear(); - e.AddOrUpdate(modLists.Select(m => - new ModListMetadataVM(_logger, this, m, _maintainer, modlistSummaries, _wjClient, _cancellationToken))); - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "While loading lists"); - ll.Fail(); - } - ll.Succeed(); - } - - private List GetGameTypeEntries() - { - 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 deleted file mode 100644 index d9336e48a..000000000 --- a/Wabbajack.App.Wpf/View Models/Gallery/ModListMetadataVM.cs +++ /dev/null @@ -1,243 +0,0 @@ -using System; -using System.Collections.Generic; -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; -using DynamicData; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Common; -using Wabbajack.DTOs; -using Wabbajack.DTOs.ServerResponses; -using Wabbajack; -using Wabbajack.Extensions; -using Wabbajack.Messages; -using Wabbajack.Models; -using Wabbajack.Networking.WabbajackClientApi; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; -using Wabbajack.RateLimiter; -using Wabbajack.Services.OSIntegrated.Services; - -namespace Wabbajack -{ - - public struct ModListTag - { - public ModListTag(string name) - { - Name = name; - } - - public string Name { get; } - } - - public class ModListMetadataVM : ViewModel - { - public ModlistMetadata Metadata { get; } - private ModListGalleryVM _parent; - - public ICommand OpenWebsiteCommand { get; } - public ICommand ExecuteCommand { get; } - - public ICommand ModListContentsCommend { get; } - - private readonly ObservableAsPropertyHelper _Exists; - public bool Exists => _Exists.Value; - - public AbsolutePath Location { get; } - - public LoadingLock LoadingImageLock { get; } = new(); - - [Reactive] - public List ModListTagList { get; private set; } - - [Reactive] - public Percent ProgressPercent { get; private set; } - - [Reactive] - public bool IsBroken { get; private set; } - - [Reactive] - public ModListStatus Status { get; set; } - - [Reactive] - public bool IsDownloading { get; private set; } - - [Reactive] - public string DownloadSizeText { get; private set; } - - [Reactive] - public string InstallSizeText { get; private set; } - - [Reactive] - public string TotalSizeRequirementText { get; private set; } - - [Reactive] - public string VersionText { get; private set; } - - [Reactive] - public bool ImageContainsTitle { get; private set; } - - [Reactive] - - public bool DisplayVersionOnlyInInstallerView { get; private set; } - - [Reactive] - public IErrorResponse Error { get; private set; } - - private readonly ObservableAsPropertyHelper _Image; - public BitmapImage Image => _Image.Value; - - private readonly ObservableAsPropertyHelper _LoadingImage; - public bool LoadingImage => _LoadingImage.Value; - - private Subject IsLoadingIdle; - 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, ModListSummary[] modlistSummaries, 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(); - - UpdateStatus().FireAndForget(); - - Metadata.Tags.ForEach(tag => - { - ModListTagList.Add(new ModListTag(tag)); - }); - ModListTagList.Add(new ModListTag(metadata.Game.MetaData().HumanFriendlyGameName)); - - DownloadSizeText = "Download size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives); - InstallSizeText = "Installation size : " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles); - TotalSizeRequirementText = "Total size requirement: " + UIUtils.FormatBytes( - Metadata.DownloadMetadata.SizeOfArchives + Metadata.DownloadMetadata.SizeOfInstalledFiles - ); - VersionText = "Modlist version : " + Metadata.Version; - ImageContainsTitle = Metadata.ImageContainsTitle; - DisplayVersionOnlyInInstallerView = Metadata.DisplayVersionOnlyInInstallerView; - var modListSummary = GetModListSummaryForModlist(modlistSummaries, metadata.NamespacedName); - IsBroken = modListSummary.HasFailures || metadata.ForceDown; - // https://www.wabbajack.org/modlist/wj-featured/aldrnari - OpenWebsiteCommand = ReactiveCommand.Create(() => UIUtils.OpenWebsite(new Uri($"https://www.wabbajack.org/modlist/{Metadata.NamespacedName}"))); - - IsLoadingIdle = new Subject(); - - ModListContentsCommend = ReactiveCommand.Create(async () => - { - UIUtils.OpenWebsite(new Uri($"https://www.wabbajack.org/search/{Metadata.NamespacedName}")); - }, IsLoadingIdle.StartWith(true)); - - ExecuteCommand = ReactiveCommand.CreateFromTask(async () => - { - if (await _maintainer.HaveModList(Metadata)) - { - LoadModlistForInstalling.Send(_maintainer.ModListPath(Metadata), Metadata); - NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); - } - else - { - await Download(); - } - }, LoadingLock.WhenAnyValue(ll => ll.IsLoading) - .CombineLatest(this.WhenAnyValue(vm => vm.IsBroken)) - .Select(v => !v.First && !v.Second)); - - _Exists = Observable.Interval(TimeSpan.FromSeconds(0.5)) - .Unit() - .StartWith(Unit.Default) - .FlowSwitch(_parent.WhenAny(x => x.IsActive)) - .SelectAsync(async _ => - { - try - { - return !IsDownloading && await maintainer.HaveModList(metadata); - } - catch (Exception) - { - return true; - } - }) - .ToGuiProperty(this, nameof(Exists)); - - var imageObs = Observable.Return(Metadata.Links.ImageUri) - .DownloadBitmapImage((ex) => _logger.LogError("Error downloading modlist image {Title}", Metadata.Title), LoadingImageLock); - - _Image = imageObs - .ToGuiProperty(this, nameof(Image)); - - _LoadingImage = imageObs - .Select(x => false) - .StartWith(true) - .ToGuiProperty(this, nameof(LoadingImage)); - } - - - - private async Task Download() - { - try - { - Status = ModListStatus.Downloading; - - using var ll = LoadingLock.WithLoading(); - var (progress, task) = _maintainer.DownloadModlist(Metadata, _cancellationToken); - var dispose = progress - .BindToStrict(this, vm => vm.ProgressPercent); - try - { - await _wjClient.SendMetric("downloading", Metadata.Title); - await task; - await UpdateStatus(); - } - finally - { - dispose.Dispose(); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "While downloading {Modlist}", Metadata.RepositoryName); - await UpdateStatus(); - } - } - - private async Task UpdateStatus() - { - if (await _maintainer.HaveModList(Metadata)) - Status = ModListStatus.Downloaded; - else if (LoadingLock.IsLoading) - Status = ModListStatus.Downloading; - else - Status = ModListStatus.NotDownloaded; - } - - public enum ModListStatus - { - NotDownloaded, - Downloading, - Downloaded - } - - private static ModListSummary GetModListSummaryForModlist(ModListSummary[] modListSummaries, string machineUrl) - { - return modListSummaries.FirstOrDefault(x => x.MachineURL == machineUrl); - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/GameVM.cs b/Wabbajack.App.Wpf/View Models/GameVM.cs deleted file mode 100644 index 602b0c4d3..000000000 --- a/Wabbajack.App.Wpf/View Models/GameVM.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Wabbajack.DTOs; - -namespace Wabbajack -{ - public class GameVM - { - public Game Game { get; } - public string DisplayName { get; } - - public GameVM(Game game) - { - Game = game; - DisplayName = game.MetaData().HumanFriendlyGameName; - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/Installers/ISubInstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/ISubInstallerVM.cs deleted file mode 100644 index 8849400a4..000000000 --- a/Wabbajack.App.Wpf/View Models/Installers/ISubInstallerVM.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Threading.Tasks; -using Wabbajack.Installer; -using Wabbajack.DTOs.Interventions; - -namespace Wabbajack -{ - public interface ISubInstallerVM - { - InstallerVM Parent { get; } - IInstaller ActiveInstallation { get; } - void Unload(); - bool SupportsAfterInstallNavigation { get; } - void AfterInstallNavigation(); - int ConfigVisualVerticalOffset { get; } - ErrorResponse CanInstall { get; } - Task Install(); - IUserIntervention InterventionConverter(IUserIntervention intervention); - } -} diff --git a/Wabbajack.App.Wpf/View Models/Installers/MO2InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/MO2InstallerVM.cs deleted file mode 100644 index 623381e0e..000000000 --- a/Wabbajack.App.Wpf/View Models/Installers/MO2InstallerVM.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Diagnostics; -using System.Reactive.Disposables; -using System.Threading.Tasks; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Installer; -using Wabbajack.DTOs.Interventions; -using Wabbajack.Paths; - -namespace Wabbajack -{ - public class MO2InstallerVM : ViewModel, ISubInstallerVM - { - public InstallerVM Parent { get; } - - [Reactive] - public ErrorResponse CanInstall { get; set; } - - [Reactive] - public IInstaller ActiveInstallation { get; private set; } - - [Reactive] - public Mo2ModlistInstallationSettings CurrentSettings { get; set; } - - public FilePickerVM Location { get; } - - public FilePickerVM DownloadLocation { get; } - - public bool SupportsAfterInstallNavigation => true; - - [Reactive] - public bool AutomaticallyOverwrite { get; set; } - - public int ConfigVisualVerticalOffset => 25; - - public MO2InstallerVM(InstallerVM installerVM) - { - Parent = installerVM; - - Location = new FilePickerVM() - { - ExistCheckOption = FilePickerVM.CheckOptions.Off, - PathType = FilePickerVM.PathTypeOptions.Folder, - PromptTitle = "Select Installation Directory", - }; - Location.WhenAnyValue(t => t.TargetPath) - .Subscribe(newPath => - { - if (newPath != default && DownloadLocation!.TargetPath == AbsolutePath.Empty) - { - DownloadLocation.TargetPath = newPath.Combine("downloads"); - } - }).DisposeWith(CompositeDisposable); - - DownloadLocation = new FilePickerVM() - { - ExistCheckOption = FilePickerVM.CheckOptions.Off, - PathType = FilePickerVM.PathTypeOptions.Folder, - PromptTitle = "Select a location for MO2 downloads", - }; - } - - public void Unload() - { - SaveSettings(this.CurrentSettings); - } - - private void SaveSettings(Mo2ModlistInstallationSettings settings) - { - //Parent.MWVM.Settings.Installer.LastInstalledListLocation = Parent.ModListLocation.TargetPath; - if (settings == null) return; - settings.InstallationLocation = Location.TargetPath; - settings.DownloadLocation = DownloadLocation.TargetPath; - settings.AutomaticallyOverrideExistingInstall = AutomaticallyOverwrite; - } - - public void AfterInstallNavigation() - { - UIUtils.OpenFolder(Location.TargetPath); - } - - public async Task Install() - { - /* - using (var installer = new MO2Installer( - archive: Parent.ModListLocation.TargetPath, - modList: Parent.ModList.SourceModList, - outputFolder: Location.TargetPath, - downloadFolder: DownloadLocation.TargetPath, - parameters: SystemParametersConstructor.Create())) - { - installer.Metadata = Parent.ModList.SourceModListMetadata; - installer.UseCompression = Parent.MWVM.Settings.Filters.UseCompression; - Parent.MWVM.Settings.Performance.SetProcessorSettings(installer); - - return await Task.Run(async () => - { - try - { - var workTask = installer.Begin(); - ActiveInstallation = installer; - return await workTask; - } - finally - { - ActiveInstallation = null; - } - }); - } - */ - return true; - } - - public IUserIntervention InterventionConverter(IUserIntervention intervention) - { - switch (intervention) - { - case ConfirmUpdateOfExistingInstall confirm: - return new ConfirmUpdateOfExistingInstallVM(this, confirm); - default: - return intervention; - } - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/Interfaces/ICpuStatusVM.cs b/Wabbajack.App.Wpf/View Models/Interfaces/ICpuStatusVM.cs deleted file mode 100644 index 3a149bae8..000000000 --- a/Wabbajack.App.Wpf/View Models/Interfaces/ICpuStatusVM.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using DynamicData.Binding; -using ReactiveUI; - -namespace Wabbajack -{ - public interface ICpuStatusVM : IReactiveObject - { - ReadOnlyObservableCollection StatusList { get; } - } -} diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs deleted file mode 100644 index cd1430ed3..000000000 --- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs +++ /dev/null @@ -1,281 +0,0 @@ -using DynamicData.Binding; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Input; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Orc.FileAssociation; -using Wabbajack.Common; -using Wabbajack.DTOs.Interventions; -using Wabbajack.Interventions; -using Wabbajack.Messages; -using Wabbajack.Models; -using Wabbajack.Networking.WabbajackClientApi; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; -using Wabbajack.UserIntervention; -using Wabbajack.View_Models; - -namespace Wabbajack -{ - /// - /// Main View Model for the application. - /// Keeps track of which sub view is being shown in the window, and has some singleton wiring like WorkQueue and Logging. - /// - public class MainWindowVM : ViewModel - { - public MainWindow MainWindow { get; } - - [Reactive] - public ViewModel ActivePane { get; private set; } - - public ObservableCollectionExtended Log { get; } = new ObservableCollectionExtended(); - - public readonly CompilerVM Compiler; - public readonly InstallerVM Installer; - public readonly SettingsVM SettingsPane; - public readonly ModListGalleryVM Gallery; - public readonly ModeSelectionVM ModeSelectionVM; - public readonly WebBrowserVM WebBrowserVM; - public readonly Lazy ModListContentsVM; - public readonly UserInterventionHandlers UserInterventionHandlers; - private readonly Client _wjClient; - private readonly ILogger _logger; - private readonly ResourceMonitor _resourceMonitor; - - private List PreviousPanes = new(); - private readonly IServiceProvider _serviceProvider; - - public ICommand CopyVersionCommand { get; } - public ICommand ShowLoginManagerVM { get; } - public ICommand OpenSettingsCommand { get; } - - public string VersionDisplay { get; } - - [Reactive] - public string ResourceStatus { get; set; } - - [Reactive] - public string AppName { get; set; } - - [Reactive] - public bool UpdateAvailable { get; private set; } - - public MainWindowVM(ILogger logger, Client wjClient, - IServiceProvider serviceProvider, ModeSelectionVM modeSelectionVM, ModListGalleryVM modListGalleryVM, ResourceMonitor resourceMonitor, - InstallerVM installer, CompilerVM compilerVM, SettingsVM settingsVM, WebBrowserVM webBrowserVM) - { - _logger = logger; - _wjClient = wjClient; - _resourceMonitor = resourceMonitor; - _serviceProvider = serviceProvider; - ConverterRegistration.Register(); - Installer = installer; - Compiler = compilerVM; - SettingsPane = settingsVM; - Gallery = modListGalleryVM; - ModeSelectionVM = modeSelectionVM; - WebBrowserVM = webBrowserVM; - ModListContentsVM = new Lazy(() => new ModListContentsVM(serviceProvider.GetRequiredService>(), this)); - UserInterventionHandlers = new UserInterventionHandlers(serviceProvider.GetRequiredService>(), this); - - MessageBus.Current.Listen() - .Subscribe(m => HandleNavigateTo(m.Screen)) - .DisposeWith(CompositeDisposable); - - MessageBus.Current.Listen() - .Subscribe(m => HandleNavigateTo(m.ViewModel)) - .DisposeWith(CompositeDisposable); - - MessageBus.Current.Listen() - .Subscribe(HandleNavigateBack) - .DisposeWith(CompositeDisposable); - - MessageBus.Current.Listen() - .ObserveOnGuiThread() - .Subscribe(HandleSpawnBrowserWindow) - .DisposeWith(CompositeDisposable); - - _resourceMonitor.Updates - .Select(r => string.Join(", ", r.Where(r => r.Throughput > 0) - .Select(s => $"{s.Name} - {s.Throughput.ToFileSizeString()}/sec"))) - .BindToStrict(this, view => view.ResourceStatus); - - - if (IsStartingFromModlist(out var path)) - { - LoadModlistForInstalling.Send(path, null); - NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); - } - else - { - // Start on mode selection - NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); - } - - try - { - var assembly = Assembly.GetExecutingAssembly(); - var assemblyLocation = assembly.Location; - var processLocation = Process.GetCurrentProcess().MainModule?.FileName ?? throw new Exception("Process location is unavailable!"); - - _logger.LogInformation("Assembly Location: {AssemblyLocation}", assemblyLocation); - _logger.LogInformation("Process Location: {ProcessLocation}", processLocation); - - var fvi = FileVersionInfo.GetVersionInfo(string.IsNullOrWhiteSpace(assemblyLocation) ? processLocation : assemblyLocation); - Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion); - VersionDisplay = $"v{fvi.FileVersion}"; - AppName = "WABBAJACK " + VersionDisplay; - _logger.LogInformation("Wabbajack Version: {FileVersion}", fvi.FileVersion); - - Task.Run(() => _wjClient.SendMetric("started_wabbajack", fvi.FileVersion)).FireAndForget(); - Task.Run(() => _wjClient.SendMetric("started_sha", ThisAssembly.Git.Sha)); - - // setup file association - try - { - var applicationRegistrationService = _serviceProvider.GetRequiredService(); - - var applicationInfo = new ApplicationInfo("Wabbajack", "Wabbajack", "Wabbajack", processLocation); - applicationInfo.SupportedExtensions.Add("wabbajack"); - applicationRegistrationService.RegisterApplication(applicationInfo); - } - catch (Exception ex) - { - _logger.LogError(ex, "While setting up file associations"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "During App configuration"); - VersionDisplay = "ERROR"; - } - CopyVersionCommand = ReactiveCommand.Create(() => - { - Clipboard.SetText($"Wabbajack {VersionDisplay}\n{ThisAssembly.Git.Sha}"); - }); - OpenSettingsCommand = ReactiveCommand.Create( - canExecute: this.WhenAny(x => x.ActivePane) - .Select(active => !object.ReferenceEquals(active, SettingsPane)), - execute: () => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Settings)); - } - - private void HandleNavigateTo(ViewModel objViewModel) - { - - ActivePane = objViewModel; - } - - private void HandleNavigateBack(NavigateBack navigateBack) - { - ActivePane = PreviousPanes.Last(); - PreviousPanes.RemoveAt(PreviousPanes.Count - 1); - } - - private void HandleManualDownload(ManualDownload manualDownload) - { - var handler = _serviceProvider.GetRequiredService(); - handler.Intervention = manualDownload; - //MessageBus.Current.SendMessage(new OpenBrowserTab(handler)); - } - - private void HandleManualBlobDownload(ManualBlobDownload manualDownload) - { - var handler = _serviceProvider.GetRequiredService(); - handler.Intervention = manualDownload; - //MessageBus.Current.SendMessage(new OpenBrowserTab(handler)); - } - - private void HandleSpawnBrowserWindow(SpawnBrowserWindow msg) - { - var window = _serviceProvider.GetRequiredService(); - window.DataContext = msg.Vm; - window.Show(); - } - - private void HandleNavigateTo(NavigateToGlobal.ScreenType s) - { - if (s is NavigateToGlobal.ScreenType.Settings) - PreviousPanes.Add(ActivePane); - - ActivePane = s switch - { - NavigateToGlobal.ScreenType.ModeSelectionView => ModeSelectionVM, - NavigateToGlobal.ScreenType.ModListGallery => Gallery, - NavigateToGlobal.ScreenType.Installer => Installer, - NavigateToGlobal.ScreenType.Compiler => Compiler, - NavigateToGlobal.ScreenType.Settings => SettingsPane, - _ => ActivePane - }; - } - - - private static bool IsStartingFromModlist(out AbsolutePath modlistPath) - { - var args = Environment.GetCommandLineArgs(); - if (args.Length == 2) - { - var arg = args[1].ToAbsolutePath(); - if (arg.FileExists() && arg.Extension == Ext.Wabbajack) - { - modlistPath = arg; - return true; - } - } - - modlistPath = default; - 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) - { - ActivePane = vm; - }*/ - - /* - public void NavigateTo(T vm) - where T : ViewModel, IBackNavigatingVM - { - vm.NavigateBackTarget = ActivePane; - ActivePane = vm; - }*/ - - public async Task ShutdownApplication() - { - /* - Dispose(); - Settings.PosX = MainWindow.Left; - Settings.PosY = MainWindow.Top; - Settings.Width = MainWindow.Width; - Settings.Height = MainWindow.Height; - await MainSettings.SaveSettings(Settings); - Application.Current.Shutdown(); - */ - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs b/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs deleted file mode 100644 index 558f67772..000000000 --- a/Wabbajack.App.Wpf/View Models/ModListContentsVM.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text.RegularExpressions; -using DynamicData; -using DynamicData.Binding; -using Microsoft.Extensions.Logging; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Common; -using Wabbajack.DTOs; -using Wabbajack.DTOs.ServerResponses; - -namespace Wabbajack.View_Models -{ - public class ModListContentsVM : BackNavigatingVM - { - private MainWindowVM _mwvm; - [Reactive] - public string Name { get; set; } - - [Reactive] - public ObservableCollection Status { get; set; } - - [Reactive] - public string SearchString { get; set; } - - private readonly ReadOnlyObservableCollection _archives; - public ReadOnlyObservableCollection Archives => _archives; - - private static readonly Regex NameMatcher = new(@"(?<=\.)[^\.]+(?=\+State)", RegexOptions.Compiled); - private readonly ILogger _logger; - - public ModListContentsVM(ILogger logger, MainWindowVM mwvm) : base(logger) - { - _logger = logger; - _mwvm = mwvm; - Status = new ObservableCollectionExtended(); - - string TransformClassName(Archive a) - { - var cname = a.State.GetType().FullName; - if (cname == null) return null; - - var match = NameMatcher.Match(cname); - return match.Success ? match.ToString() : null; - } - - this.Status - .ToObservableChangeSet() - .Transform(a => new ModListArchive - { - Name = a.Name, - Size = a.Archive?.Size ?? 0, - Downloader = TransformClassName(a.Archive) ?? "Unknown", - Hash = a.Archive!.Hash.ToBase64() - }) - .Filter(this.WhenAny(x => x.SearchString) - .StartWith("") - .Throttle(TimeSpan.FromMilliseconds(250)) - .Select>(s => (ModListArchive ar) => - string.IsNullOrEmpty(s) || - ar.Name.ContainsCaseInsensitive(s) || - ar.Downloader.ContainsCaseInsensitive(s) || - ar.Hash.ContainsCaseInsensitive(s) || - ar.Size.ToString() == s || - ar.Url.ContainsCaseInsensitive(s))) - .ObserveOnGuiThread() - .Bind(out _archives) - .Subscribe() - .DisposeWith(CompositeDisposable); - } - } - - public class ModListArchive - { - public string Name { get; set; } - public long Size { get; set; } - public string Url { get; set; } - public string Downloader { get; set; } - public string Hash { get; set; } - } -} diff --git a/Wabbajack.App.Wpf/View Models/ModListVM.cs b/Wabbajack.App.Wpf/View Models/ModListVM.cs deleted file mode 100644 index 1056f97e5..000000000 --- a/Wabbajack.App.Wpf/View Models/ModListVM.cs +++ /dev/null @@ -1,135 +0,0 @@ -using ReactiveUI; -using System; -using System.IO; -using System.IO.Compression; -using System.Reactive; -using System.Reactive.Linq; -using System.Threading.Tasks; -using System.Windows.Media.Imaging; -using Microsoft.Extensions.Logging; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Common; -using Wabbajack.DTOs; -using Wabbajack.DTOs.JsonConverters; -using Wabbajack.Installer; -using Wabbajack; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; -using Consts = Wabbajack.Consts; - -namespace Wabbajack -{ - public class ModListVM : ViewModel - { - private readonly DTOSerializer _dtos; - private readonly ILogger _logger; - public ModList SourceModList { get; private set; } - public ModlistMetadata SourceModListMetadata { get; private set; } - - [Reactive] - public Exception Error { get; set; } - public AbsolutePath ModListPath { get; } - public string Name => SourceModList?.Name; - public string Readme => SourceModList?.Readme; - public string Author => SourceModList?.Author; - public string Description => SourceModList?.Description; - public Uri Website => SourceModList?.Website; - public Version Version => SourceModList?.Version; - public Version WabbajackVersion => SourceModList?.WabbajackVersion; - public bool IsNSFW => SourceModList?.IsNSFW ?? false; - - // Image isn't exposed as a direct property, but as an observable. - // This acts as a caching mechanism, as interested parties will trigger it to be created, - // and the cached image will automatically be released when the last interested party is gone. - public IObservable ImageObservable { get; } - - public ModListVM(ILogger logger, AbsolutePath modListPath, DTOSerializer dtos) - { - _dtos = dtos; - _logger = logger; - - ModListPath = modListPath; - - Task.Run(async () => - { - try - { - SourceModList = await StandardInstaller.LoadFromFile(_dtos, modListPath); - var metadataPath = modListPath.WithExtension(Ext.ModlistMetadataExtension); - if (metadataPath.FileExists()) - { - try - { - SourceModListMetadata = await metadataPath.FromJson(); - } - catch (Exception) - { - SourceModListMetadata = null; - } - } - } - catch (Exception ex) - { - Error = ex; - _logger.LogError(ex, "Exception while loading the modlist!"); - } - }); - - ImageObservable = Observable.Return(Unit.Default) - // Download and retrieve bytes on background thread - .ObserveOn(RxApp.TaskpoolScheduler) - .SelectAsync(async filePath => - { - try - { - await using var fs = ModListPath.Open(FileMode.Open, FileAccess.Read, FileShare.Read); - using var ar = new ZipArchive(fs, ZipArchiveMode.Read); - var ms = new MemoryStream(); - var entry = ar.GetEntry("modlist-image.png"); - if (entry == null) return default(MemoryStream); - await using var e = entry.Open(); - e.CopyTo(ms); - return ms; - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception while caching Mod List image {Name}", Name); - return default(MemoryStream); - } - }) - // Create Bitmap image on GUI thread - .ObserveOnGuiThread() - .Select(memStream => - { - if (memStream == null) return default(BitmapImage); - try - { - return UIUtils.BitmapImageFromStream(memStream); - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception while caching Mod List image {Name}", Name); - return default(BitmapImage); - } - }) - // If ever would return null, show WJ logo instead - .Select(x => x ?? ResourceLinks.WabbajackLogoNoText.Value) - .Replay(1) - .RefCount(); - } - - public void OpenReadme() - { - if (string.IsNullOrEmpty(Readme)) return; - UIUtils.OpenWebsite(new Uri(Readme)); - } - - public override void Dispose() - { - base.Dispose(); - // Just drop reference explicitly, as it's large, so it can be GCed - // Even if someone is holding a stale reference to the VM - SourceModList = null; - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/ModVM.cs b/Wabbajack.App.Wpf/View Models/ModVM.cs deleted file mode 100644 index 14b0d80a9..000000000 --- a/Wabbajack.App.Wpf/View Models/ModVM.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ReactiveUI; -using System; -using System.Reactive.Linq; -using System.Windows.Media.Imaging; -using Microsoft.Extensions.Logging; -using Wabbajack.DTOs.DownloadStates; -using Wabbajack; - -namespace Wabbajack -{ - public class ModVM : ViewModel - { - private readonly ILogger _logger; - public IMetaState State { get; } - - // Image isn't exposed as a direct property, but as an observable. - // This acts as a caching mechanism, as interested parties will trigger it to be created, - // and the cached image will automatically be released when the last interested party is gone. - public IObservable ImageObservable { get; } - - public ModVM(ILogger logger, IMetaState state) - { - _logger = logger; - State = state; - - ImageObservable = Observable.Return(State.ImageURL?.ToString()) - .ObserveOn(RxApp.TaskpoolScheduler) - .DownloadBitmapImage(ex => _logger.LogError(ex, "Skipping slide for mod {Name}", State.Name), LoadingLock) - .Replay(1) - .RefCount(TimeSpan.FromMilliseconds(5000)); - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs b/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs deleted file mode 100644 index 77ca9085f..000000000 --- a/Wabbajack.App.Wpf/View Models/ModeSelectionVM.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using System; -using System.IO; -using System.Linq; -using System.Reactive; -using System.Reactive.Linq; -using System.Windows.Input; -using Wabbajack.Common; -using Wabbajack; -using Wabbajack.Messages; -using Wabbajack.Paths.IO; - -namespace Wabbajack -{ - public class ModeSelectionVM : ViewModel - { - public ICommand BrowseCommand { get; } - public ICommand InstallCommand { get; } - public ICommand CompileCommand { get; } - - public ReactiveCommand UpdateCommand { get; } - - public ModeSelectionVM() - { - InstallCommand = ReactiveCommand.Create(() => - { - LoadLastLoadedModlist.Send(); - NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Installer); - }); - CompileCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Compiler)); - BrowseCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModListGallery)); - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs b/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs deleted file mode 100644 index 4b909dce8..000000000 --- a/Wabbajack.App.Wpf/View Models/Settings/AuthorFilesVM.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Input; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Wabbajack; -using Wabbajack.Networking.WabbajackClientApi; -using Wabbajack.Services.OSIntegrated.TokenProviders; - -namespace Wabbajack.View_Models.Settings -{ - public class AuthorFilesVM : BackNavigatingVM - { - [Reactive] - public Visibility IsVisible { get; set; } - - public ICommand SelectFile { get; } - public ICommand HyperlinkCommand { get; } - public IReactiveCommand Upload { get; } - public IReactiveCommand ManageFiles { get; } - - [Reactive] public double UploadProgress { get; set; } - [Reactive] public string FinalUrl { get; set; } - public FilePickerVM Picker { get;} - - private Subject _isUploading = new(); - private readonly WabbajackApiTokenProvider _token; - private readonly Client _wjClient; - private IObservable IsUploading { get; } - - public AuthorFilesVM(ILogger logger, WabbajackApiTokenProvider token, Client wjClient, SettingsVM vm) : base(logger) - { - _token = token; - _wjClient = wjClient; - IsUploading = _isUploading; - Picker = new FilePickerVM(this); - - - IsVisible = Visibility.Hidden; - - Task.Run(async () => - { - var isAuthor = !string.IsNullOrWhiteSpace((await _token.Get())?.AuthorKey); - IsVisible = isAuthor ? Visibility.Visible : Visibility.Collapsed; - }); - - SelectFile = Picker.ConstructTypicalPickerCommand(IsUploading.StartWith(false).Select(u => !u)); - - HyperlinkCommand = ReactiveCommand.Create(() => Clipboard.SetText(FinalUrl)); - - ManageFiles = ReactiveCommand.Create(async () => - { - var authorApiKey = (await token.Get())!.AuthorKey; - UIUtils.OpenWebsite(new Uri($"{Consts.WabbajackBuildServerUri}author_controls/login/{authorApiKey}")); - }); - - Upload = ReactiveCommand.Create(async () => - { - _isUploading.OnNext(true); - try - { - var (progress, task) = await _wjClient.UploadAuthorFile(Picker.TargetPath); - - var disposable = progress.Subscribe(m => - { - FinalUrl = m.Message; - UploadProgress = (double)m.PercentDone; - }); - - var final = await task; - disposable.Dispose(); - FinalUrl = final.ToString(); - } - catch (Exception ex) - { - FinalUrl = ex.ToString(); - } - finally - { - FinalUrl = FinalUrl.Replace(" ", "%20"); - _isUploading.OnNext(false); - } - }, IsUploading.StartWith(false).Select(u => !u) - .CombineLatest(Picker.WhenAnyValue(t => t.TargetPath).Select(f => f != default), - (a, b) => a && b)); - } - - } -} diff --git a/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs b/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs deleted file mode 100644 index f2021215d..000000000 --- a/Wabbajack.App.Wpf/View Models/Settings/LoginManagerVM.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.Logging; -using Wabbajack.LoginManagers; - -namespace Wabbajack -{ - - public class LoginManagerVM : BackNavigatingVM - { - public LoginTargetVM[] Logins { get; } - - public LoginManagerVM(ILogger logger, SettingsVM settingsVM, IEnumerable logins) - : base(logger) - { - Logins = logins.Select(l => new LoginTargetVM(l)).ToArray(); - } - - } - - public class LoginTargetVM : ViewModel - { - public INeedsLogin Login { get; } - public LoginTargetVM(INeedsLogin login) - { - Login = login; - } - } - -} diff --git a/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs b/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs deleted file mode 100644 index a32855cec..000000000 --- a/Wabbajack.App.Wpf/View Models/Settings/SettingsVM.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Threading.Tasks; -using System.Windows.Input; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using Wabbajack.Common; -using Wabbajack.Downloaders; -using Wabbajack.LoginManagers; -using Wabbajack.Messages; -using Wabbajack.Networking.WabbajackClientApi; -using Wabbajack.RateLimiter; -using Wabbajack.Services.OSIntegrated; -using Wabbajack.Services.OSIntegrated.TokenProviders; -using Wabbajack.Util; -using Wabbajack.View_Models.Settings; - -namespace Wabbajack -{ - public class SettingsVM : BackNavigatingVM - { - private readonly Configuration.MainSettings _settings; - private readonly SettingsManager _settingsManager; - - public LoginManagerVM Login { get; } - public PerformanceSettings Performance { get; } - public AuthorFilesVM AuthorFile { get; } - - public ICommand OpenTerminalCommand { get; } - - public SettingsVM(ILogger logger, IServiceProvider provider) - : base(logger) - { - _settings = provider.GetRequiredService(); - _settingsManager = provider.GetRequiredService(); - - Login = new LoginManagerVM(provider.GetRequiredService>(), this, - provider.GetRequiredService>()); - AuthorFile = new AuthorFilesVM(provider.GetRequiredService>()!, - provider.GetRequiredService()!, provider.GetRequiredService()!, this); - OpenTerminalCommand = ReactiveCommand.CreateFromTask(OpenTerminal); - Performance = new PerformanceSettings( - _settings, - provider.GetRequiredService>(), - provider.GetRequiredService()); - BackCommand = ReactiveCommand.Create(() => - { - NavigateBack.Send(); - Unload(); - }); - } - - public override void Unload() - { - _settingsManager.Save(Configuration.MainSettings.SettingsFileName, _settings).FireAndForget(); - - base.Unload(); - } - - private async Task OpenTerminal() - { - var process = new ProcessStartInfo - { - FileName = "cmd.exe", - WorkingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)! - }; - Process.Start(process); - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/UserIntervention/ConfirmUpdateOfExistingInstallVM.cs b/Wabbajack.App.Wpf/View Models/UserIntervention/ConfirmUpdateOfExistingInstallVM.cs deleted file mode 100644 index ece18fe01..000000000 --- a/Wabbajack.App.Wpf/View Models/UserIntervention/ConfirmUpdateOfExistingInstallVM.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Wabbajack.Common; -using Wabbajack; -using Wabbajack.DTOs.Interventions; -using Wabbajack.Interventions; - -namespace Wabbajack -{ - public class ConfirmUpdateOfExistingInstallVM : ViewModel, IUserIntervention - { - public ConfirmUpdateOfExistingInstall Source { get; } - - public MO2InstallerVM Installer { get; } - - public bool Handled => ((IUserIntervention)Source).Handled; - public CancellationToken Token { get; } - public void SetException(Exception exception) - { - throw new NotImplementedException(); - } - - public int CpuID => 0; - - public DateTime Timestamp => DateTime.Now; - - public string ShortDescription => "Short Desc"; - - public string ExtendedDescription => "Extended Desc"; - - public ConfirmUpdateOfExistingInstallVM(MO2InstallerVM installer, ConfirmUpdateOfExistingInstall confirm) - { - Source = confirm; - Installer = installer; - } - - public void Cancel() - { - ((IUserIntervention)Source).Cancel(); - } - } -} diff --git a/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs b/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs deleted file mode 100644 index 19a49a1a5..000000000 --- a/Wabbajack.App.Wpf/View Models/UserInterventionHandlers.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using Wabbajack.Common; -using Wabbajack; -using Wabbajack.DTOs.Interventions; -using Wabbajack.Interventions; -using Wabbajack.Messages; - -namespace Wabbajack -{ - public class UserInterventionHandlers - { - public MainWindowVM MainWindow { get; } - private AsyncLock _browserLock = new(); - private readonly ILogger _logger; - - public UserInterventionHandlers(ILogger logger, MainWindowVM mvm) - { - _logger = logger; - MainWindow = mvm; - } - - private async Task WrapBrowserJob(IUserIntervention intervention, WebBrowserVM vm, Func toDo) - { - var wait = await _browserLock.WaitAsync(); - var cancel = new CancellationTokenSource(); - var oldPane = MainWindow.ActivePane; - - // TODO: FIX using var vm = await WebBrowserVM.GetNew(_logger); - NavigateTo.Send(vm); - vm.BackCommand = ReactiveCommand.Create(() => - { - cancel.Cancel(); - NavigateTo.Send(oldPane); - intervention.Cancel(); - }); - - try - { - await toDo(vm, cancel); - } - catch (TaskCanceledException) - { - intervention.Cancel(); - } - catch (Exception ex) - { - _logger.LogError(ex, "During Web browser job"); - intervention.Cancel(); - } - finally - { - wait.Dispose(); - } - - NavigateTo.Send(oldPane); - } - - public async Task Handle(IStatusMessage msg) - { - switch (msg) - { - /* - case RequestNexusAuthorization c: - await WrapBrowserJob(c, async (vm, cancel) => - { - await vm.Driver.WaitForInitialized(); - var key = await NexusApiClient.SetupNexusLogin(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token); - c.Resume(key); - }); - break; - case ManuallyDownloadNexusFile c: - await WrapBrowserJob(c, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c)); - break; - case ManuallyDownloadFile c: - await WrapBrowserJob(c, (vm, cancel) => HandleManualDownload(vm, cancel, c)); - break; - case AbstractNeedsLoginDownloader.RequestSiteLogin c: - await WrapBrowserJob(c, async (vm, cancel) => - { - await vm.Driver.WaitForInitialized(); - var data = await c.Downloader.GetAndCacheCookies(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token); - c.Resume(data); - }); - break; - case RequestOAuthLogin oa: - await WrapBrowserJob(oa, async (vm, cancel) => - { - await OAuthLogin(oa, vm, cancel); - }); - - - break; - */ - case CriticalFailureIntervention c: - MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK, - MessageBoxImage.Error); - c.Cancel(); - if (c.ExitApplication) await MainWindow.ShutdownApplication(); - break; - case ConfirmationIntervention c: - break; - default: - throw new NotImplementedException($"No handler for {msg}"); - } - } - - } -} diff --git a/Wabbajack.App.Wpf/View Models/WebBrowserVM.cs b/Wabbajack.App.Wpf/View Models/WebBrowserVM.cs deleted file mode 100644 index 3be45cfb0..000000000 --- a/Wabbajack.App.Wpf/View Models/WebBrowserVM.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Reactive; -using System.Reactive.Subjects; -using Microsoft.Extensions.Logging; -using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using Wabbajack.Messages; -using Wabbajack.Models; - -namespace Wabbajack -{ - public class WebBrowserVM : ViewModel, IBackNavigatingVM, IDisposable - { - private readonly ILogger _logger; - private readonly CefService _cefService; - - [Reactive] - public string Instructions { get; set; } - - public dynamic Browser { get; } - public dynamic Driver { get; set; } - - [Reactive] - public ViewModel NavigateBackTarget { get; set; } - - [Reactive] - public ReactiveCommand BackCommand { get; set; } - - public Subject IsBackEnabledSubject { get; } = new Subject(); - public IObservable IsBackEnabled { get; } - - public WebBrowserVM(ILogger logger, CefService cefService) - { - // CefService is required so that Cef is initalized - _logger = logger; - _cefService = cefService; - Instructions = "Wabbajack Web Browser"; - - BackCommand = ReactiveCommand.Create(NavigateBack.Send); - //Browser = cefService.CreateBrowser(); - //Driver = new CefSharpWrapper(_logger, Browser, cefService); - - } - - public override void Dispose() - { - Browser.Dispose(); - base.Dispose(); - } - } -} diff --git a/Wabbajack.App.Wpf/ViewModel.cs b/Wabbajack.App.Wpf/ViewModel.cs deleted file mode 100644 index 8eb82a25c..000000000 --- a/Wabbajack.App.Wpf/ViewModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Newtonsoft.Json; -using ReactiveUI; -using System; -using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using Wabbajack.Models; - -namespace Wabbajack -{ - public class ViewModel : ReactiveObject, IDisposable, IActivatableViewModel - { - private readonly Lazy _compositeDisposable = new(); - [JsonIgnore] - public CompositeDisposable CompositeDisposable => _compositeDisposable.Value; - - [JsonIgnore] public LoadingLock LoadingLock { get; } = new(); - - public virtual void Dispose() - { - if (_compositeDisposable.IsValueCreated) - { - _compositeDisposable.Value.Dispose(); - } - } - - protected void RaiseAndSetIfChanged( - ref T item, - T newItem, - [CallerMemberName] string? propertyName = null) - { - if (EqualityComparer.Default.Equals(item, newItem)) return; - item = newItem; - this.RaisePropertyChanged(propertyName); - } - - public ViewModelActivator Activator { get; } = new(); - } -} diff --git a/Wabbajack.App.Wpf/ViewModels/BackNavigatingVM.cs b/Wabbajack.App.Wpf/ViewModels/BackNavigatingVM.cs new file mode 100644 index 000000000..aa7791153 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/BackNavigatingVM.cs @@ -0,0 +1,73 @@ +using System; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.Messages; + +namespace Wabbajack; + +public interface IBackNavigatingVM : IReactiveObject +{ + ViewModel NavigateBackTarget { get; set; } + ReactiveCommand BackCommand { get; } + + Subject IsBackEnabledSubject { get; } + IObservable IsBackEnabled { get; } +} + +public class BackNavigatingVM : ViewModel, IBackNavigatingVM +{ + [Reactive] + public ViewModel NavigateBackTarget { get; set; } + public ReactiveCommand BackCommand { get; protected set; } + + [Reactive] + public bool IsActive { get; set; } + + public Subject IsBackEnabledSubject { get; } = new Subject(); + public IObservable IsBackEnabled { get; } + + public BackNavigatingVM(ILogger logger) + { + IsBackEnabled = IsBackEnabledSubject.StartWith(true); + BackCommand = ReactiveCommand.Create( + execute: () => logger.CatchAndLog(() => + { + NavigateBack.Send(); + Unload(); + }), + canExecute: this.ConstructCanNavigateBack() + .ObserveOnGuiThread()); + + this.WhenActivated(disposables => + { + IsActive = true; + Disposable.Create(() => IsActive = false).DisposeWith(disposables); + }); + } + + public virtual void Unload() + { + } +} + +public static class IBackNavigatingVMExt +{ + public static IObservable ConstructCanNavigateBack(this IBackNavigatingVM vm) + { + return vm.WhenAny(x => x.NavigateBackTarget) + .CombineLatest(vm.IsBackEnabled) + .Select(x => x.First != null && x.Second); + } + + public static IObservable ConstructIsActive(this IBackNavigatingVM vm, MainWindowVM mwvm) + { + return mwvm.WhenAny(x => x.ActivePane) + .Select(x => object.ReferenceEquals(vm, x)); + } +} diff --git a/Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs b/Wabbajack.App.Wpf/ViewModels/BrowserWindowViewModel.cs similarity index 97% rename from Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs rename to Wabbajack.App.Wpf/ViewModels/BrowserWindowViewModel.cs index 5daee154b..247d4a584 100644 --- a/Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs +++ b/Wabbajack.App.Wpf/ViewModels/BrowserWindowViewModel.cs @@ -1,20 +1,15 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using HtmlAgilityPack; using Microsoft.Web.WebView2.Core; -using Microsoft.Web.WebView2.Wpf; -using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Logins; using Wabbajack.Hashing.xxHash64; -using Wabbajack.Messages; using Wabbajack.Paths; -using Wabbajack.Views; namespace Wabbajack; diff --git a/Wabbajack.App.Wpf/ViewModels/CPUDisplayVM.cs b/Wabbajack.App.Wpf/ViewModels/CPUDisplayVM.cs new file mode 100644 index 000000000..6124d8271 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/CPUDisplayVM.cs @@ -0,0 +1,23 @@ +using System; +using ReactiveUI.Fody.Helpers; +using Wabbajack.RateLimiter; + +namespace Wabbajack; + +public class CPUDisplayVM : ViewModel +{ + [Reactive] + public ulong ID { get; set; } + [Reactive] + public DateTime StartTime { get; set; } + [Reactive] + public bool IsWorking { get; set; } + [Reactive] + public string Msg { get; set; } + [Reactive] + public Percent ProgressPercent { get; set; } + + public CPUDisplayVM() + { + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/BaseCompilerVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/BaseCompilerVM.cs new file mode 100644 index 000000000..04dd4ea13 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/BaseCompilerVM.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reactive.Disposables; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Paths; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Paths.IO; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Messages; + +namespace Wabbajack; + +public abstract class BaseCompilerVM : ProgressViewModel +{ + protected readonly DTOSerializer _dtos; + protected readonly SettingsManager _settingsManager; + protected readonly ILogger _logger; + protected readonly Client _wjClient; + + [Reactive] public CompilerSettingsVM Settings { get; set; } = new(); + + public BaseCompilerVM(DTOSerializer dtos, SettingsManager settingsManager, ILogger logger, Client wjClient) + { + _dtos = dtos; + _settingsManager = settingsManager; + _logger = logger; + _wjClient = wjClient; + + MessageBus.Current.Listen() + .Subscribe(msg => { + var csVm = new CompilerSettingsVM(msg.CompilerSettings); + Settings = csVm; + }) + .DisposeWith(CompositeDisposable); + } + + protected async Task SaveSettings() + { + if (Settings.Source == default || Settings.CompilerSettingsPath == default) return; + + try + { + await using var st = Settings.CompilerSettingsPath.Open(FileMode.Create, FileAccess.Write, FileShare.None); + await JsonSerializer.SerializeAsync(st, Settings.ToCompilerSettings(), new JsonSerializerOptions(_dtos.Options) { WriteIndented = true }); + } + catch(Exception ex) + { + _logger.LogError("Failed to save compiler settings to {0}! {1}", Settings.CompilerSettingsPath, ex.ToString()); + } + + var allSavedCompilerSettings = await _settingsManager.Load>(Consts.AllSavedCompilerSettingsPaths); + + // Don't simply remove Settings.CompilerSettingsPath here, because WJ sometimes likes to make default compiler settings files + allSavedCompilerSettings.RemoveAll(path => path.Parent == Settings.Source); + allSavedCompilerSettings.Insert(0, Settings.CompilerSettingsPath); + + try + { + await _settingsManager.Save(Consts.AllSavedCompilerSettingsPaths, allSavedCompilerSettings); + } + catch(Exception ex) + { + _logger.LogError("Failed to save all saved compiler settings! {0}", ex.ToString()); + } + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompiledModListTileVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompiledModListTileVM.cs new file mode 100644 index 000000000..5161460d7 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompiledModListTileVM.cs @@ -0,0 +1,32 @@ +using System.Windows.Input; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Compiler; +using Wabbajack.Messages; +using Wabbajack.Models; + +namespace Wabbajack; + +public class CompiledModListTileVM +{ + private ILogger _logger; + public LoadingLock LoadingImageLock { get; } = new(); + public ICommand CompileModListCommand { get; set; } + [Reactive] + public CompilerSettings CompilerSettings { get; set; } + + public CompiledModListTileVM(ILogger logger, CompilerSettings compilerSettings) + { + _logger = logger; + CompilerSettings = compilerSettings; + CompileModListCommand = ReactiveCommand.Create(CompileModList); + } + + private void CompileModList() + { + _logger.LogInformation($"Selected modlist {CompilerSettings.ModListName} for compilation, located in '{CompilerSettings.Source}'"); + NavigateToGlobal.Send(ScreenType.CompilerMain); + LoadCompilerSettings.Send(CompilerSettings); + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerDetailsVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerDetailsVM.cs new file mode 100644 index 000000000..3c2c1b7a9 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerDetailsVM.cs @@ -0,0 +1,279 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reactive; +using Microsoft.Extensions.Logging; +using Wabbajack.Messages; +using ReactiveUI; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using DynamicData; +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.Compiler; +using Wabbajack.DTOs; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Extensions; +using Wabbajack.Installer; +using Wabbajack.Models; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated; + +namespace Wabbajack; + +public enum CompilerState +{ + Configuration, + Compiling, + Completed, + Errored +} +public class CompilerDetailsVM : BaseCompilerVM, ICpuStatusVM +{ + private readonly ResourceMonitor _resourceMonitor; + private readonly CompilerSettingsInferencer _inferencer; + + public CompilerFileManagerVM CompilerFileManagerVM { get; private set; } + [Reactive] public List AvailableProfiles { get; set; } + + [Reactive] + public CompilerState State { get; set; } + + [Reactive] + public MO2CompilerVM SubCompilerVM { get; set; } + + // Paths + public FilePickerVM ModlistLocation { get; private set; } + public FilePickerVM DownloadLocation { get; private set; } + public FilePickerVM OutputLocation { get; private set; } + + public FilePickerVM ModListImageLocation { get; private set; } = new(); + + /* public ReactiveCommand ExecuteCommand { get; } */ + public ReactiveCommand ReInferSettingsCommand { get; set; } + public ReactiveCommand StartCommand { get; } + + public LogStream LoggerProvider { get; } + public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; + + [Reactive] + public ErrorResponse ErrorState { get; private set; } + + public CompilerDetailsVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, + IServiceProvider serviceProvider, LogStream loggerProvider, ResourceMonitor resourceMonitor, + CompilerSettingsInferencer inferencer, Client wjClient, CompilerFileManagerVM compilerFileManagerVM) : base(dtos, settingsManager, logger, wjClient) + { + LoggerProvider = loggerProvider; + _resourceMonitor = resourceMonitor; + _inferencer = inferencer; + CompilerFileManagerVM = compilerFileManagerVM; + + SubCompilerVM = new MO2CompilerVM(this); + + StartCommand = ReactiveCommand.CreateFromTask(StartCompilation); + + + this.WhenActivated(disposables => + { + State = CompilerState.Configuration; + + ModlistLocation = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.On, + PathType = FilePickerVM.PathTypeOptions.File, + PromptTitle = "Select a config file or a modlist.txt file", + TargetPath = Settings.ProfilePath + }; + + ModlistLocation.Filters.AddRange(new[] + { + new CommonFileDialogFilter("MO2 Modlist", "*" + Ext.Txt), + new CommonFileDialogFilter("Compiler Settings File", "*" + Ext.CompilerSettings) + }); + + DownloadLocation = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.On, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Location where the downloads for this list are stored" + }; + + OutputLocation = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.Off, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Location where the compiled modlist will be stored" + }; + + ModListImageLocation = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.On, + PathType = FilePickerVM.PathTypeOptions.File, + PromptTitle = "Thumbnail image file to use for the modlist" + }; + ModListImageLocation.Filters.AddRange(new[] + { + new CommonFileDialogFilter("WebP Image (preferred)", "*" + Ext.Webp), + new CommonFileDialogFilter("PNG Image", "*" + Ext.Png), + new CommonFileDialogFilter("JPG Image", "*" + Ext.Jpg), + }); + + + ModlistLocation.WhenAnyValue(vm => vm.TargetPath) + .Subscribe(async p => { + if (p == default) return; + if (Settings.CompilerSettingsPath != default) return; + else if(p.FileName == "modlist.txt".ToRelativePath()) await ReInferSettings(p); + }) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.DownloadLocation.TargetPath) + .CombineLatest(this.WhenAnyValue(x => x.ModlistLocation.TargetPath), + this.WhenAnyValue(x => x.OutputLocation.TargetPath), + this.WhenAnyValue(x => x.DownloadLocation.ErrorState), + this.WhenAnyValue(x => x.ModlistLocation.ErrorState), + this.WhenAnyValue(x => x.OutputLocation.ErrorState)) + .Select(_ => Validate()) + .BindToStrict(this, vm => vm.ErrorState) + .DisposeWith(disposables); + this.WhenAnyValue(x => x.Settings.Source) + .Subscribe(source => + { + AvailableProfiles = source.Combine("profiles").EnumerateDirectories().Select(dir => dir.FileName.ToString()).ToList(); + }) + .DisposeWith(disposables); + + }); + } + + private async Task ReInferSettings(AbsolutePath filePath) + { + var newSettings = await _inferencer.InferModListFromLocation(filePath); + + if (newSettings == null) + { + _logger.LogError("Cannot infer settings from {0}", filePath); + return; + } + + Settings.Source = newSettings.Source; + Settings.Downloads = newSettings.Downloads; + + if (string.IsNullOrEmpty(Settings.ModListName)) + Settings.OutputFile = newSettings.OutputFile.Combine(newSettings.Profile).WithExtension(Ext.Wabbajack); + else + Settings.OutputFile = newSettings.OutputFile.Combine(newSettings.ModListName).WithExtension(Ext.Wabbajack); + + Settings.Game = newSettings.Game; + Settings.Include = newSettings.Include.ToHashSet(); + Settings.Ignore = newSettings.Ignore.ToHashSet(); + Settings.AlwaysEnabled = newSettings.AlwaysEnabled.ToHashSet(); + Settings.NoMatchInclude = newSettings.NoMatchInclude.ToHashSet(); + Settings.AdditionalProfiles = newSettings.AdditionalProfiles; + } + + private ErrorResponse Validate() + { + var errors = new List + { + DownloadLocation.ErrorState, + ModlistLocation.ErrorState, + OutputLocation.ErrorState + }; + return ErrorResponse.Combine(errors); + } + + private async Task InferModListFromLocation(AbsolutePath path) + { + using var _ = LoadingLock.WithLoading(); + + CompilerSettings settings; + if (path == default) return new(); + if (path.FileName.Extension == Ext.CompilerSettings) + { + await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + settings = (await _dtos.DeserializeAsync(fs))!; + } + else if (path.FileName == "modlist.txt".ToRelativePath()) + { + settings = await _inferencer.InferModListFromLocation(path); + if (settings == null) return new(); + } + else + { + return new(); + } + + return settings; + } + + private async Task StartCompilation() + { + await SaveSettings(); + NavigateToGlobal.Send(ScreenType.CompilerMain); + LoadCompilerSettings.Send(Settings.ToCompilerSettings()); + } + + #region ListOps + + public void AddOtherProfile(string profile) + { + Settings.AdditionalProfiles = (Settings.AdditionalProfiles ?? Array.Empty()).Append(profile).Distinct().ToArray(); + } + + public void RemoveProfile(string profile) + { + Settings.AdditionalProfiles = Settings.AdditionalProfiles.Where(p => p != profile).ToArray(); + } + + public void AddAlwaysEnabled(RelativePath path) + { + Settings.AlwaysEnabled = (Settings.AlwaysEnabled ?? new()).Append(path).Distinct().ToHashSet(); + } + + public void RemoveAlwaysEnabled(RelativePath path) + { + Settings.AlwaysEnabled = Settings.AlwaysEnabled.Where(p => p != path).ToHashSet(); + } + + public void AddNoMatchInclude(RelativePath path) + { + Settings.NoMatchInclude = (Settings.NoMatchInclude ?? new()).Append(path).Distinct().ToHashSet(); + } + + public void RemoveNoMatchInclude(RelativePath path) + { + Settings.NoMatchInclude = Settings.NoMatchInclude.Where(p => p != path).ToHashSet(); + } + + public void AddInclude(RelativePath path) + { + Settings.Include = (Settings.Include ?? new()).Append(path).Distinct().ToHashSet(); + } + + public void RemoveInclude(RelativePath path) + { + Settings.Include = Settings.Include.Where(p => p != path).ToHashSet(); + } + + + public void AddIgnore(RelativePath path) + { + Settings.Ignore = (Settings.Ignore ?? new()).Append(path).Distinct().ToHashSet(); + } + + public void RemoveIgnore(RelativePath path) + { + Settings.Ignore = Settings.Ignore.Where(p => p != path).ToHashSet(); + } + + #endregion +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerFileManagerVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerFileManagerVM.cs new file mode 100644 index 000000000..50b28896b --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerFileManagerVM.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using Microsoft.Extensions.Logging; +using Wabbajack.Messages; +using ReactiveUI; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Wabbajack.Common; +using Wabbajack.Compiler; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Models; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Paths; +using Wabbajack.Services.OSIntegrated; +using System.Windows.Controls; +using System.Windows.Input; +using System.ComponentModel; + +namespace Wabbajack; + +public class CompilerFileManagerVM : BaseCompilerVM +{ + private readonly IServiceProvider _serviceProvider; + private readonly ResourceMonitor _resourceMonitor; + private readonly CompilerSettingsInferencer _inferencer; + + public ObservableCollection Files { get; set; } + + public CompilerFileManagerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, + IServiceProvider serviceProvider, ResourceMonitor resourceMonitor, + CompilerSettingsInferencer inferencer, Client wjClient) : base(dtos, settingsManager, logger, wjClient) + { + _serviceProvider = serviceProvider; + _resourceMonitor = resourceMonitor; + _inferencer = inferencer; + this.WhenActivated(disposables => + { + if (Settings.Source != default) + { + var fileTree = GetDirectoryContents(new DirectoryInfo(Settings.Source.ToString())); + Files = LoadSource(new DirectoryInfo(Settings.Source.ToString())); + } + + Disposable.Create(() => { }).DisposeWith(disposables); + }); + } + + private ObservableCollection LoadSource(DirectoryInfo parent) + { + var parentTreeItem = new FileTreeViewItem(parent) + { + IsExpanded = true, + ItemsSource = LoadDirectoryContents(parent), + }; + return [parentTreeItem]; + + } + + private IEnumerable LoadDirectoryContents(DirectoryInfo parent) + { + return parent.EnumerateDirectories() + .OrderBy(dir => dir.Name) + .Select(dir => new FileTreeViewItem(dir) { ItemsSource = (dir.EnumerateDirectories().Any() || dir.EnumerateFiles().Any()) ? new ObservableCollection([FileTreeViewItem.Placeholder]) : null}).Select(item => + { + item.Expanded += LoadingItem_Expanded; + SetFileTreeViewItemProperties(item); + return item; + }) + .Concat(parent.EnumerateFiles() + .OrderBy(file => file.Name) + .Select(file => { + var item = new FileTreeViewItem(file); + SetFileTreeViewItemProperties(item); + return item; + })) + .ToList(); + } + + private void SetFileTreeViewItemProperties(FileTreeViewItem item) + { + var header = item.Header; + header.PathRelativeToRoot = ((AbsolutePath)header.Info.FullName).RelativeTo(Settings.Source); + if (Settings.NoMatchInclude.Contains(header.PathRelativeToRoot)) { header.CompilerFileState = CompilerFileState.NoMatchInclude; } + else if (Settings.Include.Contains(header.PathRelativeToRoot)) { header.CompilerFileState = CompilerFileState.Include; } + else if (Settings.Ignore.Contains(header.PathRelativeToRoot)) { header.CompilerFileState = CompilerFileState.Ignore; } + else if (Settings.AlwaysEnabled.Contains(header.PathRelativeToRoot)) { header.CompilerFileState = CompilerFileState.AlwaysEnabled; } + SetContainedStates(header); + header.PropertyChanged += Header_PropertyChanged; + } + + private void SetContainedStates(FileTreeItemVM header) + { + if (!header.IsDirectory) return; + header.ContainsNoMatchIncludes = Settings.NoMatchInclude.Any(p => p.InFolder(header.PathRelativeToRoot)); + header.ContainsIncludes = Settings.Include.Any(p => p.InFolder(header.PathRelativeToRoot)); + header.ContainsIgnores = Settings.Ignore.Any(p => p.InFolder(header.PathRelativeToRoot)); + header.ContainsAlwaysEnableds = Settings.AlwaysEnabled.Any(p => p.InFolder(header.PathRelativeToRoot)); + } + + private async void Header_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + var updatedItem = (FileTreeItemVM)sender; + if(e.PropertyName == nameof(FileTreeItemVM.CompilerFileState)) + { + Settings.NoMatchInclude.Remove(updatedItem.PathRelativeToRoot); + Settings.Include.Remove(updatedItem.PathRelativeToRoot); + Settings.Ignore.Remove(updatedItem.PathRelativeToRoot); + Settings.AlwaysEnabled.Remove(updatedItem.PathRelativeToRoot); + + switch(updatedItem.CompilerFileState) + { + case CompilerFileState.NoMatchInclude: + Settings.NoMatchInclude.Add(updatedItem.PathRelativeToRoot); + break; + case CompilerFileState.Include: + Settings.Include.Add(updatedItem.PathRelativeToRoot); + break; + case CompilerFileState.Ignore: + Settings.Ignore.Add(updatedItem.PathRelativeToRoot); + break; + case CompilerFileState.AlwaysEnabled: + Settings.AlwaysEnabled.Add(updatedItem.PathRelativeToRoot); + break; + }; + + // Update contained states of parents upon changing compiler state on child (ContainsIgnores, ContainsIncludes) + if (updatedItem.PathRelativeToRoot.Depth > 1) + { + IEnumerable files = Files.First().ItemsSource.Cast(); + for (int i = 0; i < updatedItem.PathRelativeToRoot.Depth - 1; i++) + { + var currPathPart = updatedItem.PathRelativeToRoot.Parts[i]; + foreach (var file in files) + { + if (file.Header.ToString() == currPathPart) + { + SetContainedStates(file.Header); + files = file.ItemsSource.Cast(); + break; + } + } + } + } + + await SaveSettings(); + } + } + + private void LoadingItem_Expanded(object sender, System.Windows.RoutedEventArgs e) + { + var parent = (FileTreeViewItem)e.OriginalSource; + foreach(var child in parent.ItemsSource) + { + if (child == FileTreeViewItem.Placeholder) + { + parent.ItemsSource = LoadDirectoryContents((DirectoryInfo)parent.Header.Info); + break; + } + break; + } + } + + private IEnumerable GetDirectoryContents(DirectoryInfo dir) + { + var directories = dir.EnumerateDirectories(); + var items = dir.EnumerateFiles(); + return directories.OrderBy(x => x.Name).Concat(items.OrderBy(y => y.Name)); + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs new file mode 100644 index 000000000..d834a87d8 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using DynamicData; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using SteamKit2.GC.Dota.Internal; +using Wabbajack.Common; +using Wabbajack.Compiler; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Messages; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using Wabbajack.Services.OSIntegrated; + +namespace Wabbajack; + +public class CompilerHomeVM : ViewModel +{ + private readonly SettingsManager _settingsManager; + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly CancellationToken _cancellationToken; + private readonly DTOSerializer _dtos; + private readonly CompilerSettingsInferencer _inferencer; + + [Reactive] public ICommand NewModlistCommand { get; set; } + [Reactive] public ICommand LoadSettingsCommand { get; set; } + + [Reactive] + public ObservableCollection CompiledModLists { get; set; } + + public FilePickerVM CompilerSettingsPicker { get; private set; } + public FilePickerVM NewModlistPicker { get; private set; } + + public CompilerHomeVM(ILogger logger, SettingsManager settingsManager, + IServiceProvider serviceProvider, DTOSerializer dtos, CompilerSettingsInferencer inferencer) + { + _logger = logger; + _settingsManager = settingsManager; + _serviceProvider = serviceProvider; + _dtos = dtos; + _inferencer = inferencer; + + NewModlistPicker = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.On, + PathType = FilePickerVM.PathTypeOptions.File, + PromptTitle = "Select a Mod Organizer profile (modlist.txt)" + }; + NewModlistPicker.Filters.AddRange([ + new CommonFileDialogFilter("Modlist", "modlist" + Ext.Txt) + ]); + + CompilerSettingsPicker = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.On, + PathType = FilePickerVM.PathTypeOptions.File, + PromptTitle = "Select a compiler settings file" + }; + CompilerSettingsPicker.Filters.AddRange([ + new CommonFileDialogFilter("Compiler Settings File", "*" + Ext.CompilerSettings) + ]); + + NewModlistCommand = ReactiveCommand.CreateFromTask(async () => { + NewModlistPicker.SetTargetPathCommand.Execute(null); + if(NewModlistPicker.TargetPath != default) + { + try + { + var compilerSettings = await _inferencer.InferModListFromLocation(NewModlistPicker.TargetPath); + NavigateToGlobal.Send(ScreenType.CompilerMain); + LoadCompilerSettings.Send(compilerSettings); + } + catch (Exception ex) + { + _logger.LogError("Failed to create new compiler settings for target path {0}! {1}", NewModlistPicker.TargetPath, ex.ToString()); + } + } + }); + + LoadSettingsCommand = ReactiveCommand.Create(() => + { + CompilerSettingsPicker.SetTargetPathCommand.Execute(null); + if(CompilerSettingsPicker.TargetPath != default) + { + try + { + var compilerSettings = _dtos.Deserialize(File.ReadAllText(CompilerSettingsPicker.TargetPath.ToString())); + NavigateToGlobal.Send(ScreenType.CompilerMain); + LoadCompilerSettings.Send(compilerSettings); + } + catch (Exception ex) + { + _logger.LogError("Failed to load compiler settings from {0}! {1}", CompilerSettingsPicker.TargetPath, ex.ToString()); + } + } + }); + + this.WhenActivated(disposables => + { + LoadAllCompilerSettings().DisposeWith(disposables); + }); + } + + private async Task LoadAllCompilerSettings() + { + CompiledModLists = new(); + var savedCompilerSettingsPaths = await _settingsManager.Load>(Consts.AllSavedCompilerSettingsPaths); + foreach(var settingsPath in savedCompilerSettingsPaths) + { + await using var fs = settingsPath.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + var settings = (await _dtos.DeserializeAsync(fs))!; + CompiledModLists.Add(new CompiledModListTileVM(_logger, settings)); + } + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerMainVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerMainVM.cs new file mode 100644 index 000000000..737146d53 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerMainVM.cs @@ -0,0 +1,287 @@ +using Microsoft.Extensions.Logging; +using Wabbajack.Messages; +using ReactiveUI; +using System.Reactive.Disposables; +using ReactiveUI.Fody.Helpers; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Models; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Services.OSIntegrated; +using System.Windows.Input; +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Threading; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Wabbajack.Common; +using Wabbajack.Compiler; +using Wabbajack.DTOs; +using Wabbajack.Extensions; +using Wabbajack.Installer; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using Wabbajack.RateLimiter; +using Wabbajack.LoginManagers; +using Wabbajack.Downloaders; +using Wabbajack.DTOs.DownloadStates; + +namespace Wabbajack; + +public class CompilerMainVM : BaseCompilerVM, IHasInfoVM, ICpuStatusVM +{ + private readonly IServiceProvider _serviceProvider; + private readonly ResourceMonitor _resourceMonitor; + private readonly IEnumerable _logins; + private readonly DownloadDispatcher _downloadDispatcher; + + public CompilerDetailsVM CompilerDetailsVM { get; set; } + public CompilerFileManagerVM CompilerFileManagerVM { get; set; } + + public LogStream LoggerProvider { get; } + public CancellationTokenSource CancellationTokenSource { get; private set; } + + public ICommand InfoCommand { get; } + public ICommand StartCommand { get; } + public ICommand CancelCommand { get; } + public ICommand OpenLogCommand { get; } + public ICommand OpenFolderCommand { get; } + public ICommand PublishCommand { get; } + + [Reactive] public CompilerState State { get; set; } + public bool Cancelling { get; private set; } + + public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; + + public CompilerMainVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, + LogStream loggerProvider, Client wjClient, IServiceProvider serviceProvider, ResourceMonitor resourceMonitor, + CompilerDetailsVM compilerDetailsVM, CompilerFileManagerVM compilerFileManagerVM, IEnumerable logins) : base(dtos, settingsManager, logger, wjClient) + { + _serviceProvider = serviceProvider; + _resourceMonitor = resourceMonitor; + _logins = logins; + + LoggerProvider = loggerProvider; + CompilerDetailsVM = compilerDetailsVM; + CompilerFileManagerVM = compilerFileManagerVM; + + CancellationTokenSource = new CancellationTokenSource(); + + InfoCommand = ReactiveCommand.Create(Info); + StartCommand = ReactiveCommand.Create(StartCompilation); + CancelCommand = ReactiveCommand.Create(CancelCompilation); + OpenLogCommand = ReactiveCommand.Create(OpenLog); + OpenFolderCommand = ReactiveCommand.Create(OpenFolder); + PublishCommand = ReactiveCommand.Create(Publish); + + ProgressPercent = Percent.Zero; + this.WhenActivated(disposables => + { + if (State != CompilerState.Compiling) + { + ShowNavigation.Send(); + ConfigurationText = "Modlist Details"; + ProgressText = "Compilation"; + ProgressPercent = Percent.Zero; + CurrentStep = Step.Configuration; + State = CompilerState.Configuration; + ProgressState = ProgressState.Normal; + } + + this.WhenAnyValue(x => x.CompilerDetailsVM.Settings) + .BindTo(this, x => x.Settings) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.CompilerFileManagerVM.Settings.Include) + .BindTo(this, x => x.Settings.Include) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.CompilerFileManagerVM.Settings.Ignore) + .BindTo(this, x => x.Settings.Ignore) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.CompilerFileManagerVM.Settings.NoMatchInclude) + .BindTo(this, x => x.Settings.NoMatchInclude) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.CompilerFileManagerVM.Settings.AlwaysEnabled) + .BindTo(this, x => x.Settings.AlwaysEnabled) + .DisposeWith(disposables); + }); + } + + private void OpenLog() + { + var log = KnownFolders.LauncherAwarePath.Combine("logs").Combine("Wabbajack.current.log").ToString(); + Process.Start(new ProcessStartInfo(log) { UseShellExecute = true }); + } + + private async Task Publish() + { + bool readyForPublish = await RunPreflightChecks(CancellationToken.None); + if (!readyForPublish) return; + + _logger.LogInformation("Publishing List"); + var downloadMetadata = _dtos.Deserialize( + await Settings.OutputFile.WithExtension(Ext.Meta).WithExtension(Ext.Json).ReadAllTextAsync())!; + await _wjClient.PublishModlist(Settings.MachineUrl, Version.Parse(Settings.Version), Settings.OutputFile, downloadMetadata); + } + + private void OpenFolder() => Process.Start(new ProcessStartInfo() { FileName = "explorer.exe ", Arguments = $"/select, \"{Settings.OutputFile}\"" }); + + private void Info() => Process.Start(new ProcessStartInfo("https://wiki.wabbajack.org/modlist_author_documentation/Compilation.html") { UseShellExecute = true }); + + private async Task StartCompilation() + { + var tsk = Task.Run(async () => + { + try + { + HideNavigation.Send(); + await SaveSettings(); + var token = CancellationTokenSource.Token; + + await EnsureLoggedIntoNexus(); + + RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => + { + ProgressText = "Compiling..."; + State = CompilerState.Compiling; + CurrentStep = Step.Busy; + ProgressText = "Compiling..."; + ProgressState = ProgressState.Normal; + return Disposable.Empty; + }); + + Settings.UseGamePaths = true; + if (Settings.OutputFile.DirectoryExists()) + Settings.OutputFile = Settings.OutputFile.Combine(Settings.ModListName.ToRelativePath() + .WithExtension(Ext.Wabbajack)); + + var compiler = MO2Compiler.Create(_serviceProvider, Settings.ToCompilerSettings()); + + var events = Observable.FromEventPattern(h => compiler.OnStatusUpdate += h, + h => compiler.OnStatusUpdate -= h) + .ObserveOnGuiThread() + .Subscribe(update => + { + var s = update.EventArgs; + ProgressText = $"{s.StatusText}"; + ProgressPercent = s.StepsProgress; + }); + + + try + { + var result = await compiler.Begin(token); + if (!result) + throw new Exception("Compilation Failed"); + } + finally + { + events.Dispose(); + } + + _logger.LogInformation("Compiler Finished"); + + RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => + { + ShowNavigation.Send(); + ProgressText = "Compiled"; + ProgressPercent = Percent.One; + State = CompilerState.Completed; + CurrentStep = Step.Done; + ProgressState = ProgressState.Success; + return Disposable.Empty; + }); + + + } + catch (Exception ex) + { + RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => + { + ShowNavigation.Send(); + if (Cancelling) + { + this.ProgressText = "Compilation Cancelled"; + ProgressPercent = Percent.Zero; + State = CompilerState.Configuration; + _logger.LogInformation(ex, "Cancelled Compilation : {Message}", ex.Message); + Cancelling = false; + return Disposable.Empty; + } + else + { + this.ProgressText = "Compilation Failed"; + ProgressPercent = Percent.Zero; + + State = CompilerState.Errored; + _logger.LogInformation(ex, "Failed Compilation : {Message}", ex.Message); + return Disposable.Empty; + } + }); + } + }); + + await tsk; + } + + private async Task EnsureLoggedIntoNexus() + { + var nexusDownloadState = new Nexus(); + foreach (var downloader in await _downloadDispatcher.AllDownloaders([nexusDownloadState])) + { + + var manager = _logins.FirstOrDefault(l => l.LoginFor() == typeof(Nexus)); + if (manager == null) + { + _logger.LogError("Cannot compile, could not prepare Nexus for verifying"); + throw new Exception($"No way to prepare {nexusDownloadState}"); + } + + RxApp.MainThreadScheduler.Schedule(manager, (_, _) => + { + manager.TriggerLogin.Execute(null); + return Disposable.Empty; + }); + + while (true) + { + if (await downloader.Prepare()) + break; + await Task.Delay(1000); + } + } + } + + private async Task CancelCompilation() + { + if (State != CompilerState.Compiling) return; + Cancelling = true; + _logger.LogInformation("Cancel pressed, cancelling compilation..."); + await CancellationTokenSource.CancelAsync(); + CancellationTokenSource = new CancellationTokenSource(); + } + + private async Task RunPreflightChecks(CancellationToken token) + { + var lists = await _wjClient.GetMyModlists(token); + if (!lists.Any(x => x.Equals(Settings.MachineUrl, StringComparison.InvariantCultureIgnoreCase))) + { + _logger.LogError("Preflight Check failed, list {MachineUrl} not found in any repository", Settings.MachineUrl); + return false; + } + + if (!Version.TryParse(Settings.Version, out var version)) + { + _logger.LogError("Preflight Check failed, version {Version} was not valid", Settings.Version); + return false; + } + + return true; + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerSettingsVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerSettingsVM.cs new file mode 100644 index 000000000..08d4aa48c --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerSettingsVM.cs @@ -0,0 +1,152 @@ +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using Wabbajack.Common; +using Wabbajack.Compiler; +using Wabbajack.DTOs; +using Wabbajack.Paths; + +namespace Wabbajack; + +public class CompilerSettingsVM : ViewModel +{ + public CompilerSettingsVM() { } + public CompilerSettingsVM(CompilerSettings cs) + { + ModlistIsNSFW = cs.ModlistIsNSFW; + Source = cs.Source; + Downloads = cs.Downloads; + Game = cs.Game; + OutputFile = cs.OutputFile; + ModListImage = cs.ModListImage; + UseGamePaths = cs.UseGamePaths; + UseTextureRecompression = cs.UseTextureRecompression; + OtherGames = cs.OtherGames; + MaxVerificationTime = cs.MaxVerificationTime; + ModListName = cs.ModListName; + ModListAuthor = cs.ModListAuthor; + ModListDescription = cs.ModListDescription; + ModListReadme = cs.ModListReadme; + ModListWebsite = cs.ModListWebsite; + ModlistVersion = cs.ModlistVersion?.ToString() ?? ""; + MachineUrl = cs.MachineUrl; + Profile = cs.Profile; + AdditionalProfiles = cs.AdditionalProfiles; + NoMatchInclude = cs.NoMatchInclude.ToHashSet(); + Include = cs.Include.ToHashSet(); + Ignore = cs.Ignore.ToHashSet(); + AlwaysEnabled = cs.AlwaysEnabled.ToHashSet(); + Version = cs.Version?.ToString() ?? ""; + Description = cs.Description; + } + + [Reactive] public bool ModlistIsNSFW { get; set; } + [Reactive] public AbsolutePath Source { get; set; } + [Reactive] public AbsolutePath Downloads { get; set; } + [Reactive] public Game Game { get; set; } + [Reactive] public AbsolutePath OutputFile { get; set; } + + [Reactive] public AbsolutePath ModListImage { get; set; } + [Reactive] public bool UseGamePaths { get; set; } + + [Reactive] public bool UseTextureRecompression { get; set; } = false; + [Reactive] public Game[] OtherGames { get; set; } = Array.Empty(); + + [Reactive] public TimeSpan MaxVerificationTime { get; set; } = TimeSpan.FromMinutes(1); + [Reactive] public string ModListName { get; set; } = ""; + [Reactive] public string ModListAuthor { get; set; } = ""; + [Reactive] public string ModListDescription { get; set; } = ""; + [Reactive] public string ModListReadme { get; set; } = ""; + [Reactive] public Uri? ModListWebsite { get; set; } + [Reactive] public string ModlistVersion { get; set; } = ""; + [Reactive] public string MachineUrl { get; set; } = ""; + + /// + /// The main (default) profile + /// + [Reactive] public string Profile { get; set; } = ""; + + /// + /// Secondary profiles to include in the modlist + /// + [Reactive] public string[] AdditionalProfiles { get; set; } = Array.Empty(); + + + /// + /// All profiles to be added to the compiled modlist + /// + public IEnumerable AllProfiles => AdditionalProfiles.Append(Profile); + + public bool IsMO2Modlist => AllProfiles.Any(p => !string.IsNullOrWhiteSpace(p)); + + + + /// + /// This file, or files in these folders, are automatically included if they don't match + /// any other step + /// + [Reactive] public HashSet NoMatchInclude { get; set; } = new(); + + /// + /// These files are inlined into the modlist + /// + [Reactive] public HashSet Include { get; set; } = new(); + + /// + /// These files are ignored when compiling the modlist + /// + [Reactive] public HashSet Ignore { get; set; } = new(); + + [Reactive] public HashSet AlwaysEnabled { get; set; } = new(); + [Reactive] public string Version { get; set; } + [Reactive] public string Description { get; set; } + + public CompilerSettings ToCompilerSettings() + { + return new CompilerSettings() + { + ModlistIsNSFW = ModlistIsNSFW, + Source = Source, + Downloads = Downloads, + Game = Game, + OutputFile = OutputFile, + ModListImage = ModListImage, + UseGamePaths = UseGamePaths, + UseTextureRecompression = UseTextureRecompression, + OtherGames = OtherGames, + MaxVerificationTime = MaxVerificationTime, + ModListName = ModListName, + ModListAuthor = ModListAuthor, + ModListDescription = ModListDescription, + ModListReadme = ModListReadme, + ModListWebsite = ModListWebsite, + ModlistVersion = System.Version.Parse(ModlistVersion), + MachineUrl = MachineUrl, + Profile = Profile, + AdditionalProfiles = AdditionalProfiles, + NoMatchInclude = NoMatchInclude.ToArray(), + Include = Include.ToArray(), + Ignore = Ignore.ToArray(), + AlwaysEnabled = AlwaysEnabled.ToArray(), + Version = System.Version.Parse(Version), + Description = Description + }; + } + public AbsolutePath CompilerSettingsPath + { + get + { + if (Source == default || string.IsNullOrEmpty(Profile)) return default; + return Source.Combine(ModListName).WithExtension(Ext.CompilerSettings); + } + } + public AbsolutePath ProfilePath + { + get + { + if (Source == default || string.IsNullOrEmpty(Profile)) return default; + return Source.Combine("profiles").Combine(Profile).Combine("modlist").WithExtension(Ext.Txt); + } + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/FileTreeItemVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/FileTreeItemVM.cs new file mode 100644 index 000000000..98f5a7886 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/FileTreeItemVM.cs @@ -0,0 +1,102 @@ +using FluentIcons.Common; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows.Controls; +using Wabbajack.Paths; + +namespace Wabbajack; + +public enum CompilerFileState +{ + [Description("Auto Match")] + AutoMatch, + [Description("No Match Include")] + NoMatchInclude, + [Description("Force Include")] + Include, + [Description("Force Ignore")] + Ignore, + [Description("Always Enabled")] + AlwaysEnabled +} + +public class FileTreeViewItem : TreeViewItem +{ + public FileTreeViewItem(DirectoryInfo dir) + { + base.Header = new FileTreeItemVM(dir); + } + public FileTreeViewItem(FileInfo file) + { + base.Header = new FileTreeItemVM(file); + } + public new FileTreeItemVM Header => base.Header as FileTreeItemVM; + public static FileTreeViewItem Placeholder => default; +} + +/// +/// TODO: Bit of a super class for both files and folders atm, refactor? +/// +public class FileTreeItemVM : ReactiveObject, IDisposable +{ + private readonly CompositeDisposable _disposable = new(); + public FileSystemInfo Info { get; set; } + public bool IsDirectory { get; set; } + public Symbol Symbol { get; set; } + [Reactive] public CompilerFileState CompilerFileState { get; set; } + + public RelativePath PathRelativeToRoot { get; set; } + [Reactive] public bool SpecialFileState { get; set; } + [Reactive] public bool ContainsNoMatchIncludes { get; set; } + [Reactive] public bool ContainsIncludes { get; set; } + [Reactive] public bool ContainsIgnores { get; set; } + [Reactive] public bool ContainsAlwaysEnableds { get; set; } + + public FileTreeItemVM(DirectoryInfo info) + { + Info = info; + IsDirectory = true; + Symbol = Symbol.Folder; + this.WhenAnyValue(f => f.CompilerFileState) + .Select((x) => x != CompilerFileState.AutoMatch) + .BindToStrict(this, fti => fti.SpecialFileState) + .DisposeWith(_disposable); + } + public FileTreeItemVM(FileInfo info) + { + Info = info; + Symbol = info.Extension.ToLower() switch { + ".7z" or ".zip" or ".rar" or ".bsa" or ".ba2" or ".wabbajack" or ".tar" or ".tar.gz" => Symbol.Archive, + ".toml" or ".ini" or ".cfg" or ".json" or ".yaml" or ".xml" or ".yml" or ".meta" => Symbol.DocumentSettings, + ".txt" or ".md" or ".compiler_settings" or ".log" => Symbol.DocumentText, + ".dds" or ".jpg" or ".png" or ".webp" or ".svg" or ".xnb" => Symbol.DocumentImage, + ".hkx" => Symbol.DocumentPerson, + ".nif" or ".btr" => Symbol.DocumentCube, + ".mp3" or ".wav" or ".fuz" => Symbol.DocumentCatchUp, + ".js" => Symbol.DocumentJavascript, + ".java" => Symbol.DocumentJava, + ".pdf" => Symbol.DocumentPdf, + ".lua" or ".py" or ".bat" or ".reds" or ".psc" => Symbol.Receipt, + ".exe" => Symbol.ReceiptPlay, + ".esp" or ".esl" or ".esm" or ".archive" => Symbol.DocumentTable, + _ => Symbol.Document + }; + SpecialFileState = CompilerFileState != CompilerFileState.AutoMatch; + this.WhenAnyValue(f => f.CompilerFileState) + .Select((x) => x != CompilerFileState.AutoMatch) + .BindToStrict(this, fti => fti.SpecialFileState) + .DisposeWith(_disposable); + } + public override string ToString() => Info.Name; + public void Dispose() + { + GC.SuppressFinalize(this); + _disposable.Dispose(); + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/MO2CompilerVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/MO2CompilerVM.cs new file mode 100644 index 000000000..f617ebb4b --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/MO2CompilerVM.cs @@ -0,0 +1,37 @@ +using ReactiveUI.Fody.Helpers; +using System; +using System.Threading.Tasks; +using Wabbajack.Compiler; +using Wabbajack.DTOs; + +namespace Wabbajack; + +public class MO2CompilerVM : ViewModel +{ + public BaseCompilerVM Parent { get; } + + public FilePickerVM DownloadLocation { get; } + + public FilePickerVM ModListLocation { get; } + + [Reactive] + public ACompiler ActiveCompilation { get; private set; } + + [Reactive] + public object StatusTracker { get; private set; } + + public void Unload() + { + throw new NotImplementedException(); + } + + public IObservable CanCompile { get; } + public Task> Compile() + { + throw new NotImplementedException(); + } + + public MO2CompilerVM(BaseCompilerVM parent) + { + } +} diff --git a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml similarity index 90% rename from Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml rename to Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml index 54c7ffd2f..77e4784f1 100644 --- a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml +++ b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml @@ -1,13 +1,13 @@ - diff --git a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml.cs b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml.cs similarity index 87% rename from Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml.cs rename to Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml.cs index 9dcb281c5..190577200 100644 --- a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml.cs +++ b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml.cs @@ -1,8 +1,7 @@ using System.Reactive.Disposables; -using System.Windows.Controls; using ReactiveUI; -namespace Wabbajack.View_Models.Controls; +namespace Wabbajack.ViewModels.Controls; public partial class RemovableItemView : ReactiveUserControl { diff --git a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemViewModel.cs b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemViewModel.cs similarity index 78% rename from Wabbajack.App.Wpf/View Models/Controls/RemovableItemViewModel.cs rename to Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemViewModel.cs index b7ba65813..25a7d5075 100644 --- a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemViewModel.cs +++ b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemViewModel.cs @@ -1,7 +1,6 @@ using System; -using ReactiveUI.Fody.Helpers; -namespace Wabbajack.View_Models.Controls; +namespace Wabbajack.ViewModels.Controls; public class RemovableItemViewModel : ViewModel { diff --git a/Wabbajack.App.Wpf/ViewModels/Gallery/BaseModListMetadataVM.cs b/Wabbajack.App.Wpf/ViewModels/Gallery/BaseModListMetadataVM.cs new file mode 100644 index 000000000..27aaba5ff --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/BaseModListMetadataVM.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net.Http; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; +using System.Windows.Media.Imaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.DTOs; +using Wabbajack.DTOs.ModListValidation; +using Wabbajack.Messages; +using Wabbajack.Models; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Paths; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated.Services; + +namespace Wabbajack; + + +public readonly record struct ModListTag(string name) +{ + public string Name { get; } = name; + public override string ToString() => Name; +} + +public readonly record struct ModListMod(string name) +{ + public string Name { get; } = name; + public override string ToString() => Name; +} + +public class BaseModListMetadataVM : ViewModel +{ + public ModlistMetadata Metadata { get; } + public AbsolutePath Location { get; } + public LoadingLock LoadingImageLock { get; } = new(); + [Reactive] public HashSet ModListTagList { get; protected set; } + [Reactive] public Percent ProgressPercent { get; set; } + [Reactive] public bool IsBroken { get; protected set; } + [Reactive] public ModListStatus Status { get; set; } + [Reactive] public bool IsDownloading { get; protected set; } + [Reactive] public string DownloadSizeText { get; protected set; } + [Reactive] public string InstallSizeText { get; protected set; } + [Reactive] public string TotalSizeRequirementText { get; protected set; } + [Reactive] public string VersionText { get; protected set; } + [Reactive] public bool ImageContainsTitle { get; protected set; } + [Reactive] public GameMetaData GameMetaData { get; protected set; } + [Reactive] public bool DisplayVersionOnlyInInstallerView { get; protected set; } + [Reactive] public ICommand InstallCommand { get; protected set; } + + [Reactive] public IErrorResponse Error { get; protected set; } + + protected ObservableAsPropertyHelper _Image { get; set; } + public BitmapImage Image => _Image.Value; + + protected ObservableAsPropertyHelper _LoadingImage { get; set; } + public bool LoadingImage => _LoadingImage.Value; + + public ModListSummary? Summary { get; set; } + + protected Subject IsLoadingIdle; + protected readonly ILogger _logger; + protected readonly ModListDownloadMaintainer _maintainer; + protected readonly Client _wjClient; + protected readonly CancellationToken _cancellationToken; + protected readonly ServiceProvider _serviceProvider; + protected readonly ImageCacheManager _icm; + + public BaseModListMetadataVM(ILogger logger, ModlistMetadata metadata, + ModListDownloadMaintainer maintainer, ModListSummary? summary, Client wjClient, CancellationToken cancellationToken, HttpClient client, ImageCacheManager icm) + { + _logger = logger; + _maintainer = maintainer; + Metadata = metadata; + Summary = summary; + _wjClient = wjClient; + _cancellationToken = cancellationToken; + + GameMetaData = Metadata.Game.MetaData(); + Location = LauncherUpdater.CommonFolder.Value.Combine("downloaded_mod_lists", Metadata.NamespacedName).WithExtension(Ext.Wabbajack); + + UpdateStatus().FireAndForget(); + + ModListTagList = Metadata.Tags?.Select(tag => new ModListTag(tag)).ToHashSet(); + ModListTagList.Add(new ModListTag(GameMetaData.HumanFriendlyGameName)); + + DownloadSizeText = "Download size: " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfArchives); + InstallSizeText = "Installation size: " + UIUtils.FormatBytes(Metadata.DownloadMetadata.SizeOfInstalledFiles); + TotalSizeRequirementText = "Total size requirement: " + UIUtils.FormatBytes( Metadata.DownloadMetadata.TotalSize ); + VersionText = "Modlist version: " + Metadata.Version; + ImageContainsTitle = Metadata.ImageContainsTitle; + DisplayVersionOnlyInInstallerView = Metadata.DisplayVersionOnlyInInstallerView; + IsBroken = (Summary?.HasFailures ?? false) || metadata.ForceDown; + + IsLoadingIdle = new Subject(); + + var smallImageUri = UIUtils.GetSmallImageUri(metadata); + var imageObs = Observable.Return(smallImageUri) + .DownloadBitmapImage( + (ex) => _logger.LogError("Error downloading modlist image {Title} from {ImageUri}: {Exception}", + Metadata.Title, smallImageUri, ex.ToString()), LoadingImageLock, client, icm); + + _Image = imageObs + .ToGuiProperty(this, nameof(Image)) + .DisposeWith(CompositeDisposable); + + _LoadingImage = imageObs + .Select(x => false) + .StartWith(true) + .ToGuiProperty(this, nameof(LoadingImage)) + .DisposeWith(CompositeDisposable); + + InstallCommand = ReactiveCommand.CreateFromTask(async () => + { + if (await _maintainer.HaveModList(Metadata)) + { + Install(); + } + else + { + await Download(); + Install(); + } + }, LoadingLock.WhenAnyValue(ll => ll.IsLoading) + .CombineLatest(this.WhenAnyValue(vm => vm.IsBroken)) + .Select(v => !v.First && !v.Second)); + } + + private void Install() + { + LoadModlistForInstalling.Send(_maintainer.ModListPath(Metadata), Metadata); + NavigateToGlobal.Send(ScreenType.Installer); + ShowFloatingWindow.Send(FloatingScreenType.None); + } + + protected async Task Download() + { + try + { + Status = ModListStatus.Downloading; + + using var ll = LoadingLock.WithLoading(); + var (progress, task) = _maintainer.DownloadModlist(Metadata, _cancellationToken); + var dispose = progress + .BindToStrict(this, vm => vm.ProgressPercent); + try + { + await _wjClient.SendMetric("downloading", Metadata.Title); + await task; + await UpdateStatus(); + } + finally + { + dispose.Dispose(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "While downloading {Modlist}", Metadata.RepositoryName); + await UpdateStatus(); + } + } + + protected async Task UpdateStatus() + { + if (await _maintainer.HaveModList(Metadata)) + Status = ModListStatus.Downloaded; + else if (LoadingLock.IsLoading) + Status = ModListStatus.Downloading; + else + Status = ModListStatus.NotDownloaded; + } + + public enum ModListStatus + { + NotDownloaded, + Downloading, + Downloaded + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Gallery/GalleryModListMetadataVM.cs b/Wabbajack.App.Wpf/ViewModels/Gallery/GalleryModListMetadataVM.cs new file mode 100644 index 000000000..f5be33754 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/GalleryModListMetadataVM.cs @@ -0,0 +1,63 @@ +using System; +using System.Net.Http; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using System.Windows.Input; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using Wabbajack.DTOs; +using Wabbajack.Extensions; +using Wabbajack.Messages; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Services.OSIntegrated.Services; + +namespace Wabbajack; + +public class GalleryModListMetadataVM : BaseModListMetadataVM +{ + private ModListGalleryVM _parent; + + private readonly ObservableAsPropertyHelper _Exists; + public bool Exists => _Exists.Value; + + public ICommand DetailsCommand { get; set; } + public ICommand OpenWebsiteCommand { get; } + public ICommand ModListContentsCommend { get; } + + public GalleryModListMetadataVM(ILogger logger, ModListGalleryVM parent, ModlistMetadata metadata, + ModListDownloadMaintainer maintainer, ModListSummary? summary, Client wjClient, CancellationToken cancellationToken, HttpClient client, ImageCacheManager icm) : base(logger, metadata, maintainer, summary, wjClient, cancellationToken, client, icm) + { + _parent = parent; + _Exists = Observable.Interval(TimeSpan.FromSeconds(0.5)) + .Unit() + .StartWith(Unit.Default) + .FlowSwitch(_parent.WhenAny(x => x.IsActive)) + .SelectAsync(async _ => + { + try + { + return !IsDownloading && await maintainer.HaveModList(metadata); + } + catch (Exception) + { + return true; + } + }) + .ToGuiProperty(this, nameof(Exists)); + + // https://www.wabbajack.org/modlist/wj-featured/aldrnari + DetailsCommand = ReactiveCommand.Create(() => { + LoadModlistForDetails.Send(this); + ShowFloatingWindow.Send(FloatingScreenType.ModListDetails); + }); + OpenWebsiteCommand = ReactiveCommand.Create(() => UIUtils.OpenWebsite(new Uri($"https://www.wabbajack.org/modlist/{Metadata.NamespacedName}"))); + + ModListContentsCommend = ReactiveCommand.Create(async () => + { + UIUtils.OpenWebsite(new Uri($"https://www.wabbajack.org/search/{Metadata.NamespacedName}")); + }, IsLoadingIdle.StartWith(true)); + + + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Gallery/ModListGalleryVM.cs b/Wabbajack.App.Wpf/ViewModels/Gallery/ModListGalleryVM.cs new file mode 100644 index 000000000..e91a3c4ee --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/ModListGalleryVM.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Net.Http; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms.VisualStyles; +using System.Windows.Input; +using DynamicData; +using DynamicData.Binding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ReactiveMarbles.ObservableEvents; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.Downloaders.GameFile; +using Wabbajack.DTOs; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Services.OSIntegrated.Services; + +namespace Wabbajack; +public class ModListGalleryVM : BackNavigatingVM +{ + public class GameTypeEntry + { + public GameTypeEntry(GameMetaData gameMetaData, int amount) + { + GameMetaData = gameMetaData; + IsAllGamesEntry = gameMetaData == null; + GameIdentifier = IsAllGamesEntry ? ALL_GAME_IDENTIFIER : gameMetaData?.HumanFriendlyGameName; + Amount = amount; + FormattedName = IsAllGamesEntry ? $"{ALL_GAME_IDENTIFIER} ({Amount})" : $"{gameMetaData.HumanFriendlyGameName} ({Amount})"; + } + + public bool IsAllGamesEntry { get; set; } + public GameMetaData GameMetaData { get; private set; } + public int Amount { get; private set; } + public string FormattedName { get; private set; } + public string GameIdentifier { get; private set; } + public static GameTypeEntry GetAllGamesEntry(int amount) => new(null, amount); + } + + public MainWindowVM MWVM { get; } + + private bool _savingSettings = false; + private readonly SourceCache _modLists = new(x => x.Metadata.NamespacedName); + public ReadOnlyObservableCollection _filteredModLists; + + public ReadOnlyObservableCollection ModLists => _filteredModLists; + + private const string ALL_GAME_IDENTIFIER = "All games"; + + [Reactive] public IErrorResponse Error { get; set; } + + [Reactive] public string Search { get; set; } + + [Reactive] public bool OnlyInstalled { get; set; } + + [Reactive] public bool IncludeNSFW { get; set; } + + [Reactive] public bool IncludeUnofficial { get; set; } + + [Reactive] public string GameType { get; set; } + [Reactive] public double MinModlistSize { get; set; } + [Reactive] public double MaxModlistSize { get; set; } + + [Reactive] public HashSet AllTags { get; set; } = new(); + [Reactive] public ObservableCollection HasTags { get; set; } = new(); + + + [Reactive] public HashSet AllMods { get; set; } = new(); + [Reactive] public ObservableCollection HasMods { get; set; } = new(); + [Reactive] public Dictionary> ModsPerList { get; set; } = new(); + + [Reactive] public GalleryModListMetadataVM SmallestSizedModlist { get; set; } + [Reactive] public GalleryModListMetadataVM LargestSizedModlist { get; set; } + + [Reactive] public ObservableCollection GameTypeEntries { get; set; } + private bool _filteringOnGame; + private GameTypeEntry _selectedGameTypeEntry = null; + + public GameTypeEntry SelectedGameTypeEntry + { + get => _selectedGameTypeEntry; + set + { + RaiseAndSetIfChanged(ref _selectedGameTypeEntry, value ?? GameTypeEntries?.FirstOrDefault(gte => gte.IsAllGamesEntry)); + GameType = _selectedGameTypeEntry?.GameIdentifier; + } + } + + 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 readonly IServiceProvider _serviceProvider; + + public ICommand ResetFiltersCommand { get; set; } + + public ModListGalleryVM(ILogger logger, Client wjClient, GameLocator locator, + SettingsManager settingsManager, ModListDownloadMaintainer maintainer, CancellationToken cancellationToken, IServiceProvider serviceProvider) + : base(logger) + { + var searchThrottle = TimeSpan.FromSeconds(0.35); + _wjClient = wjClient; + _logger = logger; + _locator = locator; + _maintainer = maintainer; + _settingsManager = settingsManager; + _cancellationToken = cancellationToken; + _serviceProvider = serviceProvider; + + ResetFiltersCommand = ReactiveCommand.Create(() => { + OnlyInstalled = false; + IncludeNSFW = false; + IncludeUnofficial = false; + Search = string.Empty; + SelectedGameTypeEntry = GameTypeEntries?.FirstOrDefault(); + HasTags = new ObservableCollection(); + HasMods = new ObservableCollection(); + }); + + this.WhenActivated(disposables => + { + LoadModLists().FireAndForget(); + LoadSettings().FireAndForget(); + + this.WhenAnyValue(x => x.IncludeNSFW, x => x.IncludeUnofficial, x => x.OnlyInstalled, x => x.GameType) + .Subscribe(_ => SaveSettings().FireAndForget()) + .DisposeWith(disposables); + + var searchTextPredicates = this.ObservableForProperty(vm => vm.Search) + .Throttle(searchThrottle, RxApp.MainThreadScheduler) + .Select(change => change.Value?.Trim() ?? "") + .StartWith(Search) + .Select>(txt => + { + if (string.IsNullOrWhiteSpace(txt)) return _ => true; + return item => item.Metadata.Title.ContainsCaseInsensitive(txt) || + item.Metadata.Description.ContainsCaseInsensitive(txt) || + item.Metadata.Tags.Contains(txt); + }); + + var onlyInstalledGamesFilter = this.ObservableForProperty(vm => vm.OnlyInstalled) + .Select(v => v.Value) + .Select>(onlyInstalled => + { + if (onlyInstalled == false) return _ => true; + return item => _locator.IsInstalled(item.Metadata.Game); + }) + .StartWith(_ => true); + + var includeUnofficialFilter = this.ObservableForProperty(vm => vm.IncludeUnofficial) + .Select(v => v.Value) + .StartWith(IncludeUnofficial) + .Select>(unoffical => + { + if (unoffical) return x => true; + return x => x.Metadata.Official; + }); + + var includeNSFWFilter = this.ObservableForProperty(vm => vm.IncludeNSFW) + .Select(v => v.Value) + .StartWith(IncludeNSFW) + .Select>(showNsfw => + { + if (showNsfw) return x => true; + return x => !x.Metadata.NSFW; + }); + + var gameFilter = this.ObservableForProperty(vm => vm.GameType) + .Select(v => v.Value) + .Select>(selected => + { + _filteringOnGame = true; + if (selected is null or ALL_GAME_IDENTIFIER) return _ => true; + return item => item.Metadata.Game.MetaData().HumanFriendlyGameName == selected; + }) + .StartWith(_ => true); + + var minModlistSizeFilter = this.ObservableForProperty(vm => vm.MinModlistSize) + .Throttle(TimeSpan.FromSeconds(0.05), RxApp.MainThreadScheduler) + .Select(v => v.Value) + .Select>(minModlistSize => + { + return item => item.Metadata.DownloadMetadata.TotalSize >= minModlistSize; + }); + + var maxModlistSizeFilter = this.ObservableForProperty(vm => vm.MaxModlistSize) + .Throttle(TimeSpan.FromSeconds(0.05), RxApp.MainThreadScheduler) + .Select(v => v.Value) + .Select>(maxModlistSize => + { + return item => item.Metadata.DownloadMetadata.TotalSize <= maxModlistSize; + }); + + var includedTagsFilter = this.ObservableForProperty(vm => vm.HasTags) + .Select(v => v.Value) + .Select, Func>(filteredTags => + { + if(!filteredTags?.Any() ?? true) return _ => true; + + return item => filteredTags.All(tag => item.Metadata.Tags.Contains(tag.Name)); + }) + .StartWith(_ => true); + + var includedModsFilter = this.ObservableForProperty(vm => vm.HasMods) + .Select(v => v.Value) + .Select, Func>(filteredMods => + { + if(!filteredMods?.Any() ?? true) return _ => true; + + return item => + ModsPerList.TryGetValue(item.Metadata.Links.MachineURL, out var mods) && filteredMods.All(mod => mods.Contains(mod.Name)); + }) + .StartWith(_ => true); + + + var searchSorter = this.WhenValueChanged(vm => vm.Search) + .Throttle(searchThrottle, RxApp.MainThreadScheduler) + .Select(s => SortExpressionComparer + .Descending(m => m.Metadata.Title.StartsWith(s ?? "", StringComparison.InvariantCultureIgnoreCase)) + .ThenByDescending(m => m.Metadata.Title.Contains(s ?? "", StringComparison.InvariantCultureIgnoreCase)) + .ThenByDescending(m => !m.IsBroken)); + _modLists.Connect() + .ObserveOn(RxApp.MainThreadScheduler) + .Filter(searchTextPredicates) + .Filter(onlyInstalledGamesFilter) + .Filter(includeUnofficialFilter) + .Filter(includeNSFWFilter) + .Filter(gameFilter) + .Filter(minModlistSizeFilter) + .Filter(maxModlistSizeFilter) + .Filter(includedTagsFilter) + .Filter(includedModsFilter) + .Sort(searchSorter) + .TreatMovesAsRemoveAdd() + .Bind(out _filteredModLists) + .Subscribe(_ => + { + if (!_filteringOnGame) + { + var previousGameType = GameType; + SelectedGameTypeEntry = null; + GameTypeEntries = GetGameTypeEntries(); + var nextEntry = GameTypeEntries.FirstOrDefault(gte => previousGameType == gte.GameIdentifier); + SelectedGameTypeEntry = nextEntry ?? GameTypeEntries.FirstOrDefault(gte => GameType == ALL_GAME_IDENTIFIER); + } + + _filteringOnGame = false; + }) + .DisposeWith(disposables); + }); + } + + public override void Unload() + { + Error = null; + } + + private async Task SaveSettings() + { + if (_savingSettings) return; + + _savingSettings = true; + await _settingsManager.Save("modlist_gallery", new GalleryFilterSettings + { + GameType = GameType, + IncludeNSFW = IncludeNSFW, + IncludeUnofficial = IncludeUnofficial, + OnlyInstalled = OnlyInstalled, + }); + _savingSettings = false; + } + + private async Task LoadSettings() + { + using var ll = LoadingLock.WithLoading(); + RxApp.MainThreadScheduler.Schedule(await _settingsManager.Load("modlist_gallery"), + (_, s) => + { + SelectedGameTypeEntry = GameTypeEntries?.FirstOrDefault(gte => gte.GameIdentifier.Equals(s.GameType)); + IncludeNSFW = s.IncludeNSFW; + IncludeUnofficial = s.IncludeUnofficial; + OnlyInstalled = s.OnlyInstalled; + return Disposable.Empty; + }); + } + + private async Task LoadModLists() + { + using var ll = LoadingLock.WithLoading(); + try + { + var allowedTags = await _wjClient.LoadAllowedTags(); + AllTags = allowedTags.Select(t => new ModListTag(t)) + .OrderBy(t => t.Name) + .Prepend(new ModListTag("NSFW")) + .Prepend(new ModListTag("Featured")) + .ToHashSet(); + var searchIndex = await _wjClient.LoadSearchIndex(); + ModsPerList = searchIndex.ModsPerList; + AllMods = searchIndex.AllMods.Select(mod => new ModListMod(mod)).ToHashSet(); + var modLists = await _wjClient.LoadLists(); + var modlistSummaries = (await _wjClient.GetListStatuses()).ToDictionary(summary => summary.MachineURL); + var httpClient = _serviceProvider.GetRequiredService(); + var cacheManager = _serviceProvider.GetRequiredService(); + foreach (var modlist in modLists) + { + modlist.Tags = modlist.Tags.Where(allowedTags.Contains).ToList(); + if (modlist.NSFW) modlist.Tags.Add("NSFW"); + if (modlist.Official) modlist.Tags.Add("Featured"); + } + _modLists.Edit(e => + { + e.Clear(); + e.AddOrUpdate(modLists.Select(m => + new GalleryModListMetadataVM(_logger, this, m, _maintainer, modlistSummaries.TryGetValue(m.Links.MachineURL, out var summary) ? summary : null, _wjClient, _cancellationToken, + httpClient, cacheManager))); + }); + DetermineListSizeRange(); + } + catch (Exception ex) + { + _logger.LogError(ex, "While loading lists"); + ll.Fail(); + } + ll.Succeed(); + } + + private void DetermineListSizeRange() + { + SmallestSizedModlist = null; + LargestSizedModlist = null; + foreach(var item in _modLists.Items) + { + if (SmallestSizedModlist == null) SmallestSizedModlist = item; + if (LargestSizedModlist == null) LargestSizedModlist = item; + + var itemTotalSize = item.Metadata.DownloadMetadata.TotalSize; + var smallestSize = SmallestSizedModlist.Metadata.DownloadMetadata.TotalSize; + var largestSize = LargestSizedModlist.Metadata.DownloadMetadata.TotalSize; + + if (itemTotalSize < smallestSize) SmallestSizedModlist = item; + + if (itemTotalSize > largestSize) LargestSizedModlist = item; + } + MinModlistSize = SmallestSizedModlist.Metadata.DownloadMetadata.TotalSize; + MaxModlistSize = LargestSizedModlist.Metadata.DownloadMetadata.TotalSize; + } + + private ObservableCollection GetGameTypeEntries() + { + return new(ModLists.Select(fm => fm.Metadata) + .GroupBy(m => m.Game) + .Select(g => new GameTypeEntry(g.Key.MetaData(), g.Count())) + .OrderBy(gte => gte.GameMetaData.HumanFriendlyGameName) + .Prepend(GameTypeEntry.GetAllGamesEntry(ModLists.Count)) + .ToList()); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/ViewModels/GameVM.cs b/Wabbajack.App.Wpf/ViewModels/GameVM.cs new file mode 100644 index 000000000..096b090ae --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/GameVM.cs @@ -0,0 +1,15 @@ +using Wabbajack.DTOs; + +namespace Wabbajack; + +public class GameVM +{ + public Game Game { get; } + public string DisplayName { get; } + + public GameVM(Game game) + { + Game = game; + DisplayName = game.MetaData().HumanFriendlyGameName; + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/HomeVM.cs b/Wabbajack.App.Wpf/ViewModels/HomeVM.cs new file mode 100644 index 000000000..fedb0fa73 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/HomeVM.cs @@ -0,0 +1,53 @@ +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Reactive; +using System.Windows.Input; +using Wabbajack.Common; +using Wabbajack.Messages; +using Wabbajack.Networking.WabbajackClientApi; +using System.Threading.Tasks; +using Wabbajack.DTOs; +using Microsoft.Extensions.Logging; +using System.Diagnostics; + +namespace Wabbajack; + +public class HomeVM : ViewModel, IHasInfoVM +{ + private readonly ILogger _logger; + private readonly Client _wjClient; + + public HomeVM(ILogger logger, Client wjClient) + { + _logger = logger; + _wjClient = wjClient; + BrowseCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(ScreenType.ModListGallery)); + InfoCommand = ReactiveCommand.Create(() => Process.Start(new ProcessStartInfo("https://wiki.wabbajack.org/") { UseShellExecute = true })); + VisitModlistWizardCommand = ReactiveCommand.Create(() => Process.Start(new ProcessStartInfo(Consts.WabbajackModlistWizardUri.ToString()) { UseShellExecute = true })); + LoadModLists().FireAndForget(); + } + private async Task LoadModLists() + { + using var ll = LoadingLock.WithLoading(); + try + { + Modlists = await _wjClient.LoadLists(); + } + catch (Exception ex) + { + _logger.LogError(ex, "While loading lists"); + ll.Fail(); + } + ll.Succeed(); + } + + public ICommand VisitModlistWizardCommand { get; } + public ICommand BrowseCommand { get; } + public ReactiveCommand UpdateCommand { get; } + + [Reactive] + public ModlistMetadata[] Modlists { get; private set; } + + public ICommand InfoCommand { get; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/InfoVM.cs b/Wabbajack.App.Wpf/ViewModels/InfoVM.cs new file mode 100644 index 000000000..f48b0dfc9 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/InfoVM.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Reactive.Disposables; +using Wabbajack.Messages; + +namespace Wabbajack; + +public class InfoVM : BackNavigatingVM +{ + public InfoVM(ILogger logger) : base(logger) + { + MessageBus.Current.Listen() + .Subscribe(msg => { + Info = msg.Info; + NavigateBackTarget = msg.NavigateBackTarget; + BackCommand = ReactiveCommand.Create(() => NavigateTo.Send(NavigateBackTarget)); + }) + .DisposeWith(CompositeDisposable); + } + [Reactive] public string Info { get; set; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Installers/ISubInstallerVM.cs b/Wabbajack.App.Wpf/ViewModels/Installers/ISubInstallerVM.cs new file mode 100644 index 000000000..48a3d654a --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Installers/ISubInstallerVM.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Wabbajack.Installer; +using Wabbajack.DTOs.Interventions; + +namespace Wabbajack; + +public interface ISubInstallerVM +{ + InstallationVM Parent { get; } + IInstaller ActiveInstallation { get; } + void Unload(); + bool SupportsAfterInstallNavigation { get; } + void AfterInstallNavigation(); + int ConfigVisualVerticalOffset { get; } + ErrorResponse CanInstall { get; } + Task Install(); + IUserIntervention InterventionConverter(IUserIntervention intervention); +} diff --git a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs b/Wabbajack.App.Wpf/ViewModels/Installers/InstallationVM.cs similarity index 81% rename from Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs rename to Wabbajack.App.Wpf/ViewModels/Installers/InstallationVM.cs index 99918e1bc..a307316a9 100644 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ b/Wabbajack.App.Wpf/ViewModels/Installers/InstallationVM.cs @@ -15,7 +15,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Shell; -using System.Windows.Threading; using Microsoft.Extensions.Logging; using Microsoft.WindowsAPICodePack.Dialogs; using Wabbajack.Common; @@ -34,18 +33,14 @@ using Wabbajack.Paths.IO; using Wabbajack.Services.OSIntegrated; using Wabbajack.Util; -using System.Windows.Forms; -using Microsoft.Extensions.DependencyInjection; using Wabbajack.CLI.Verbs; +using Microsoft.Extensions.DependencyInjection; using Wabbajack.VFS; +using Humanizer; +using System.Text.RegularExpressions; namespace Wabbajack; -public enum ModManager -{ - Standard -} - public enum InstallState { Configuration, @@ -54,30 +49,21 @@ public enum InstallState Failure } -public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM +public class InstallationVM : ProgressViewModel { private const string LastLoadedModlist = "last-loaded-modlist"; private const string InstallSettingsPrefix = "install-settings-"; - private Random _random = new(); + private readonly Random _random = new(); - [Reactive] - public Percent StatusProgress { get; set; } - - [Reactive] - public string StatusText { get; set; } - [Reactive] public ModList ModList { get; set; } [Reactive] public ModlistMetadata ModlistMetadata { get; set; } - - [Reactive] - public ErrorResponse? Completed { get; set; } [Reactive] - public FilePickerVM ModListLocation { get; set; } + public FilePickerVM WabbajackFileLocation { get; set; } [Reactive] public MO2InstallerVM Installer { get; set; } @@ -92,12 +78,6 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM [Reactive] public InstallState InstallState { get; set; } - - [Reactive] - protected ErrorResponse[] Errors { get; private set; } - - [Reactive] - public ErrorResponse Error { get; private set; } /// /// Slideshow Data @@ -111,9 +91,15 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM [Reactive] public string SlideShowDescription { get; set; } + [Reactive] + public string SuggestedInstallFolder { get; set; } + + [Reactive] + public string SuggestedDownloadFolder { get; set; } + private readonly DTOSerializer _dtos; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly SettingsManager _settingsManager; private readonly IServiceProvider _serviceProvider; private readonly SystemParametersConstructor _parametersConstructor; @@ -153,13 +139,12 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM public ReactiveCommand OpenLogsCommand { get; } public ReactiveCommand GoToInstallCommand { get; } public ReactiveCommand BeginCommand { get; } - public ReactiveCommand VerifyCommand { get; } - public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, IServiceProvider serviceProvider, + public InstallationVM(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, - CancellationToken cancellationToken) : base(logger) + CancellationToken cancellationToken) { _logger = logger; _configuration = configuration; @@ -175,13 +160,16 @@ public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsMana _logins = logins; _cancellationToken = cancellationToken; + ConfigurationText = $"Setup"; + ProgressText = $"Installation"; + Installer = new MO2InstallerVM(this); +/* + ConfigurationText = "Installation Setup"; + ProgressText = "Installing..."; + */ - BackCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView)); - BeginCommand = ReactiveCommand.Create(() => BeginInstall().FireAndForget()); - - VerifyCommand = ReactiveCommand.Create(() => Verify().FireAndForget()); OpenReadmeCommand = ReactiveCommand.Create(() => { @@ -198,13 +186,13 @@ public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsMana UIUtils.OpenWebsite(ModList!.Website); }, LoadingLock.IsNotLoadingObservable); - ModListLocation = new FilePickerVM + WabbajackFileLocation = new FilePickerVM { ExistCheckOption = FilePickerVM.CheckOptions.On, PathType = FilePickerVM.PathTypeOptions.File, PromptTitle = "Select a ModList to install" }; - ModListLocation.Filters.Add(new CommonFileDialogFilter("Wabbajack Modlist", "*.wabbajack")); + WabbajackFileLocation.Filters.Add(new CommonFileDialogFilter("Wabbajack Modlist", "*.wabbajack")); OpenLogsCommand = ReactiveCommand.Create(() => { @@ -250,19 +238,43 @@ public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsMana this.WhenActivated(disposables => { - ModListLocation.WhenAnyValue(l => l.TargetPath) + WabbajackFileLocation.WhenAnyValue(l => l.TargetPath) .Subscribe(p => LoadModlist(p, null).FireAndForget()) .DisposeWith(disposables); + this.WhenAnyValue(x => x.ModlistMetadata) + .ObserveOn(RxApp.TaskpoolScheduler) + .Select(x => + { + var folderName = x.Title; + // Ignore everything after a dash + folderName = folderName.Split('-')[0]; + // Remove all special characters + folderName = Regex.Replace(folderName, "[^a-zA-Z0-9_ .]+", ""); + // Get preferred installation drive (SSD with enough space) + var preferredPartition = DriveHelper.GetPreferredInstallationDrive(x.DownloadMetadata.SizeOfInstalledFiles); + var words = folderName.Split(' '); + // Abbreviate the list name if it's too long, otherwise convert it to PascalCase + folderName = words.Length >= 3 ? string.Join("", words.Select(w => w[0])).ToUpper() : folderName.Pascalize(); + + return $"{preferredPartition.Name}Modlists\\{folderName.Trim()}\\"; + }) + .Subscribe(x => { + SuggestedInstallFolder = x; + SuggestedDownloadFolder = $"{x}\\Downloads\\"; + }) + .DisposeWith(disposables); + + var token = new CancellationTokenSource(); BeginSlideShow(token.Token).FireAndForget(); Disposable.Create(() => token.Cancel()) .DisposeWith(disposables); - this.WhenAny(vm => vm.ModListLocation.ErrorState) + this.WhenAny(vm => vm.WabbajackFileLocation.ErrorState) .CombineLatest(this.WhenAny(vm => vm.Installer.DownloadLocation.ErrorState), this.WhenAny(vm => vm.Installer.Location.ErrorState), - this.WhenAny(vm => vm.ModListLocation.TargetPath), + this.WhenAny(vm => vm.WabbajackFileLocation.TargetPath), this.WhenAny(vm => vm.Installer.Location.TargetPath), this.WhenAny(vm => vm.Installer.DownloadLocation.TargetPath)) .Select(t => @@ -276,13 +288,33 @@ public InstallerVM(ILogger logger, DTOSerializer dtos, SettingsMana }) .BindTo(this, vm => vm.ErrorState) .DisposeWith(disposables); + + this.WhenAny(vm => vm.InstallState) + .Subscribe(state => + { + CurrentStep = state switch + { + InstallState.Configuration => Step.Configuration, + InstallState.Installing => Step.Busy, + InstallState.Failure => Step.Configuration, + InstallState.Success => Step.Done, + _ => Step.Configuration + }; + ProgressState = state switch + { + InstallState.Success => ProgressState.Success, + InstallState.Failure => ProgressState.Error, + _ => ProgressState.Normal + }; + }) + .DisposeWith(disposables); }); } private IEnumerable Validate() { - if (!ModListLocation.TargetPath.FileExists()) + if (!WabbajackFileLocation.TargetPath.FileExists()) yield return ErrorResponse.Fail("Mod list source does not exist"); var downloadPath = Installer.DownloadLocation.TargetPath; @@ -326,7 +358,7 @@ private IEnumerable Validate() yield return ErrorResponse.Fail("Installing in this folder may overwrite Wabbajack"); } - if (installPath.ToString().Length != 0 && installPath != LastInstallPath && !OverwriteFiles && + if (installPath.ToString().Length != 0 && installPath != LastInstallPath && !OverwriteFiles && installPath.DirectoryExists() && Directory.EnumerateFileSystemEntries(installPath.ToString()).Any()) { yield return ErrorResponse.Fail("There are files in the install folder, please tick 'Overwrite Installation' to confirm you want to install to this folder " + Environment.NewLine + @@ -390,13 +422,13 @@ private async Task LoadLastModlist() var lst = await _settingsManager.Load(LastLoadedModlist); if (lst.FileExists()) { - ModListLocation.TargetPath = lst; + WabbajackFileLocation.TargetPath = lst; } } private async Task LoadModlistFromGallery(AbsolutePath path, ModlistMetadata metadata) { - ModListLocation.TargetPath = path; + WabbajackFileLocation.TargetPath = path; ModlistMetadata = metadata; } @@ -404,20 +436,17 @@ private async Task LoadModlist(AbsolutePath path, ModlistMetadata? metadata) { using var ll = LoadingLock.WithLoading(); InstallState = InstallState.Configuration; - ModListLocation.TargetPath = path; + WabbajackFileLocation.TargetPath = path; try { ModList = await StandardInstaller.LoadFromFile(_dtos, path); ModListImage = BitmapFrame.Create(await StandardInstaller.ModListImageStream(path)); - - if (!string.IsNullOrWhiteSpace(ModList.Readme)) - UIUtils.OpenWebsite(new Uri(ModList.Readme)); - - StatusText = $"Install configuration for {ModList.Name}"; + ConfigurationText = $"Setup - {ModlistMetadata.Title}"; + ProgressText = $"Installation"; TaskBarUpdate.Send($"Loaded {ModList.Name}", TaskbarItemProgressState.Normal); - var hex = (await ModListLocation.TargetPath.ToString().Hash()).ToHex(); + var hex = (await WabbajackFileLocation.TargetPath.ToString().Hash()).ToHex(); var prevSettings = await _settingsManager.Load(InstallSettingsPrefix + hex); if (path.WithExtension(Ext.MetaData).FileExists()) @@ -436,7 +465,7 @@ private async Task LoadModlist(AbsolutePath path, ModlistMetadata? metadata) if (prevSettings.ModListLocation == path) { - ModListLocation.TargetPath = prevSettings.ModListLocation; + WabbajackFileLocation.TargetPath = prevSettings.ModListLocation; LastInstallPath = prevSettings.InstallLocation; Installer.Location.TargetPath = prevSettings.InstallLocation; Installer.DownloadLocation.TargetPath = prevSettings.DownloadLoadction; @@ -467,29 +496,28 @@ private async Task Verify() await Task.Run(async () => { InstallState = InstallState.Installing; - - StatusText = $"Verifying {ModList.Name}"; - - - var cmd = new VerifyModlistInstall(_serviceProvider.GetRequiredService>(), _dtos, - _serviceProvider.GetRequiredService>(), + + ProgressText = $"Verifying {ModList.Name}"; + + + var cmd = new VerifyModlistInstall(_serviceProvider.GetRequiredService>(), _dtos, + _serviceProvider.GetRequiredService>(), _serviceProvider.GetRequiredService()); - - var result = await cmd.Run(ModListLocation.TargetPath, Installer.Location.TargetPath, _cancellationToken); - + + var result = await cmd.Run(WabbajackFileLocation.TargetPath, Installer.Location.TargetPath, _cancellationToken); + if (result != 0) { TaskBarUpdate.Send($"Error during verification of {ModList.Name}", TaskbarItemProgressState.Error); InstallState = InstallState.Failure; - StatusText = $"Error during install of {ModList.Name}"; - StatusProgress = Percent.Zero; + ProgressText = $"Error during install of {ModList.Name}"; + ProgressPercent = Percent.Zero; } else { TaskBarUpdate.Send($"Finished verification of {ModList.Name}", TaskbarItemProgressState.Normal); InstallState = InstallState.Success; } - }); } @@ -529,15 +557,15 @@ await Task.Run(async () => } - var postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex(); + var postfix = (await WabbajackFileLocation.TargetPath.ToString().Hash()).ToHex(); await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings { - ModListLocation = ModListLocation.TargetPath, + ModListLocation = WabbajackFileLocation.TargetPath, InstallLocation = Installer.Location.TargetPath, DownloadLoadction = Installer.DownloadLocation.TargetPath, Metadata = ModlistMetadata }); - await _settingsManager.Save(LastLoadedModlist, ModListLocation.TargetPath); + await _settingsManager.Save(LastLoadedModlist, WabbajackFileLocation.TargetPath); try { @@ -547,7 +575,7 @@ await Task.Run(async () => Downloads = Installer.DownloadLocation.TargetPath, Install = Installer.Location.TargetPath, ModList = ModList, - ModlistArchive = ModListLocation.TargetPath, + ModlistArchive = WabbajackFileLocation.TargetPath, SystemParameters = _parametersConstructor.Create(), GameFolder = _gameLocator.GameLocation(ModList.GameType) }); @@ -555,8 +583,8 @@ await Task.Run(async () => installer.OnStatusUpdate = update => { - StatusText = update.StatusText; - StatusProgress = update.StepsProgress; + ProgressText = update.StatusText; + ProgressPercent = update.StepsProgress; TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate, update.StepsProgress.Value); @@ -566,8 +594,8 @@ await Task.Run(async () => { TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error); InstallState = InstallState.Failure; - StatusText = $"Error during install of {ModList.Name}"; - StatusProgress = Percent.Zero; + ProgressText = $"Error during install of {ModList.Name}"; + ProgressPercent = Percent.Zero; } else { @@ -584,8 +612,8 @@ await Task.Run(async () => TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error); _logger.LogError(ex, ex.Message); InstallState = InstallState.Failure; - StatusText = $"Error during install of {ModList.Name}"; - StatusProgress = Percent.Zero; + ProgressText = $"Error during install of {ModList.Name}"; + ProgressPercent = Percent.Zero; } }); diff --git a/Wabbajack.App.Wpf/ViewModels/Installers/MO2InstallerVM.cs b/Wabbajack.App.Wpf/ViewModels/Installers/MO2InstallerVM.cs new file mode 100644 index 000000000..c97c64b4b --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Installers/MO2InstallerVM.cs @@ -0,0 +1,125 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Installer; +using Wabbajack.DTOs.Interventions; +using Wabbajack.Paths; + +namespace Wabbajack; + +public class MO2InstallerVM : ViewModel, ISubInstallerVM +{ + public InstallationVM Parent { get; } + + [Reactive] + public ErrorResponse CanInstall { get; set; } + + [Reactive] + public IInstaller ActiveInstallation { get; private set; } + + [Reactive] + public Mo2ModlistInstallationSettings CurrentSettings { get; set; } + + public FilePickerVM Location { get; } + + public FilePickerVM DownloadLocation { get; } + + public bool SupportsAfterInstallNavigation => true; + + [Reactive] + public bool AutomaticallyOverwrite { get; set; } + + public int ConfigVisualVerticalOffset => 25; + + public MO2InstallerVM(InstallationVM installerVM) + { + Parent = installerVM; + + Location = new FilePickerVM() + { + ExistCheckOption = FilePickerVM.CheckOptions.Off, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select Installation Directory", + }; + Location.WhenAnyValue(t => t.TargetPath) + .Subscribe(newPath => + { + if (newPath != default && DownloadLocation!.TargetPath == AbsolutePath.Empty) + { + DownloadLocation.TargetPath = newPath.Combine("downloads"); + } + }).DisposeWith(CompositeDisposable); + + DownloadLocation = new FilePickerVM() + { + ExistCheckOption = FilePickerVM.CheckOptions.Off, + PathType = FilePickerVM.PathTypeOptions.Folder, + PromptTitle = "Select a location for MO2 downloads", + }; + } + + public void Unload() + { + SaveSettings(this.CurrentSettings); + } + + private void SaveSettings(Mo2ModlistInstallationSettings settings) + { + //Parent.MWVM.Settings.Installer.LastInstalledListLocation = Parent.ModListLocation.TargetPath; + if (settings == null) return; + settings.InstallationLocation = Location.TargetPath; + settings.DownloadLocation = DownloadLocation.TargetPath; + settings.AutomaticallyOverrideExistingInstall = AutomaticallyOverwrite; + } + + public void AfterInstallNavigation() + { + Process.Start("explorer.exe", Location.TargetPath.ToString()); + } + + public async Task Install() + { + /* + using (var installer = new MO2Installer( + archive: Parent.ModListLocation.TargetPath, + modList: Parent.ModList.SourceModList, + outputFolder: Location.TargetPath, + downloadFolder: DownloadLocation.TargetPath, + parameters: SystemParametersConstructor.Create())) + { + installer.Metadata = Parent.ModList.SourceModListMetadata; + installer.UseCompression = Parent.MWVM.Settings.Filters.UseCompression; + Parent.MWVM.Settings.Performance.SetProcessorSettings(installer); + + return await Task.Run(async () => + { + try + { + var workTask = installer.Begin(); + ActiveInstallation = installer; + return await workTask; + } + finally + { + ActiveInstallation = null; + } + }); + } + */ + return true; + } + + public IUserIntervention InterventionConverter(IUserIntervention intervention) + { + switch (intervention) + { + case ConfirmUpdateOfExistingInstall confirm: + return new ConfirmUpdateOfExistingInstallVM(this, confirm); + default: + return intervention; + } + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Interfaces/ICpuStatusVM.cs b/Wabbajack.App.Wpf/ViewModels/Interfaces/ICpuStatusVM.cs new file mode 100644 index 000000000..08ea26d79 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Interfaces/ICpuStatusVM.cs @@ -0,0 +1,9 @@ +using System.Collections.ObjectModel; +using ReactiveUI; + +namespace Wabbajack; + +public interface ICpuStatusVM : IReactiveObject +{ + ReadOnlyObservableCollection StatusList { get; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Interfaces/IHasInfoVM.cs b/Wabbajack.App.Wpf/ViewModels/Interfaces/IHasInfoVM.cs new file mode 100644 index 000000000..ba50cb347 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Interfaces/IHasInfoVM.cs @@ -0,0 +1,8 @@ +using System.Windows.Input; + +namespace Wabbajack; + +public interface IHasInfoVM +{ + public ICommand InfoCommand { get; } +} diff --git a/Wabbajack.App.Wpf/View Models/Interfaces/INeedsLoginCredentials.cs b/Wabbajack.App.Wpf/ViewModels/Interfaces/INeedsLoginCredentials.cs similarity index 100% rename from Wabbajack.App.Wpf/View Models/Interfaces/INeedsLoginCredentials.cs rename to Wabbajack.App.Wpf/ViewModels/Interfaces/INeedsLoginCredentials.cs diff --git a/Wabbajack.App.Wpf/ViewModels/Interfaces/IProgressVM.cs b/Wabbajack.App.Wpf/ViewModels/Interfaces/IProgressVM.cs new file mode 100644 index 000000000..374618958 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Interfaces/IProgressVM.cs @@ -0,0 +1,25 @@ +using Wabbajack.RateLimiter; + +namespace Wabbajack; + +public enum Step +{ + Configuration, // Configuration is enlarged + Busy, // Progress bar is enlarged + Done // Both are same size +} +public enum ProgressState +{ + Normal, // Progress bar is not highlighted + Success, // Operation succeeded, progress bar gets highlighted + Error // Operation failed, progress bar gets highlighted +} + +public interface IProgressVM +{ + public Step CurrentStep { get; set; } + public ProgressState ProgressState { get; set; } + public string ConfigurationText { get; set; } + public string ProgressText { get; set; } + public Percent ProgressPercent { get; set; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs b/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs new file mode 100644 index 000000000..7ff856afa --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs @@ -0,0 +1,333 @@ +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Orc.FileAssociation; +using Wabbajack.Common; +using Wabbajack.DTOs.Interventions; +using Wabbajack.Interventions; +using Wabbajack.Messages; +using Wabbajack.Models; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using Wabbajack.UserIntervention; +using Wabbajack.ViewModels; + +namespace Wabbajack; + +/// +/// Main View Model for the application. +/// Keeps track of which sub view is being shown in the window, and has some singleton wiring like WorkQueue and Logging. +/// +public class MainWindowVM : ViewModel +{ + public MainWindow MainWindow { get; } + + [Reactive] + public ViewModel ActivePane { get; private set; } + + [Reactive] + public ViewModel? ActiveFloatingPane { get; private set; } = null; + + [Reactive] + public NavigationVM NavigationVM { get; private set; } + + public ObservableCollectionExtended Log { get; } = new ObservableCollectionExtended(); + + public readonly CompilerHomeVM CompilerHomeVM; + public readonly CompilerDetailsVM CompilerDetailsVM; + public readonly CompilerFileManagerVM CompilerFileManagerVM; + public readonly CompilerMainVM CompilerMainVM; + public readonly InstallationVM InstallerVM; + public readonly SettingsVM SettingsPaneVM; + public readonly ModListGalleryVM GalleryVM; + public readonly HomeVM HomeVM; + public readonly WebBrowserVM WebBrowserVM; + public readonly ModListDetailsVM ModListDetailsVM; + public readonly InfoVM InfoVM; + public readonly UserInterventionHandlers UserInterventionHandlers; + private readonly Client _wjClient; + private readonly ILogger _logger; + private readonly ResourceMonitor _resourceMonitor; + + private List PreviousPanes = new(); + private readonly IServiceProvider _serviceProvider; + + public ICommand CopyVersionCommand { get; } + public ICommand ShowLoginManagerVM { get; } + public ICommand InfoCommand { get; } + public ICommand MinimizeCommand { get; } + public ICommand MaximizeCommand { get; } + public ICommand CloseCommand { get; } + + public string VersionDisplay { get; } + + [Reactive] + public string ResourceStatus { get; set; } + + [Reactive] + public string WindowTitle { get; set; } + + [Reactive] + public bool UpdateAvailable { get; private set; } + + [Reactive] + public bool NavigationVisible { get; private set; } = true; + + public MainWindowVM(ILogger logger, Client wjClient, + IServiceProvider serviceProvider, HomeVM homeVM, ModListGalleryVM modListGalleryVM, ResourceMonitor resourceMonitor, + InstallationVM installerVM, CompilerHomeVM compilerHomeVM, CompilerDetailsVM compilerDetailsVM, CompilerFileManagerVM compilerFileManagerVM, CompilerMainVM compilerMainVM, SettingsVM settingsVM, WebBrowserVM webBrowserVM, NavigationVM navigationVM, InfoVM infoVM, ModListDetailsVM modlistDetailsVM) + { + _logger = logger; + _wjClient = wjClient; + _resourceMonitor = resourceMonitor; + _serviceProvider = serviceProvider; + ConverterRegistration.Register(); + InstallerVM = installerVM; + CompilerHomeVM = compilerHomeVM; + CompilerDetailsVM = compilerDetailsVM; + CompilerFileManagerVM = compilerFileManagerVM; + CompilerMainVM = compilerMainVM; + SettingsPaneVM = settingsVM; + GalleryVM = modListGalleryVM; + HomeVM = homeVM; + WebBrowserVM = webBrowserVM; + NavigationVM = navigationVM; + ModListDetailsVM = modlistDetailsVM; + InfoVM = infoVM; + UserInterventionHandlers = new UserInterventionHandlers(serviceProvider.GetRequiredService>(), this); + + MessageBus.Current.Listen() + .Subscribe(m => HandleNavigateTo(m.Screen)) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .Subscribe(m => HandleNavigateTo(m.ViewModel)) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .Subscribe(HandleNavigateBack) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .ObserveOnGuiThread() + .Subscribe(HandleSpawnBrowserWindow) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .ObserveOnGuiThread() + .Subscribe((_) => NavigationVisible = true) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .ObserveOnGuiThread() + .Subscribe((_) => NavigationVisible = false) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .Subscribe(m => HandleShowFloatingWindow(m.Screen)) + .DisposeWith(CompositeDisposable); + + _resourceMonitor.Updates + .Select(r => string.Join(", ", r.Where(r => r.Throughput > 0) + .Select(s => $"{s.Name} - {s.Throughput.ToFileSizeString()}/s"))) + .BindToStrict(this, view => view.ResourceStatus); + + + if (IsStartingFromModlist(out var path)) + { + LoadModlistForInstalling.Send(path, null); + NavigateToGlobal.Send(ScreenType.Installer); + } + else + { + // Start on mode selection + NavigateToGlobal.Send(ScreenType.Home); + } + + try + { + var assembly = Assembly.GetExecutingAssembly(); + var assemblyLocation = assembly.Location; + var processLocation = Process.GetCurrentProcess().MainModule?.FileName ?? throw new Exception("Process location is unavailable!"); + + _logger.LogInformation("Assembly Location: {AssemblyLocation}", assemblyLocation); + _logger.LogInformation("Process Location: {ProcessLocation}", processLocation); + + var fvi = FileVersionInfo.GetVersionInfo(string.IsNullOrWhiteSpace(assemblyLocation) ? processLocation : assemblyLocation); + Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion); + WindowTitle = Consts.AppName; + _logger.LogInformation("Wabbajack Version: {FileVersion}", fvi.FileVersion); + + Task.Run(() => _wjClient.SendMetric("started_wabbajack", fvi.FileVersion)).FireAndForget(); + Task.Run(() => _wjClient.SendMetric("started_sha", ThisAssembly.Git.Sha)); + + // setup file association + try + { + var applicationRegistrationService = _serviceProvider.GetRequiredService(); + + var applicationInfo = new ApplicationInfo("Wabbajack", "Wabbajack", "Wabbajack", processLocation); + applicationInfo.SupportedExtensions.Add("wabbajack"); + applicationRegistrationService.RegisterApplication(applicationInfo); + } + catch (Exception ex) + { + _logger.LogError(ex, "While setting up file associations"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "During App configuration"); + VersionDisplay = "ERROR"; + } + CopyVersionCommand = ReactiveCommand.Create(() => + { + Clipboard.SetText($"Wabbajack {VersionDisplay}\n{ThisAssembly.Git.Sha}"); + }); + InfoCommand = ReactiveCommand.Create(ShowInfo); + MinimizeCommand = ReactiveCommand.Create(Minimize); + MaximizeCommand = ReactiveCommand.Create(Maximize); + CloseCommand = ReactiveCommand.Create(Close); + } + + private void ShowInfo() + { + if (ActivePane is IHasInfoVM) ((IHasInfoVM)ActivePane).InfoCommand.Execute(null); + } + + private void Minimize() + { + var mainWindow = _serviceProvider.GetRequiredService(); + mainWindow.WindowState = WindowState.Minimized; + } + + private void Maximize() + { + var mainWindow = _serviceProvider.GetRequiredService(); + mainWindow.WindowState = WindowState.Maximized; + } + + private void Close() + { + Environment.Exit(0); + } + + private void HandleNavigateTo(ViewModel objViewModel) + { + ActivePane = objViewModel; + } + + private void HandleNavigateBack(NavigateBack navigateBack) + { + ActivePane = PreviousPanes.Last(); + PreviousPanes.RemoveAt(PreviousPanes.Count - 1); + } + + private void HandleManualDownload(ManualDownload manualDownload) + { + var handler = _serviceProvider.GetRequiredService(); + handler.Intervention = manualDownload; + //MessageBus.Current.SendMessage(new OpenBrowserTab(handler)); + } + + private void HandleManualBlobDownload(ManualBlobDownload manualDownload) + { + var handler = _serviceProvider.GetRequiredService(); + handler.Intervention = manualDownload; + //MessageBus.Current.SendMessage(new OpenBrowserTab(handler)); + } + + private void HandleSpawnBrowserWindow(SpawnBrowserWindow msg) + { + var window = _serviceProvider.GetRequiredService(); + window.DataContext = msg.Vm; + window.Show(); + } + + private void HandleNavigateTo(ScreenType s) + { + if (s is ScreenType.Settings) + PreviousPanes.Add(ActivePane); + + ActivePane = s switch + { + ScreenType.Home => HomeVM, + ScreenType.ModListGallery => GalleryVM, + ScreenType.Installer => InstallerVM, + ScreenType.CompilerHome => CompilerHomeVM, + ScreenType.CompilerMain => CompilerMainVM, + ScreenType.ModListDetails => ModListDetailsVM, + ScreenType.Settings => SettingsPaneVM, + ScreenType.Info => InfoVM, + _ => ActivePane + }; + } + private void HandleShowFloatingWindow(FloatingScreenType s) + { + ActiveFloatingPane = s switch + { + FloatingScreenType.None => null, + FloatingScreenType.ModListDetails => ModListDetailsVM, + _ => ActiveFloatingPane + }; + } + + + private static bool IsStartingFromModlist(out AbsolutePath modlistPath) + { + var args = Environment.GetCommandLineArgs(); + if (args.Length == 2) + { + var arg = args[1].ToAbsolutePath(); + if (arg.FileExists() && arg.Extension == Ext.Wabbajack) + { + modlistPath = arg; + return true; + } + } + + modlistPath = default; + return false; + } + + public void CancelRunningTasks(TimeSpan timeout) + { + var endTime = DateTime.Now.Add(timeout); + var cancellationTokenSource = _serviceProvider.GetRequiredService(); + cancellationTokenSource.Cancel(); + + bool IsInstalling() => InstallerVM.InstallState is InstallState.Installing; + + while (DateTime.Now < endTime && IsInstalling()) + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + } + } + + public async Task ShutdownApplication() + { + /* + Dispose(); + Settings.PosX = MainWindow.Left; + Settings.PosY = MainWindow.Top; + Settings.Width = MainWindow.Width; + Settings.Height = MainWindow.Height; + await MainSettings.SaveSettings(Settings); + Application.Current.Shutdown(); + */ + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/ModListDetailsVM.cs b/Wabbajack.App.Wpf/ViewModels/ModListDetailsVM.cs new file mode 100644 index 000000000..dd0761d15 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/ModListDetailsVM.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Windows.Input; +using DynamicData; +using DynamicData.Binding; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Web.WebView2.Wpf; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; +using Wabbajack.DTOs.ModListValidation; +using Wabbajack.DTOs.ServerResponses; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Messages; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.RateLimiter; + +namespace Wabbajack; + +public class ModListDetailsVM : BackNavigatingVM +{ + private readonly Client _wjClient; + [Reactive] + public BaseModListMetadataVM MetadataVM { get; set; } + + [Reactive] + public ValidatedModList ValidatedModlist { get; set; } + + [Reactive] + public ObservableCollection Status { get; set; } + + [Reactive] + public string Search { get; set; } + + private readonly SourceCache _archives = new(a => a.Hash); + private ReadOnlyObservableCollection _filteredArchives; + public ReadOnlyObservableCollection Archives => _filteredArchives; + + private readonly ILogger _logger; + + public ICommand OpenWebsiteCommand { get; set; } + public ICommand OpenDiscordCommand { get; set; } + public ICommand OpenReadmeCommand { get; set; } + + public WebView2 Browser { get; set; } + + public ModListDetailsVM(ILogger logger, IServiceProvider serviceProvider, Client wjClient) : base(logger) + { + _logger = logger; + _wjClient = wjClient; + + Browser = serviceProvider.GetRequiredService(); + + MessageBus.Current.Listen() + .Subscribe(msg => MetadataVM = msg.MetadataVM) + .DisposeWith(CompositeDisposable); + + OpenWebsiteCommand = ReactiveCommand.Create(() => Process.Start(new ProcessStartInfo(MetadataVM.Metadata.Links.WebsiteURL) { UseShellExecute = true }), + this.WhenAnyValue(x => x.MetadataVM.Metadata.Links.WebsiteURL, x => !string.IsNullOrEmpty(x)).ObserveOnGuiThread()); + OpenDiscordCommand = ReactiveCommand.Create(() => Process.Start(new ProcessStartInfo(MetadataVM.Metadata.Links.DiscordURL) { UseShellExecute = true }), + this.WhenAnyValue(x => x.MetadataVM.Metadata.Links.DiscordURL, x => !string.IsNullOrEmpty(x)).ObserveOnGuiThread()); + OpenReadmeCommand = ReactiveCommand.Create(() => Process.Start(new ProcessStartInfo(MetadataVM.Metadata.Links.Readme) { UseShellExecute = true }), + this.WhenAnyValue(x => x.MetadataVM.Metadata.Links.Readme, x => !string.IsNullOrEmpty(x)).ObserveOnGuiThread()); + + BackCommand = ReactiveCommand.Create(() => ShowFloatingWindow.Send(FloatingScreenType.None)); + this.WhenActivated(disposables => + { + + LoadArchives(MetadataVM.Metadata.RepositoryName, MetadataVM.Metadata.Links.MachineURL).FireAndForget(); + + var searchThrottle = TimeSpan.FromSeconds(0.5); + + var searchTextPredicates = this.ObservableForProperty(vm => vm.Search) + .Throttle(searchThrottle, RxApp.MainThreadScheduler) + .Select(change => change.Value?.Trim() ?? "") + .StartWith(Search) + .Select>(txt => + { + if (string.IsNullOrWhiteSpace(txt)) return _ => true; + return item => item.State is Nexus nexus ? nexus.Name.ContainsCaseInsensitive(txt) : item.Name.ContainsCaseInsensitive(txt); + }); + + var searchSorter = this.WhenValueChanged(vm => vm.Search) + .Throttle(searchThrottle, RxApp.MainThreadScheduler) + .Select(s => SortExpressionComparer + .Descending(a => a.State is Nexus ? ((Nexus)a.State).Name?.StartsWith(s ?? "", StringComparison.InvariantCultureIgnoreCase) : false) + .ThenByDescending(a => a.Name?.StartsWith(s ?? "", StringComparison.InvariantCultureIgnoreCase)) + .ThenByDescending(a => a.Name?.Contains(s ?? "", StringComparison.InvariantCultureIgnoreCase))); + + _archives.Connect() + .ObserveOn(RxApp.MainThreadScheduler) + .Filter(searchTextPredicates) + .Sort(searchSorter) + .TreatMovesAsRemoveAdd() + .Bind(out _filteredArchives) + .Subscribe() + .DisposeWith(disposables); + + MetadataVM.ProgressPercent = Percent.One; + }); + } + + private async Task LoadArchives(string repo, string machineURL) + { + using var ll = LoadingLock.WithLoading(); + try + { + var validatedModlist = await _wjClient.GetDetailedStatus(repo, machineURL); + var archives = validatedModlist.Archives.Select(a => a.Original).ToList(); + _archives.Edit(a => + { + a.Clear(); + a.AddOrUpdate(archives); + }); + ll.Succeed(); + } + catch(Exception ex) + { + _logger.LogError("Exception while loading archives: {0}", ex.ToString()); + ll.Fail(); + } + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/ModListVM.cs b/Wabbajack.App.Wpf/ViewModels/ModListVM.cs new file mode 100644 index 000000000..c4676adbe --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/ModListVM.cs @@ -0,0 +1,132 @@ +using ReactiveUI; +using System; +using System.IO; +using System.IO.Compression; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Microsoft.Extensions.Logging; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.DTOs; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Installer; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; + +namespace Wabbajack; + +public class ModListVM : ViewModel +{ + private readonly DTOSerializer _dtos; + private readonly ILogger _logger; + public ModList SourceModList { get; private set; } + public ModlistMetadata SourceModListMetadata { get; private set; } + + [Reactive] + public Exception Error { get; set; } + public AbsolutePath ModListPath { get; } + public string Name => SourceModList?.Name; + public string Readme => SourceModList?.Readme; + public string Author => SourceModList?.Author; + public string Description => SourceModList?.Description; + public Uri Website => SourceModList?.Website; + public Version Version => SourceModList?.Version; + public Version WabbajackVersion => SourceModList?.WabbajackVersion; + public bool IsNSFW => SourceModList?.IsNSFW ?? false; + + // Image isn't exposed as a direct property, but as an observable. + // This acts as a caching mechanism, as interested parties will trigger it to be created, + // and the cached image will automatically be released when the last interested party is gone. + public IObservable ImageObservable { get; } + + public ModListVM(ILogger logger, AbsolutePath modListPath, DTOSerializer dtos) + { + _dtos = dtos; + _logger = logger; + + ModListPath = modListPath; + + Task.Run(async () => + { + try + { + SourceModList = await StandardInstaller.LoadFromFile(_dtos, modListPath); + var metadataPath = modListPath.WithExtension(Ext.ModlistMetadataExtension); + if (metadataPath.FileExists()) + { + try + { + SourceModListMetadata = await metadataPath.FromJson(); + } + catch (Exception) + { + SourceModListMetadata = null; + } + } + } + catch (Exception ex) + { + Error = ex; + _logger.LogError(ex, "Exception while loading the modlist!"); + } + }); + + ImageObservable = Observable.Return(Unit.Default) + // Download and retrieve bytes on background thread + .ObserveOn(RxApp.TaskpoolScheduler) + .SelectAsync(async filePath => + { + try + { + await using var fs = ModListPath.Open(FileMode.Open, FileAccess.Read, FileShare.Read); + using var ar = new ZipArchive(fs, ZipArchiveMode.Read); + var ms = new MemoryStream(); + var entry = ar.GetEntry("modlist-image.png"); + if (entry == null) return default(MemoryStream); + await using var e = entry.Open(); + e.CopyTo(ms); + return ms; + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while caching Mod List image {Name}", Name); + return default(MemoryStream); + } + }) + // Create Bitmap image on GUI thread + .ObserveOnGuiThread() + .Select(memStream => + { + if (memStream == null) return default(BitmapImage); + try + { + return UIUtils.BitmapImageFromStream(memStream); + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception while caching Mod List image {Name}", Name); + return default(BitmapImage); + } + }) + // If ever would return null, show WJ logo instead + .Select(x => x ?? ResourceLinks.WabbajackLogoNoText.Value) + .Replay(1) + .RefCount(); + } + + public void OpenReadme() + { + if (string.IsNullOrEmpty(Readme)) return; + UIUtils.OpenWebsite(new Uri(Readme)); + } + + public override void Dispose() + { + base.Dispose(); + // Just drop reference explicitly, as it's large, so it can be GCed + // Even if someone is holding a stale reference to the VM + SourceModList = null; + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/ModVM.cs b/Wabbajack.App.Wpf/ViewModels/ModVM.cs new file mode 100644 index 000000000..dd6bd5b95 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/ModVM.cs @@ -0,0 +1,40 @@ +using ReactiveUI; +using System; +using System.Drawing; +using System.Net.Http; +using System.Reactive.Linq; +using System.Windows.Media.Imaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Wabbajack.DTOs.DownloadStates; + +namespace Wabbajack; + +public class ModVM : ViewModel +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + private HttpClient _httpClient; + private ImageCacheManager _icm; + public IMetaState State { get; } + + // Image isn't exposed as a direct property, but as an observable. + // This acts as a caching mechanism, as interested parties will trigger it to be created, + // and the cached image will automatically be released when the last interested party is gone. + public IObservable ImageObservable { get; } + + public ModVM(ILogger logger, IServiceProvider serviceProvider, IMetaState state, ImageCacheManager icm) + { + _logger = logger; + _serviceProvider = serviceProvider; + _httpClient = _serviceProvider.GetService(); + _icm = icm; + State = state; + + ImageObservable = Observable.Return(State.ImageURL?.ToString()) + .ObserveOn(RxApp.TaskpoolScheduler) + .DownloadBitmapImage(ex => _logger.LogWarning(ex, "Skipping slide for mod {Name}", State.Name), LoadingLock, _httpClient, _icm) + .Replay(1) + .RefCount(TimeSpan.FromMilliseconds(5000)); + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/NavigationVM.cs b/Wabbajack.App.Wpf/ViewModels/NavigationVM.cs new file mode 100644 index 000000000..4c79c6e92 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/NavigationVM.cs @@ -0,0 +1,53 @@ +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Reactive.Linq; +using System.Windows.Input; +using Wabbajack.Messages; +using Microsoft.Extensions.Logging; +using System.Reactive.Disposables; +using System.Diagnostics; +using System.Reflection; + +namespace Wabbajack; + +public class NavigationVM : ViewModel +{ + private readonly ILogger _logger; + [Reactive] + public ScreenType ActiveScreen { get; set; } + public NavigationVM(ILogger logger) + { + _logger = logger; + HomeCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(ScreenType.Home)); + BrowseCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(ScreenType.ModListGallery)); + InstallCommand = ReactiveCommand.Create(() => + { + LoadLastLoadedModlist.Send(); + NavigateToGlobal.Send(ScreenType.Installer); + }); + CompileModListCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(ScreenType.CompilerHome)); + SettingsCommand = ReactiveCommand.Create( + /* + canExecute: this.WhenAny(x => x.ActivePane) + .Select(active => !object.ReferenceEquals(active, SettingsPane)), + */ + execute: () => NavigateToGlobal.Send(ScreenType.Settings)); + MessageBus.Current.Listen() + .Subscribe(x => ActiveScreen = x.Screen) + .DisposeWith(CompositeDisposable); + + var processLocation = Process.GetCurrentProcess().MainModule?.FileName ?? throw new Exception("Process location is unavailable!"); + var assembly = Assembly.GetExecutingAssembly(); + var assemblyLocation = assembly.Location; + var fvi = FileVersionInfo.GetVersionInfo(string.IsNullOrWhiteSpace(assemblyLocation) ? processLocation : assemblyLocation); + Version = $"{fvi.FileVersion}"; + } + + public ICommand HomeCommand { get; } + public ICommand BrowseCommand { get; } + public ICommand InstallCommand { get; } + public ICommand CompileModListCommand { get; } + public ICommand SettingsCommand { get; } + public string Version { get; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/ProgressViewModel.cs b/Wabbajack.App.Wpf/ViewModels/ProgressViewModel.cs new file mode 100644 index 000000000..c3533680e --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/ProgressViewModel.cs @@ -0,0 +1,13 @@ +using ReactiveUI.Fody.Helpers; +using Wabbajack.RateLimiter; + +namespace Wabbajack; + +public abstract class ProgressViewModel : ViewModel, IProgressVM +{ + [Reactive] public Step CurrentStep { get; set; } + [Reactive] public ProgressState ProgressState { get; set; } + [Reactive] public string ConfigurationText { get; set; } + [Reactive] public string ProgressText { get; set; } + [Reactive] public Percent ProgressPercent { get; set; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Settings/AuthorFilesVM.cs b/Wabbajack.App.Wpf/ViewModels/Settings/AuthorFilesVM.cs new file mode 100644 index 000000000..61710d841 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Settings/AuthorFilesVM.cs @@ -0,0 +1,92 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.Services.OSIntegrated.TokenProviders; + +namespace Wabbajack.ViewModels.Settings; + +public class AuthorFilesVM : BackNavigatingVM +{ + [Reactive] + public Visibility IsVisible { get; set; } + + public ICommand SelectFile { get; } + public ICommand HyperlinkCommand { get; } + public IReactiveCommand Upload { get; } + public IReactiveCommand ManageFiles { get; } + + [Reactive] public double UploadProgress { get; set; } + [Reactive] public string FinalUrl { get; set; } + public FilePickerVM Picker { get;} + + private Subject _isUploading = new(); + private readonly WabbajackApiTokenProvider _token; + private readonly Client _wjClient; + private IObservable IsUploading { get; } + + public AuthorFilesVM(ILogger logger, WabbajackApiTokenProvider token, Client wjClient, SettingsVM vm) : base(logger) + { + _token = token; + _wjClient = wjClient; + IsUploading = _isUploading; + Picker = new FilePickerVM(this); + + + IsVisible = Visibility.Hidden; + + Task.Run(async () => + { + var isAuthor = !string.IsNullOrWhiteSpace((await _token.Get())?.AuthorKey); + IsVisible = isAuthor ? Visibility.Visible : Visibility.Collapsed; + }); + + SelectFile = Picker.ConstructTypicalPickerCommand(IsUploading.StartWith(false).Select(u => !u)); + + HyperlinkCommand = ReactiveCommand.Create(() => Clipboard.SetText(FinalUrl)); + + ManageFiles = ReactiveCommand.Create(async () => + { + var authorApiKey = (await token.Get())!.AuthorKey; + UIUtils.OpenWebsite(new Uri($"{Consts.WabbajackBuildServerUri}author_controls/login/{authorApiKey}")); + }); + + Upload = ReactiveCommand.Create(async () => + { + _isUploading.OnNext(true); + try + { + var (progress, task) = await _wjClient.UploadAuthorFile(Picker.TargetPath); + + var disposable = progress.Subscribe(m => + { + FinalUrl = m.Message; + UploadProgress = (double)m.PercentDone; + }); + + var final = await task; + disposable.Dispose(); + FinalUrl = final.ToString(); + } + catch (Exception ex) + { + FinalUrl = ex.ToString(); + } + finally + { + FinalUrl = FinalUrl.Replace(" ", "%20"); + _isUploading.OnNext(false); + } + }, IsUploading.StartWith(false).Select(u => !u) + .CombineLatest(Picker.WhenAnyValue(t => t.TargetPath).Select(f => f != default), + (a, b) => a && b)); + } + +} diff --git a/Wabbajack.App.Wpf/ViewModels/Settings/LoginManagerVM.cs b/Wabbajack.App.Wpf/ViewModels/Settings/LoginManagerVM.cs new file mode 100644 index 000000000..e7103efd6 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Settings/LoginManagerVM.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Logging; +using Wabbajack.LoginManagers; + +namespace Wabbajack; + + +public class LoginManagerVM : BackNavigatingVM +{ + public LoginTargetVM[] Logins { get; } + + public LoginManagerVM(ILogger logger, SettingsVM settingsVM, IEnumerable logins) + : base(logger) + { + Logins = logins.Select(l => new LoginTargetVM(l)).ToArray(); + } + +} + +public class LoginTargetVM : ViewModel +{ + public INeedsLogin Login { get; } + public LoginTargetVM(INeedsLogin login) + { + Login = login; + } +} + diff --git a/Wabbajack.App.Wpf/ViewModels/Settings/SettingsVM.cs b/Wabbajack.App.Wpf/ViewModels/Settings/SettingsVM.cs new file mode 100644 index 000000000..52aaf9294 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Settings/SettingsVM.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using System.Windows.Input; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using Wabbajack.Common; +using Wabbajack.Downloaders; +using Wabbajack.LoginManagers; +using Wabbajack.Messages; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Services.OSIntegrated.TokenProviders; +using Wabbajack.Util; +using Wabbajack.ViewModels.Settings; + +namespace Wabbajack; + +public class SettingsVM : BackNavigatingVM +{ + private readonly Configuration.MainSettings _settings; + private readonly SettingsManager _settingsManager; + + public LoginManagerVM Login { get; } + public PerformanceSettings Performance { get; } + public AuthorFilesVM AuthorFile { get; } + + public ICommand OpenTerminalCommand { get; } + + public SettingsVM(ILogger logger, IServiceProvider provider) + : base(logger) + { + _settings = provider.GetRequiredService(); + _settingsManager = provider.GetRequiredService(); + + Login = new LoginManagerVM(provider.GetRequiredService>(), this, + provider.GetRequiredService>()); + AuthorFile = new AuthorFilesVM(provider.GetRequiredService>()!, + provider.GetRequiredService()!, provider.GetRequiredService()!, this); + OpenTerminalCommand = ReactiveCommand.CreateFromTask(OpenTerminal); + Performance = new PerformanceSettings( + _settings, + provider.GetRequiredService>(), + provider.GetRequiredService()); + BackCommand = ReactiveCommand.Create(() => + { + NavigateBack.Send(); + Unload(); + }); + } + + public override void Unload() + { + _settingsManager.Save(Configuration.MainSettings.SettingsFileName, _settings).FireAndForget(); + + base.Unload(); + } + + private async Task OpenTerminal() + { + var process = new ProcessStartInfo + { + FileName = "cmd.exe", + WorkingDirectory = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)! + }; + Process.Start(process); + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/UserIntervention/ConfirmUpdateOfExistingInstallVM.cs b/Wabbajack.App.Wpf/ViewModels/UserIntervention/ConfirmUpdateOfExistingInstallVM.cs new file mode 100644 index 000000000..267d45d8a --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/UserIntervention/ConfirmUpdateOfExistingInstallVM.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading; +using Wabbajack.DTOs.Interventions; + +namespace Wabbajack; + +public class ConfirmUpdateOfExistingInstallVM : ViewModel, IUserIntervention +{ + public ConfirmUpdateOfExistingInstall Source { get; } + + public MO2InstallerVM Installer { get; } + + public bool Handled => ((IUserIntervention)Source).Handled; + public CancellationToken Token { get; } + public void SetException(Exception exception) + { + throw new NotImplementedException(); + } + + public int CpuID => 0; + + public DateTime Timestamp => DateTime.Now; + + public string ShortDescription => "Short Desc"; + + public string ExtendedDescription => "Extended Desc"; + + public ConfirmUpdateOfExistingInstallVM(MO2InstallerVM installer, ConfirmUpdateOfExistingInstall confirm) + { + Source = confirm; + Installer = installer; + } + + public void Cancel() + { + ((IUserIntervention)Source).Cancel(); + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/UserInterventionHandlers.cs b/Wabbajack.App.Wpf/ViewModels/UserInterventionHandlers.cs new file mode 100644 index 000000000..64ec13661 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/UserInterventionHandlers.cs @@ -0,0 +1,111 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using Wabbajack.Common; +using Wabbajack.DTOs.Interventions; +using Wabbajack.Interventions; +using Wabbajack.Messages; + +namespace Wabbajack; + +public class UserInterventionHandlers +{ + public MainWindowVM MainWindow { get; } + private AsyncLock _browserLock = new(); + private readonly ILogger _logger; + + public UserInterventionHandlers(ILogger logger, MainWindowVM mvm) + { + _logger = logger; + MainWindow = mvm; + } + + private async Task WrapBrowserJob(IUserIntervention intervention, WebBrowserVM vm, Func toDo) + { + var wait = await _browserLock.WaitAsync(); + var cancel = new CancellationTokenSource(); + var oldPane = MainWindow.ActivePane; + + // TODO: FIX using var vm = await WebBrowserVM.GetNew(_logger); + NavigateTo.Send(vm); + vm.BackCommand = ReactiveCommand.Create(() => + { + cancel.Cancel(); + NavigateTo.Send(oldPane); + intervention.Cancel(); + }); + + try + { + await toDo(vm, cancel); + } + catch (TaskCanceledException) + { + intervention.Cancel(); + } + catch (Exception ex) + { + _logger.LogError(ex, "During Web browser job"); + intervention.Cancel(); + } + finally + { + wait.Dispose(); + } + + NavigateTo.Send(oldPane); + } + + public async Task Handle(IStatusMessage msg) + { + switch (msg) + { + /* + case RequestNexusAuthorization c: + await WrapBrowserJob(c, async (vm, cancel) => + { + await vm.Driver.WaitForInitialized(); + var key = await NexusApiClient.SetupNexusLogin(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token); + c.Resume(key); + }); + break; + case ManuallyDownloadNexusFile c: + await WrapBrowserJob(c, (vm, cancel) => HandleManualNexusDownload(vm, cancel, c)); + break; + case ManuallyDownloadFile c: + await WrapBrowserJob(c, (vm, cancel) => HandleManualDownload(vm, cancel, c)); + break; + case AbstractNeedsLoginDownloader.RequestSiteLogin c: + await WrapBrowserJob(c, async (vm, cancel) => + { + await vm.Driver.WaitForInitialized(); + var data = await c.Downloader.GetAndCacheCookies(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token); + c.Resume(data); + }); + break; + case RequestOAuthLogin oa: + await WrapBrowserJob(oa, async (vm, cancel) => + { + await OAuthLogin(oa, vm, cancel); + }); + + + break; + */ + case CriticalFailureIntervention c: + MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK, + MessageBoxImage.Error); + c.Cancel(); + if (c.ExitApplication) await MainWindow.ShutdownApplication(); + break; + case ConfirmationIntervention c: + break; + default: + throw new NotImplementedException($"No handler for {msg}"); + } + } + +} diff --git a/Wabbajack.App.Wpf/ViewModels/ViewModel.cs b/Wabbajack.App.Wpf/ViewModels/ViewModel.cs new file mode 100644 index 000000000..6cdf6a922 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/ViewModel.cs @@ -0,0 +1,38 @@ +using Newtonsoft.Json; +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using Wabbajack.Models; + +namespace Wabbajack; + +public class ViewModel : ReactiveObject, IDisposable, IActivatableViewModel +{ + private readonly Lazy _compositeDisposable = new(); + [JsonIgnore] + public CompositeDisposable CompositeDisposable => _compositeDisposable.Value; + + [JsonIgnore] public LoadingLock LoadingLock { get; } = new(); + + public virtual void Dispose() + { + if (_compositeDisposable.IsValueCreated) + { + _compositeDisposable.Value.Dispose(); + } + } + + protected void RaiseAndSetIfChanged( + ref T item, + T newItem, + [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(item, newItem)) return; + item = newItem; + this.RaisePropertyChanged(propertyName); + } + + public ViewModelActivator Activator { get; } = new(); +} diff --git a/Wabbajack.App.Wpf/ViewModels/WebBrowserVM.cs b/Wabbajack.App.Wpf/ViewModels/WebBrowserVM.cs new file mode 100644 index 000000000..a9a5b4674 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/WebBrowserVM.cs @@ -0,0 +1,50 @@ +using System; +using System.Reactive; +using System.Reactive.Subjects; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Messages; +using Wabbajack.Models; + +namespace Wabbajack; + +public class WebBrowserVM : ViewModel, IBackNavigatingVM, IDisposable +{ + private readonly ILogger _logger; + private readonly CefService _cefService; + + [Reactive] + public string Instructions { get; set; } + + public dynamic Browser { get; } + public dynamic Driver { get; set; } + + [Reactive] + public ViewModel NavigateBackTarget { get; set; } + + [Reactive] + public ReactiveCommand BackCommand { get; set; } + + public Subject IsBackEnabledSubject { get; } = new Subject(); + public IObservable IsBackEnabled { get; } + + public WebBrowserVM(ILogger logger, CefService cefService) + { + // CefService is required so that Cef is initalized + _logger = logger; + _cefService = cefService; + Instructions = "Wabbajack Web Browser"; + + BackCommand = ReactiveCommand.Create(NavigateBack.Send); + //Browser = cefService.CreateBrowser(); + //Driver = new CefSharpWrapper(_logger, Browser, cefService); + + } + + public override void Dispose() + { + Browser.Dispose(); + base.Dispose(); + } +} diff --git a/Wabbajack.App.Wpf/Views/BrowserView.xaml.cs b/Wabbajack.App.Wpf/Views/BrowserView.xaml.cs index 52a68d523..969f5fff4 100644 --- a/Wabbajack.App.Wpf/Views/BrowserView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/BrowserView.xaml.cs @@ -1,8 +1,3 @@ -using System; -using System.Windows.Controls; -using Microsoft.Web.WebView2.WinForms; -using ReactiveUI; - namespace Wabbajack.Views; public partial class BrowserView diff --git a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml index a1fa5033d..381939aa4 100644 --- a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml +++ b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml @@ -6,7 +6,7 @@ xmlns:local="clr-namespace:Wabbajack" xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:viewModels="clr-namespace:Wabbajack.View_Models" + xmlns:viewModels="clr-namespace:Wabbajack.ViewModels" xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" ShowTitleBar="False" Title="Browser Window" @@ -23,7 +23,7 @@ WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}" ContentRendered="BrowserWindow_OnActivated" mc:Ignorable="d"> - + @@ -36,9 +36,9 @@ - + - + diff --git a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs index d9fb66456..041bd8f25 100644 --- a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs +++ b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs @@ -1,9 +1,7 @@ using System; using System.Reactive.Concurrency; using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Threading; -using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; @@ -11,7 +9,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Web.WebView2.Wpf; using ReactiveUI; -using Wabbajack.Common; namespace Wabbajack; diff --git a/Wabbajack.App.Wpf/Views/Common/AttentionBorder.cs b/Wabbajack.App.Wpf/Views/Common/AttentionBorder.cs index 5cfdb3431..54cd8305d 100644 --- a/Wabbajack.App.Wpf/Views/Common/AttentionBorder.cs +++ b/Wabbajack.App.Wpf/Views/Common/AttentionBorder.cs @@ -1,31 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; +using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for AttentionBorder.xaml +/// +public partial class AttentionBorder : UserControl { - /// - /// Interaction logic for AttentionBorder.xaml - /// - public partial class AttentionBorder : UserControl + public bool Failure { - public bool Failure - { - get => (bool)GetValue(FailureProperty); - set => SetValue(FailureProperty, value); - } - public static readonly DependencyProperty FailureProperty = DependencyProperty.Register(nameof(Failure), typeof(bool), typeof(AttentionBorder), - new FrameworkPropertyMetadata(default(bool))); + get => (bool)GetValue(FailureProperty); + set => SetValue(FailureProperty, value); } + public static readonly DependencyProperty FailureProperty = DependencyProperty.Register(nameof(Failure), typeof(bool), typeof(AttentionBorder), + new FrameworkPropertyMetadata(default(bool))); } diff --git a/Wabbajack.App.Wpf/Views/Common/BeginButton.xaml b/Wabbajack.App.Wpf/Views/Common/BeginButton.xaml index 7ecb0cec0..18d9c7b71 100644 --- a/Wabbajack.App.Wpf/Views/Common/BeginButton.xaml +++ b/Wabbajack.App.Wpf/Views/Common/BeginButton.xaml @@ -24,7 +24,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs b/Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs index 5ac20e794..3d91504e8 100644 --- a/Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs @@ -1,132 +1,99 @@ using ReactiveUI; -using ReactiveUI.Fody.Helpers; -using System; using System.Linq; -using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Windows; using System.Windows.Media; -using Wabbajack; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for DetailImageView.xaml +/// +public partial class DetailImageView : UserControlRx { - /// - /// Interaction logic for DetailImageView.xaml - /// - public partial class DetailImageView : UserControlRx + public ImageSource Image { - public ImageSource Image - { - get => (ImageSource)GetValue(ImageProperty); - set => SetValue(ImageProperty, value); - } - public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(ImageSource), typeof(DetailImageView), - new FrameworkPropertyMetadata(default(ImageSource), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + get => (ImageSource)GetValue(ImageProperty); + set => SetValue(ImageProperty, value); + } + public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(ImageSource), typeof(DetailImageView), + new FrameworkPropertyMetadata(default(ImageSource), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - public ImageSource Badge - { - get => (ImageSource)GetValue(BadgeProperty); - set => SetValue(BadgeProperty, value); - } - public static readonly DependencyProperty BadgeProperty = DependencyProperty.Register(nameof(Badge), typeof(ImageSource), typeof(DetailImageView), - new FrameworkPropertyMetadata(default(ImageSource), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(DetailImageView), + new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - public string Title - { - get => (string)GetValue(TitleProperty); - set => SetValue(TitleProperty, value); - } - public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(DetailImageView), - new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + public double TitleFontSize + { + get => (double)GetValue(TitleFontSizeProperty); + set => SetValue(TitleFontSizeProperty, value); + } + public static readonly DependencyProperty TitleFontSizeProperty = DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(DetailImageView), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - public string Author - { - get => (string)GetValue(AuthorProperty); - set => SetValue(AuthorProperty, value); - } - public static readonly DependencyProperty AuthorProperty = DependencyProperty.Register(nameof(Author), typeof(string), typeof(DetailImageView), - new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + public string Author + { + get => (string)GetValue(AuthorProperty); + set => SetValue(AuthorProperty, value); + } + public static readonly DependencyProperty AuthorProperty = DependencyProperty.Register(nameof(Author), typeof(string), typeof(DetailImageView), + new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + public double AuthorFontSize + { + get => (double)GetValue(AuthorFontSizeProperty); + set => SetValue(AuthorFontSizeProperty, value); + } + public static readonly DependencyProperty AuthorFontSizeProperty = DependencyProperty.Register(nameof(AuthorFontSize), typeof(double), typeof(DetailImageView), new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - public string Description - { - get => (string)GetValue(DescriptionProperty); - set => SetValue(DescriptionProperty, value); - } - public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(DetailImageView), - new FrameworkPropertyMetadata(default(string), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); - public DetailImageView() - { - InitializeComponent(); + public DetailImageView() + { + InitializeComponent(); - this.WhenActivated(dispose => - { - // Update textboxes - var authorVisible = this.WhenAny(x => x.Author) - .Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible) - .Replay(1) - .RefCount(); - authorVisible - .BindToStrict(this, x => x.AuthorTextBlock.Visibility) - .DisposeWith(dispose); - authorVisible - .BindToStrict(this, x => x.AuthorTextShadow.Visibility) - .DisposeWith(dispose); - this.WhenAny(x => x.Author) - .BindToStrict(this, x => x.AuthorTextRun.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.Author) - .BindToStrict(this, x => x.AuthorShadowTextRun.Text) - .DisposeWith(dispose); + this.WhenActivated(dispose => + { + // Update textboxes + var authorVisible = this.WhenAny(x => x.Author) + .Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible) + .Replay(1) + .RefCount(); + authorVisible + .BindToStrict(this, x => x.AuthorTextBlock.Visibility) + .DisposeWith(dispose); + this.WhenAny(x => x.Author) + .BindToStrict(this, x => x.AuthorTextRun.Text) + .DisposeWith(dispose); - var descVisible = this.WhenAny(x => x.Description) - .Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible) - .Replay(1) - .RefCount(); - descVisible - .BindToStrict(this, x => x.DescriptionTextBlock.Visibility) - .DisposeWith(dispose); - descVisible - .BindToStrict(this, x => x.DescriptionTextShadow.Visibility) - .DisposeWith(dispose); - this.WhenAny(x => x.Description) - .BindToStrict(this, x => x.DescriptionTextBlock.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.Description) - .BindToStrict(this, x => x.DescriptionTextShadow.Text) - .DisposeWith(dispose); + var titleVisible = this.WhenAny(x => x.Title) + .Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible) + .Replay(1) + .RefCount(); + titleVisible + .BindToStrict(this, x => x.TitleTextBlock.Visibility) + .DisposeWith(dispose); + this.WhenAny(x => x.Title) + .BindToStrict(this, x => x.TitleTextBlock.Text) + .DisposeWith(dispose); - var titleVisible = this.WhenAny(x => x.Title) - .Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Collapsed : Visibility.Visible) - .Replay(1) - .RefCount(); - titleVisible - .BindToStrict(this, x => x.TitleTextBlock.Visibility) - .DisposeWith(dispose); - titleVisible - .BindToStrict(this, x => x.TitleTextShadow.Visibility) - .DisposeWith(dispose); - this.WhenAny(x => x.Title) - .BindToStrict(this, x => x.TitleTextBlock.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.Title) - .BindToStrict(this, x => x.TitleTextShadow.Text) - .DisposeWith(dispose); + this.WhenAny(x => x.Image) + .Select(f => f) + .BindToStrict(this, x => x.ModlistImage.Source) + .DisposeWith(dispose); + this.WhenAny(x => x.Image) + .Select(img => img == null ? Visibility.Hidden : Visibility.Visible) + .BindToStrict(this, x => x.ModlistImage.Visibility) + .DisposeWith(dispose); - // Update other items - this.WhenAny(x => x.Badge) - .BindToStrict(this, x => x.BadgeImage.Source) - .DisposeWith(dispose); - this.WhenAny(x => x.Image) - .Select(f => f) - .BindToStrict(this, x => x.ModlistImage.Source) - .DisposeWith(dispose); - this.WhenAny(x => x.Image) - .Select(img => img == null ? Visibility.Hidden : Visibility.Visible) - .BindToStrict(this, x => x.Visibility) - .DisposeWith(dispose); - }); - } + this.WhenAny(x => x.TitleFontSize) + .BindToStrict(this, x => x.TitleTextBlock.FontSize) + .DisposeWith(dispose); + this.WhenAny(x => x.AuthorFontSize) + .BindToStrict(this, x => x.AuthorTextBlock.FontSize) + .DisposeWith(dispose); + }); } } diff --git a/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml b/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml index 4b94b33d6..9b62a9745 100644 --- a/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml +++ b/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml @@ -3,154 +3,101 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:local="clr-namespace:Wabbajack" + xmlns:ic="clr-namespace:FluentIcons.WPF;assembly=FluentIcons.WPF" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" d:DesignHeight="35" d:DesignWidth="400" - BorderBrush="{StaticResource DarkBackgroundBrush}" mc:Ignorable="d"> - - - - - - + + + + + - - - - - - - - + + + + + + + + - + - + - + BorderThickness="0" + CornerRadius="4"> + + + + - - - - - - - - - - - - diff --git a/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml.cs b/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml.cs index 608cb1b0e..a3ddad644 100644 --- a/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/FilePicker.xaml.cs @@ -1,27 +1,40 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using Wabbajack; -namespace Wabbajack +using FluentIcons.Common; +using System.Windows; + +namespace Wabbajack; + +/// +/// Interaction logic for FilePicker.xaml +/// +public partial class FilePicker { - /// - /// Interaction logic for FilePicker.xaml - /// - public partial class FilePicker + // This exists, as utilizing the datacontext directly seemed to bug out the exit animations + // "Bouncing" off this property seems to fix it, though. Could perhaps be done other ways. + public FilePickerVM PickerVM { - // This exists, as utilizing the datacontext directly seemed to bug out the exit animations - // "Bouncing" off this property seems to fix it, though. Could perhaps be done other ways. - public FilePickerVM PickerVM - { - get => (FilePickerVM)GetValue(PickerVMProperty); - set => SetValue(PickerVMProperty, value); - } - public static readonly DependencyProperty PickerVMProperty = DependencyProperty.Register(nameof(PickerVM), typeof(FilePickerVM), typeof(FilePicker), - new FrameworkPropertyMetadata(default(FilePickerVM))); + get => (FilePickerVM)GetValue(PickerVMProperty); + set => SetValue(PickerVMProperty, value); + } + public static readonly DependencyProperty PickerVMProperty = DependencyProperty.Register(nameof(PickerVM), typeof(FilePickerVM), typeof(FilePicker), + new FrameworkPropertyMetadata(default(FilePickerVM))); - public FilePicker() - { - InitializeComponent(); - } + public Symbol Icon + { + get => (Symbol)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Symbol), typeof(FilePicker), + new PropertyMetadata(default(Symbol))); + public string Watermark + { + get => (string)GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); + } + public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register(nameof(Watermark), typeof(string), typeof(FilePicker), + new PropertyMetadata(default(string))); + + public FilePicker() + { + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Common/HeatedBackgroundView.xaml.cs b/Wabbajack.App.Wpf/Views/Common/HeatedBackgroundView.xaml.cs index 5011cd868..2f88c072d 100644 --- a/Wabbajack.App.Wpf/Views/Common/HeatedBackgroundView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/HeatedBackgroundView.xaml.cs @@ -1,36 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; +using System.Windows; using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for HeatedBackgroundView.xaml +/// +public partial class HeatedBackgroundView : UserControl { - /// - /// Interaction logic for HeatedBackgroundView.xaml - /// - public partial class HeatedBackgroundView : UserControl + public double PercentCompleted { - public double PercentCompleted - { - get => (double)GetValue(PercentCompletedProperty); - set => SetValue(PercentCompletedProperty, value); - } - public static readonly DependencyProperty PercentCompletedProperty = DependencyProperty.Register(nameof(PercentCompleted), typeof(double), typeof(HeatedBackgroundView), - new FrameworkPropertyMetadata(default(double))); + get => (double)GetValue(PercentCompletedProperty); + set => SetValue(PercentCompletedProperty, value); + } + public static readonly DependencyProperty PercentCompletedProperty = DependencyProperty.Register(nameof(PercentCompleted), typeof(double), typeof(HeatedBackgroundView), + new FrameworkPropertyMetadata(default(double))); - public HeatedBackgroundView() - { - InitializeComponent(); - } + public HeatedBackgroundView() + { + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Common/LogView.xaml b/Wabbajack.App.Wpf/Views/Common/LogView.xaml index 3b21e9c95..11359a6e2 100644 --- a/Wabbajack.App.Wpf/Views/Common/LogView.xaml +++ b/Wabbajack.App.Wpf/Views/Common/LogView.xaml @@ -13,12 +13,41 @@ + ScrollViewer.HorizontalScrollBarVisibility="Disabled" + AlternationCount="2"> + + + + - + diff --git a/Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs b/Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs index ff853410f..411919966 100644 --- a/Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs @@ -1,24 +1,23 @@ using System.Windows; using System.Windows.Controls; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for LogView.xaml +/// +public partial class LogView : UserControl { - /// - /// Interaction logic for LogView.xaml - /// - public partial class LogView : UserControl + public double ProgressPercent { - public double ProgressPercent - { - get => (double)GetValue(ProgressPercentProperty); - set => SetValue(ProgressPercentProperty, value); - } - public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(LogView), - new FrameworkPropertyMetadata(default(double))); + get => (double)GetValue(ProgressPercentProperty); + set => SetValue(ProgressPercentProperty, value); + } + public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(LogView), + new FrameworkPropertyMetadata(default(double))); - public LogView() - { - InitializeComponent(); - } + public LogView() + { + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Common/RadioButtonView.xaml.cs b/Wabbajack.App.Wpf/Views/Common/RadioButtonView.xaml.cs index 019b57451..99fdaa032 100644 --- a/Wabbajack.App.Wpf/Views/Common/RadioButtonView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/RadioButtonView.xaml.cs @@ -3,45 +3,44 @@ using System.Windows.Input; using System.Windows.Media.Imaging; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for ImageRadioButtonView.xaml +/// +public partial class ImageRadioButtonView : UserControl { - /// - /// Interaction logic for ImageRadioButtonView.xaml - /// - public partial class ImageRadioButtonView : UserControl + public bool IsChecked { - public bool IsChecked - { - get => (bool)GetValue(IsCheckedProperty); - set => SetValue(IsCheckedProperty, value); - } - public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ImageRadioButtonView), - new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + get => (bool)GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } + public static readonly DependencyProperty IsCheckedProperty = DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ImageRadioButtonView), + new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); - public BitmapImage Image - { - get => (BitmapImage)GetValue(ImageProperty); - set => SetValue(ImageProperty, value); - } - public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(BitmapImage), typeof(ImageRadioButtonView), - new FrameworkPropertyMetadata(default(BitmapImage))); + public BitmapImage Image + { + get => (BitmapImage)GetValue(ImageProperty); + set => SetValue(ImageProperty, value); + } + public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(nameof(Image), typeof(BitmapImage), typeof(ImageRadioButtonView), + new FrameworkPropertyMetadata(default(BitmapImage))); - public ICommand Command - { - get => (ICommand)GetValue(CommandProperty); - set => SetValue(CommandProperty, value); - } - public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(ImageRadioButtonView), - new FrameworkPropertyMetadata(default(ICommand))); + public ICommand Command + { + get => (ICommand)GetValue(CommandProperty); + set => SetValue(CommandProperty, value); + } + public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(ImageRadioButtonView), + new FrameworkPropertyMetadata(default(ICommand))); - public ImageRadioButtonView() - { - InitializeComponent(); - } + public ImageRadioButtonView() + { + InitializeComponent(); + } - private void Button_Click(object sender, RoutedEventArgs e) - { - IsChecked = true; - } + private void Button_Click(object sender, RoutedEventArgs e) + { + IsChecked = true; } } diff --git a/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml b/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml index 52f64fa0c..390df3383 100644 --- a/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml +++ b/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml @@ -78,7 +78,7 @@ Width="130" Margin="0,0,0,0" VerticalAlignment="Center" - FontFamily="Lucida Sans" + FontFamily="{StaticResource PrimaryFont}" FontWeight="Black" Foreground="{StaticResource ComplementaryBrush}" TextAlignment="Right" /> @@ -89,7 +89,7 @@ x:Name="TitleText" Margin="15,0,0,0" VerticalAlignment="Center" - FontFamily="Lucida Sans" + FontFamily="{StaticResource PrimaryFont}" FontSize="25" FontWeight="Black" /> diff --git a/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml.cs b/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml.cs index ebe3f7b35..73d0e9005 100644 --- a/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/TopProgressView.xaml.cs @@ -1,114 +1,109 @@ using System.Reactive.Linq; using System.Windows; -using System.Windows.Controls; using ReactiveUI; -using System; -using ReactiveUI.Fody.Helpers; -using Wabbajack; using System.Reactive.Disposables; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for TopProgressView.xaml +/// +public partial class TopProgressView : UserControlRx { - /// - /// Interaction logic for TopProgressView.xaml - /// - public partial class TopProgressView : UserControlRx + public double ProgressPercent { - public double ProgressPercent - { - get => (double)GetValue(ProgressPercentProperty); - set => SetValue(ProgressPercentProperty, value); - } - public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(TopProgressView), - new FrameworkPropertyMetadata(default(double), WireNotifyPropertyChanged)); + get => (double)GetValue(ProgressPercentProperty); + set => SetValue(ProgressPercentProperty, value); + } + public static readonly DependencyProperty ProgressPercentProperty = DependencyProperty.Register(nameof(ProgressPercent), typeof(double), typeof(TopProgressView), + new FrameworkPropertyMetadata(default(double), WireNotifyPropertyChanged)); - public string Title - { - get => (string)GetValue(TitleProperty); - set => SetValue(TitleProperty, value); - } - public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(TopProgressView), - new FrameworkPropertyMetadata(default(string), WireNotifyPropertyChanged)); + public string Title + { + get => (string)GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(TopProgressView), + new FrameworkPropertyMetadata(default(string), WireNotifyPropertyChanged)); - public string StatePrefixTitle - { - get => (string)GetValue(StatePrefixTitleProperty); - set => SetValue(StatePrefixTitleProperty, value); - } - public static readonly DependencyProperty StatePrefixTitleProperty = DependencyProperty.Register(nameof(StatePrefixTitle), typeof(string), typeof(TopProgressView), - new FrameworkPropertyMetadata(default(string), WireNotifyPropertyChanged)); + public string StatePrefixTitle + { + get => (string)GetValue(StatePrefixTitleProperty); + set => SetValue(StatePrefixTitleProperty, value); + } + public static readonly DependencyProperty StatePrefixTitleProperty = DependencyProperty.Register(nameof(StatePrefixTitle), typeof(string), typeof(TopProgressView), + new FrameworkPropertyMetadata(default(string), WireNotifyPropertyChanged)); - public bool OverhangShadow - { - get => (bool)GetValue(OverhangShadowProperty); - set => SetValue(OverhangShadowProperty, value); - } - public static readonly DependencyProperty OverhangShadowProperty = DependencyProperty.Register(nameof(OverhangShadow), typeof(bool), typeof(TopProgressView), - new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged)); + public bool OverhangShadow + { + get => (bool)GetValue(OverhangShadowProperty); + set => SetValue(OverhangShadowProperty, value); + } + public static readonly DependencyProperty OverhangShadowProperty = DependencyProperty.Register(nameof(OverhangShadow), typeof(bool), typeof(TopProgressView), + new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged)); - public bool ShadowMargin - { - get => (bool)GetValue(ShadowMarginProperty); - set => SetValue(ShadowMarginProperty, value); - } - public static readonly DependencyProperty ShadowMarginProperty = DependencyProperty.Register(nameof(ShadowMargin), typeof(bool), typeof(TopProgressView), - new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged)); + public bool ShadowMargin + { + get => (bool)GetValue(ShadowMarginProperty); + set => SetValue(ShadowMarginProperty, value); + } + public static readonly DependencyProperty ShadowMarginProperty = DependencyProperty.Register(nameof(ShadowMargin), typeof(bool), typeof(TopProgressView), + new FrameworkPropertyMetadata(true, WireNotifyPropertyChanged)); - public TopProgressView() + public TopProgressView() + { + InitializeComponent(); + this.WhenActivated(dispose => { - InitializeComponent(); - this.WhenActivated(dispose => - { - this.WhenAny(x => x.ProgressPercent) - .Select(x => 0.3 + x * 0.7) - .BindToStrict(this, x => x.LargeProgressBar.Opacity) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.LargeProgressBar.Value) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.BottomProgressBarDarkGlow.Value) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.LargeProgressBarTopGlow.Value) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.BottomProgressBarBrightGlow1.Value) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.BottomProgressBarBrightGlow2.Value) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.BottomProgressBar.Value) - .DisposeWith(dispose); - this.WhenAny(x => x.ProgressPercent) - .BindToStrict(this, x => x.BottomProgressBarHighlight.Value) - .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .Select(x => 0.3 + x * 0.7) + .BindToStrict(this, x => x.LargeProgressBar.Opacity) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.LargeProgressBar.Value) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.BottomProgressBarDarkGlow.Value) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.LargeProgressBarTopGlow.Value) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.BottomProgressBarBrightGlow1.Value) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.BottomProgressBarBrightGlow2.Value) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.BottomProgressBar.Value) + .DisposeWith(dispose); + this.WhenAny(x => x.ProgressPercent) + .BindToStrict(this, x => x.BottomProgressBarHighlight.Value) + .DisposeWith(dispose); - this.WhenAny(x => x.OverhangShadow) - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, x => x.OverhangShadowRect.Visibility) - .DisposeWith(dispose); - this.WhenAny(x => x.ShadowMargin) - .DistinctUntilChanged() - .Select(x => x ? new Thickness(6, 0, 6, 0) : new Thickness(0)) - .BindToStrict(this, x => x.OverhangShadowRect.Margin) - .DisposeWith(dispose); - this.WhenAny(x => x.Title) - .BindToStrict(this, x => x.TitleText.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.StatePrefixTitle) - .Select(x => x == null ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, x => x.PrefixSpacerRect.Visibility) - .DisposeWith(dispose); - this.WhenAny(x => x.StatePrefixTitle) - .Select(x => x == null ? Visibility.Collapsed : Visibility.Visible) - .BindToStrict(this, x => x.StatePrefixText.Visibility) - .DisposeWith(dispose); - this.WhenAny(x => x.StatePrefixTitle) - .BindToStrict(this, x => x.StatePrefixText.Text) - .DisposeWith(dispose); - }); - } + this.WhenAny(x => x.OverhangShadow) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, x => x.OverhangShadowRect.Visibility) + .DisposeWith(dispose); + this.WhenAny(x => x.ShadowMargin) + .DistinctUntilChanged() + .Select(x => x ? new Thickness(6, 0, 6, 0) : new Thickness(0)) + .BindToStrict(this, x => x.OverhangShadowRect.Margin) + .DisposeWith(dispose); + this.WhenAny(x => x.Title) + .BindToStrict(this, x => x.TitleText.Text) + .DisposeWith(dispose); + this.WhenAny(x => x.StatePrefixTitle) + .Select(x => x == null ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, x => x.PrefixSpacerRect.Visibility) + .DisposeWith(dispose); + this.WhenAny(x => x.StatePrefixTitle) + .Select(x => x == null ? Visibility.Collapsed : Visibility.Visible) + .BindToStrict(this, x => x.StatePrefixText.Visibility) + .DisposeWith(dispose); + this.WhenAny(x => x.StatePrefixTitle) + .BindToStrict(this, x => x.StatePrefixText.Text) + .DisposeWith(dispose); + }); } } diff --git a/Wabbajack.App.Wpf/Views/Common/UnderMaintenanceOverlay.xaml b/Wabbajack.App.Wpf/Views/Common/UnderMaintenanceOverlay.xaml index 55ba853b9..5fc7f3a75 100644 --- a/Wabbajack.App.Wpf/Views/Common/UnderMaintenanceOverlay.xaml +++ b/Wabbajack.App.Wpf/Views/Common/UnderMaintenanceOverlay.xaml @@ -21,47 +21,49 @@ - + - + + Margin="10" + Visibility="{Binding ElementName=MaintenanceGrid,Path=IsMouseOver, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=True}"> + Visibility="{Binding Path=IsMouseOver, ElementName=MaintenanceGrid, Converter={StaticResource bool2VisibilityConverter}, ConverterParameter=False}"> @@ -72,7 +74,7 @@ - +/// Interaction logic for UnderMaintenanceOverlay.xaml +/// +public partial class UnderMaintenanceOverlay : UserControl { - /// - /// Interaction logic for UnderMaintenanceOverlay.xaml - /// - public partial class UnderMaintenanceOverlay : UserControl + public UnderMaintenanceOverlay() { - public bool ShowHelp - { - get => (bool)GetValue(ShowHelpProperty); - set => SetValue(ShowHelpProperty, value); - } - public static readonly DependencyProperty ShowHelpProperty = DependencyProperty.Register(nameof(ShowHelp), typeof(bool), typeof(UnderMaintenanceOverlay), - new FrameworkPropertyMetadata(default(bool))); - - public UnderMaintenanceOverlay() - { - InitializeComponent(); - } - - private void Help_Click(object sender, RoutedEventArgs e) - { - ShowHelp = !ShowHelp; - } + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Common/WJButton.xaml b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml new file mode 100644 index 000000000..3bdfb32ce --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml @@ -0,0 +1,22 @@ + diff --git a/Wabbajack.App.Wpf/Views/Common/WJButton.xaml.cs b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml.cs new file mode 100644 index 000000000..620f8d152 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml.cs @@ -0,0 +1,181 @@ +using FluentIcons.Common; +using ReactiveUI; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows; +using System; +using System.Windows.Input; +using Wabbajack.RateLimiter; +using System.Windows.Media; +using ReactiveUI.Fody.Helpers; +using System.Windows.Controls; +using System.ComponentModel; + +namespace Wabbajack; + +/// +/// Interaction logic for WJButton.xaml +/// +public enum ButtonStyle +{ + Mono, + Color, + Danger, + Progress +} +public partial class WJButtonVM : ViewModel +{ +} + +public partial class WJButton : Button, IViewFor, IReactiveObject +{ + private string _text; + + public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangingEventHandler PropertyChanging; + + public string Text + { + get => _text; + set + { + this.RaiseAndSetIfChanged(ref _text, value); + RaisePropertyChanged(new PropertyChangedEventArgs(nameof(Content))); + } + } + [Reactive] public Symbol Icon { get; set; } + [Reactive] public double IconSize { get; set; } = 24D; + [Reactive] public FlowDirection Direction { get; set; } + [Reactive] public ButtonStyle ButtonStyle { get; set; } + + private Percent _progressPercentage = Percent.One; + public Percent ProgressPercentage + { + get => _progressPercentage; + set + { + this.RaiseAndSetIfChanged(ref _progressPercentage, value); + } + } + + public WJButtonVM ViewModel { get; set; } + object IViewFor.ViewModel { get => ViewModel; set => ViewModel = (WJButtonVM)value; } + + public WJButton() + { + InitializeComponent(); + this.WhenActivated(dispose => + { + this.WhenAnyValue(x => x.Text) + .BindToStrict(this, x => x.ButtonTextBlock.Text) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.Icon) + .BindToStrict(this, x => x.ButtonSymbolIcon.Symbol) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.Direction) + .Subscribe(x => SetDirection(x)) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.IconSize) + .BindToStrict(this, x => x.ButtonSymbolIcon.FontSize) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ButtonStyle) + .Subscribe(x => Style = x switch + { + ButtonStyle.Mono => (Style)Application.Current.Resources["WJButtonStyle"], + ButtonStyle.Color => (Style)Application.Current.Resources["WJColorButtonStyle"], + ButtonStyle.Danger => (Style)Application.Current.Resources["WJDangerButtonStyle"], + ButtonStyle.Progress => (Style)Application.Current.Resources["WJColorButtonStyle"], + _ => (Style)Application.Current.Resources["WJButtonStyle"], + }) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ProgressPercentage) + .Subscribe(percent => + { + if (ButtonStyle != ButtonStyle.Progress) return; + if (percent == Percent.One) + { + Background = new SolidColorBrush((Color)Application.Current.Resources["Primary"]); + Foreground = new SolidColorBrush((Color)Application.Current.Resources["BackgroundColor"]); + } + else if (percent == Percent.Zero) + { + Background = new SolidColorBrush((Color)Application.Current.Resources["ComplementaryPrimary08"]); + Foreground = new SolidColorBrush((Color)Application.Current.Resources["ForegroundColor"]); + } + else + { + var bgBrush = new LinearGradientBrush(); + + bgBrush.StartPoint = new Point(0, 0); + bgBrush.EndPoint = new Point(1, 0); + bgBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["Primary"], 0.0)); + bgBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["Primary"], percent.Value)); + bgBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["ComplementaryPrimary08"], percent.Value + 0.001)); + bgBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["ComplementaryPrimary08"], 1.0)); + Background = bgBrush; + + var textBrush = new LinearGradientBrush(); + var textStartPercent = 1 - (ActualWidth - ButtonTextBlock.Margin.Left) / ActualWidth; + var textModifier = ActualWidth / (ActualWidth - ButtonTextBlock.Margin.Left); + var textPercent = percent.Value < textStartPercent ? 0 : (percent.Value - textStartPercent) * textModifier; + // Since the text has a smaller width compared to the background of the whole button, we need to scale the gradient to the same bounds + textBrush.RelativeTransform = new ScaleTransform(ActualWidth / ButtonTextBlock.ActualWidth, 1); + textBrush.StartPoint = new Point(0, 0); + textBrush.EndPoint = new Point(1, 0); + textBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["BackgroundColor"], 0.0)); + textBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["BackgroundColor"], textPercent)); + textBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["DisabledForegroundColor"], textPercent + 0.001)); + textBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["DisabledForegroundColor"], 1.0)); + ButtonTextBlock.Foreground = textBrush; + + var iconBrush = new LinearGradientBrush(); + var iconStartPercent = (ActualWidth - ButtonSymbolIcon.ActualWidth - ButtonSymbolIcon.Margin.Right) / ActualWidth; + var iconModifier = ActualWidth / (ActualWidth - ButtonSymbolIcon.ActualWidth - ButtonSymbolIcon.Margin.Right); + var iconPercent = percent.Value < iconStartPercent ? 0 : (percent.Value - iconStartPercent) * iconModifier; + iconBrush.RelativeTransform = new ScaleTransform(ActualWidth / ButtonSymbolIcon.ActualWidth, 1); + iconBrush.StartPoint = new Point(0, 0); + iconBrush.EndPoint = new Point(1, 0); + iconBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["BackgroundColor"], 0.0)); + iconBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["BackgroundColor"], iconPercent)); + iconBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["DisabledForegroundColor"], iconPercent + 0.001)); + iconBrush.GradientStops.Add(new GradientStop((Color)Application.Current.Resources["DisabledForegroundColor"], 1.0)); + ButtonSymbolIcon.Foreground = iconBrush; + } + }).DisposeWith(dispose); + }); + + } + + private void SetDirection(FlowDirection direction) + { + if (direction == FlowDirection.LeftToRight) + { + ButtonTextBlock.Margin = new Thickness(16, 0, 0, 0); + ButtonTextBlock.HorizontalAlignment = HorizontalAlignment.Left; + ButtonSymbolIcon.Margin = new Thickness(0, 0, 16, 0); + ButtonSymbolIcon.HorizontalAlignment = HorizontalAlignment.Right; + } + else + { + ButtonTextBlock.Margin = new Thickness(0, 0, 16, 0); + ButtonTextBlock.HorizontalAlignment = HorizontalAlignment.Right; + ButtonSymbolIcon.Margin = new Thickness(16, 0, 0, 0); + ButtonSymbolIcon.HorizontalAlignment = HorizontalAlignment.Left; + } + } + + public void RaisePropertyChanging(PropertyChangingEventArgs args) + { + PropertyChanging?.Invoke(this, args); + } + + public void RaisePropertyChanged(PropertyChangedEventArgs args) + { + PropertyChanged?.Invoke(this, args); + } +} diff --git a/Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml similarity index 98% rename from Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml rename to Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml index 0a1df3fdc..170d370ef 100644 --- a/Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml @@ -9,7 +9,7 @@ xmlns:rxui="http://reactiveui.net" d:DesignHeight="450" d:DesignWidth="800" - x:TypeArguments="local:CompilerVM" + x:TypeArguments="local:CompilerDetailsVM" mc:Ignorable="d"> @@ -26,7 +26,7 @@ x:Name="TitleText" HorizontalAlignment="Center" VerticalAlignment="Bottom" - FontFamily="Lucida Sans" + FontFamily="{StaticResource PrimaryFont}" FontSize="22" FontWeight="Black"> diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml.cs new file mode 100644 index 000000000..b783176b3 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml.cs @@ -0,0 +1,13 @@ +namespace Wabbajack; + +/// +/// Interaction logic for CompilationCompleteView.xaml +/// +public partial class CompilationCompleteView +{ + public CompilationCompleteView() + { + InitializeComponent(); + + } +} diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml new file mode 100644 index 000000000..e64fd33a4 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml.cs new file mode 100644 index 000000000..9873ce388 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml.cs @@ -0,0 +1,38 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows; +using ReactiveUI; +using ReactiveMarbles.ObservableEvents; +using System.Reactive; + +namespace Wabbajack; + +/// +/// Interaction logic for CreateModListTileView.xaml +/// +public partial class CompiledModListTileView : ReactiveUserControl +{ + public CompiledModListTileView() + { + InitializeComponent(); + this.WhenActivated(dispose => + { + ViewModel.WhenAnyValue(vm => vm.CompilerSettings.ModListImage) + .Select(imagePath => { UIUtils.TryGetBitmapImageFromFile(imagePath, out var bitmapImage); return bitmapImage; }) + .BindToStrict(this, v => v.ModlistImage.ImageSource) + .DisposeWith(dispose); + + CompiledModListTile + .Events().MouseDown + .Select(args => Unit.Default) + .InvokeCommand(this, x => x.ViewModel.CompileModListCommand) + .DisposeWith(dispose); + + + ViewModel.WhenAnyValue(x => x.LoadingImageLock.IsLoading) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, x => x.LoadingProgress.Visibility) + .DisposeWith(dispose); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml new file mode 100644 index 000000000..a381b99bd --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml.cs new file mode 100644 index 000000000..b88a0d2c5 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml.cs @@ -0,0 +1,307 @@ +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using ReactiveUI; +using DynamicData; +using Microsoft.WindowsAPICodePack.Dialogs; +using Wabbajack.Common; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using System.Collections.Generic; + +namespace Wabbajack; + +/// +/// Interaction logic for CompilerDetailsView.xaml +/// +public partial class CompilerDetailsView : ReactiveUserControl +{ + public CompilerDetailsView() + { + InitializeComponent(); + + this.WhenActivated(disposables => + { + this.Bind(ViewModel, vm => vm.Settings.ModListName, view => view.ModListNameSetting.Text) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.ModListAuthor, view => view.AuthorNameSetting.Text) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.Version, view => view.VersionSetting.Text) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.ModListDescription, view => view.DescriptionSetting.Text) + .DisposeWith(disposables); + + + this.Bind(ViewModel, vm => vm.ModListImageLocation, view => view.ImageFilePicker.PickerVM) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.ModListImage, view => view.ImageFilePicker.PickerVM.TargetPath) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.ModListWebsite, view => view.WebsiteSetting.Text) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.ModListReadme, view => view.ReadmeSetting.Text) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.ModlistIsNSFW, view => view.NSFWSetting.IsChecked) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.UseTextureRecompression, view => view.TextureRecompressionSetting.IsChecked) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.AvailableProfiles) + .BindToStrict(this, view => view.ProfileSetting.ItemsSource) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.Profile, view => view.ProfileSetting.SelectedItem) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(v => v.AvailableProfiles, v => v.Settings.Profile) + .Select((x) => x.Item1.Except([x.Item2]).ToList()) + .BindToStrict(this, x => x.AdditionalProfilesSetting.ItemsSource) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.MachineUrl, view => view.MachineUrl.Text) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.OutputLocation, view => view.OutputFilePicker.PickerVM) + .DisposeWith(disposables); + + this.Bind(ViewModel, vm => vm.Settings.OutputFile, view => view.OutputFilePicker.PickerVM.TargetPath) + .DisposeWith(disposables); + }); + + } + + public async Task AddAlwaysEnabledCommand() + { + AbsolutePath dirPath; + + if (ViewModel!.Settings.Source != default && ViewModel.Settings.Source.Combine("mods").DirectoryExists()) + { + dirPath = ViewModel.Settings.Source.Combine("mods"); + } + else + { + dirPath = ViewModel.Settings.Source; + } + + var dlg = new CommonOpenFileDialog + { + Title = "Please select a folder", + IsFolderPicker = true, + InitialDirectory = dirPath.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = dirPath.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + foreach (var fileName in dlg.FileNames) + { + var selectedPath = fileName.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source)) continue; + + ViewModel.AddAlwaysEnabled(selectedPath.RelativeTo(ViewModel.Settings.Source)); + } + } + + public async Task AddOtherProfileCommand() + { + AbsolutePath dirPath; + + if (ViewModel!.Settings.Source != default && ViewModel.Settings.Source.Combine("mods").DirectoryExists()) + { + dirPath = ViewModel.Settings.Source.Combine("mods"); + } + else + { + dirPath = ViewModel.Settings.Source; + } + + var dlg = new CommonOpenFileDialog + { + Title = "Please select a profile folder", + IsFolderPicker = true, + InitialDirectory = dirPath.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = dirPath.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + foreach (var filename in dlg.FileNames) + { + var selectedPath = filename.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source.Combine("profiles"))) continue; + + ViewModel.AddOtherProfile(selectedPath.FileName.ToString()); + } + } + + public Task AddNoMatchIncludeCommand() + { + var dlg = new CommonOpenFileDialog + { + Title = "Please select a folder", + IsFolderPicker = true, + InitialDirectory = ViewModel!.Settings.Source.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = ViewModel!.Settings.Source.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return Task.CompletedTask; + foreach (var filename in dlg.FileNames) + { + var selectedPath = filename.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source)) continue; + + ViewModel.AddNoMatchInclude(selectedPath.RelativeTo(ViewModel!.Settings.Source)); + } + + return Task.CompletedTask; + } + + public async Task AddIncludeCommand() + { + var dlg = new CommonOpenFileDialog + { + Title = "Please select folders to include", + IsFolderPicker = true, + InitialDirectory = ViewModel!.Settings.Source.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = ViewModel!.Settings.Source.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + foreach (var filename in dlg.FileNames) + { + var selectedPath = filename.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source)) continue; + + ViewModel.AddInclude(selectedPath.RelativeTo(ViewModel!.Settings.Source)); + } + } + + public async Task AddIncludeFilesCommand() + { + var dlg = new CommonOpenFileDialog + { + Title = "Please select files to include", + IsFolderPicker = false, + InitialDirectory = ViewModel!.Settings.Source.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = ViewModel!.Settings.Source.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + foreach (var filename in dlg.FileNames) + { + var selectedPath = filename.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source)) continue; + + ViewModel.AddInclude(selectedPath.RelativeTo(ViewModel!.Settings.Source)); + } + } + + public async Task AddIgnoreCommand() + { + var dlg = new CommonOpenFileDialog + { + Title = "Please select folders to ignore", + IsFolderPicker = true, + InitialDirectory = ViewModel!.Settings.Source.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = ViewModel!.Settings.Source.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + foreach (var filename in dlg.FileNames) + { + var selectedPath = filename.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source)) continue; + + ViewModel.AddIgnore(selectedPath.RelativeTo(ViewModel!.Settings.Source)); + } + } + + public async Task AddIgnoreFilesCommand() + { + var dlg = new CommonOpenFileDialog + { + Title = "Please select files to ignore", + IsFolderPicker = false, + InitialDirectory = ViewModel!.Settings.Source.ToString(), + AddToMostRecentlyUsedList = false, + AllowNonFileSystemItems = false, + DefaultDirectory = ViewModel!.Settings.Source.ToString(), + EnsureFileExists = true, + EnsurePathExists = true, + EnsureReadOnly = false, + EnsureValidNames = true, + Multiselect = true, + ShowPlacesList = true, + }; + + if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; + foreach (var filename in dlg.FileNames) + { + var selectedPath = filename.ToAbsolutePath(); + + if (!selectedPath.InFolder(ViewModel.Settings.Source)) continue; + + ViewModel.AddIgnore(selectedPath.RelativeTo(ViewModel!.Settings.Source)); + } + } +} diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerFileManagerView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompilerFileManagerView.xaml new file mode 100644 index 000000000..6883fb6dd --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerFileManagerView.xaml @@ -0,0 +1,30 @@ + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerFileManagerView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/CompilerFileManagerView.xaml.cs new file mode 100644 index 000000000..006dee457 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerFileManagerView.xaml.cs @@ -0,0 +1,25 @@ +using System.Reactive.Disposables; +using ReactiveUI; + +namespace Wabbajack; + +/// +/// Interaction logic for CompilerFileManagerView.xaml +/// +public partial class CompilerFileManagerView : ReactiveUserControl +{ + public CompilerFileManagerView() + { + InitializeComponent(); + + + this.WhenActivated(disposables => + { + this.WhenAny(x => x.ViewModel.Files) + .BindToStrict(this, v => v.FileTreeView.ItemsSource) + .DisposeWith(disposables); + }); + + } + +} diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerHomeView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompilerHomeView.xaml new file mode 100644 index 000000000..33f4ba6b3 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerHomeView.xaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Recently Compiled Modlists + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerHomeView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/CompilerHomeView.xaml.cs new file mode 100644 index 000000000..d3906a787 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerHomeView.xaml.cs @@ -0,0 +1,41 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Windows.Controls; +using ReactiveUI; +using System.Windows; +using Wabbajack.Common; +using ReactiveMarbles.ObservableEvents; +using System.Reactive; +using System.Windows.Automation.Peers; + +namespace Wabbajack; + +/// +/// Interaction logic for CreateModList.xaml +/// +public partial class CompilerHomeView : ReactiveUserControl +{ + public CompilerHomeView() + { + InitializeComponent(); + + this.WhenActivated(dispose => + { + this.WhenAnyValue(x => x.ViewModel.CompiledModLists) + .BindToStrict(this, x => x.CompiledModListsControl.ItemsSource) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ViewModel.NewModlistCommand) + .BindToStrict(this, x => x.NewModlistButton.Command) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ViewModel.LoadSettingsCommand) + .BindToStrict(this, x => x.LoadSettingsButton.Command) + .DisposeWith(dispose); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml new file mode 100644 index 000000000..87f40695d --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml.cs new file mode 100644 index 000000000..867c82219 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml.cs @@ -0,0 +1,120 @@ +using System.Linq; +using System.Reactive.Linq; +using ReactiveUI; +using Wabbajack.Common; +using Wabbajack.Paths.IO; +using System.Windows; +using System.Reactive.Disposables; +using System; +using System.Windows.Media.Imaging; + +namespace Wabbajack; + +/// +/// Interaction logic for CompilingView.xaml +/// +public partial class CompilerMainView : ReactiveUserControl +{ + public CompilerMainView() + { + InitializeComponent(); + + this.WhenActivated(disposables => + { + ViewModel.WhenAny(vm => vm.Settings.ModListImage) + .Where(i => i.FileExists()) + .Select(i => (UIUtils.TryGetBitmapImageFromFile(i, out var img), img)) + .Subscribe(x => + { + bool success = x.Item1; + + if(success) + { + CompiledImage.Image = DetailImage.Image = x.img; + } + }) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.Settings.ModListName) + .BindToStrict(this, view => view.DetailImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.Settings.ModListAuthor) + .BindToStrict(this, view => view.DetailImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.Settings.ModListName) + .BindToStrict(this, view => view.CompiledImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.Settings.ModListAuthor) + .BindToStrict(this, view => view.CompiledImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Configuration ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.CompilerDetailsView.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Configuration ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.FileManager.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Configuration ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.ConfigurationButtons.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Compiling || s == CompilerState.Errored ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.LogView.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Compiling ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.CpuView.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Compiling ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.CompilationButtons.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Completed) + .BindToStrict(this, view => view.OpenFolderButton.IsEnabled) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Completed) + .BindToStrict(this, view => view.PublishButton.IsEnabled) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Completed ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.CompiledImage.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.State) + .Select(s => s == CompilerState.Completed ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.CompletedButtons.Visibility) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.StartCommand, x => x.StartButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.CancelCommand, x => x.CancelButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.OpenLogCommand, x => x.OpenLogButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.OpenFolderCommand, x => x.OpenFolderButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.PublishCommand, x => x.PublishButton) + .DisposeWith(disposables); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml b/Wabbajack.App.Wpf/Views/Compiler/MO2CompilerConfigView.xaml similarity index 74% rename from Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml rename to Wabbajack.App.Wpf/Views/Compiler/MO2CompilerConfigView.xaml index 21cd04383..82be787a7 100644 --- a/Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml +++ b/Wabbajack.App.Wpf/Views/Compiler/MO2CompilerConfigView.xaml @@ -23,7 +23,7 @@ @@ -32,12 +32,12 @@ Grid.Row="0" Grid.Column="2" Height="30" VerticalAlignment="Center" - FontSize="14" + FontSize="13" ToolTip="The MO2 modlist.txt file you want to use as your source" /> @@ -45,20 +45,7 @@ x:Name="DownloadsLocation" Height="30" VerticalAlignment="Center" - FontSize="14" + FontSize="13" ToolTip="The folder where MO2 downloads your mods." /> - - diff --git a/Wabbajack.App.Wpf/Views/Compiler/MO2CompilerConfigView.xaml.cs b/Wabbajack.App.Wpf/Views/Compiler/MO2CompilerConfigView.xaml.cs new file mode 100644 index 000000000..ea9de7e8d --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/MO2CompilerConfigView.xaml.cs @@ -0,0 +1,14 @@ +using System.Windows.Controls; + +namespace Wabbajack; + +/// +/// Interaction logic for MO2CompilerConfigView.xaml +/// +public partial class MO2CompilerConfigView : UserControl +{ + public MO2CompilerConfigView() + { + InitializeComponent(); + } +} diff --git a/Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml.cs b/Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml.cs deleted file mode 100644 index 3bfb39565..000000000 --- a/Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Reactive.Disposables; -using System.Reactive.Linq; -using ReactiveUI; - -namespace Wabbajack -{ - /// - /// Interaction logic for CompilationCompleteView.xaml - /// - public partial class CompilationCompleteView - { - public CompilationCompleteView() - { - InitializeComponent(); - - } - } -} diff --git a/Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml b/Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml deleted file mode 100644 index b88638c32..000000000 --- a/Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml +++ /dev/null @@ -1,296 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml.cs b/Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml.cs deleted file mode 100644 index b5abf0f2f..000000000 --- a/Wabbajack.App.Wpf/Views/Compilers/CompilerView.xaml.cs +++ /dev/null @@ -1,465 +0,0 @@ -using System; -using System.Diagnostics.Eventing.Reader; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading.Tasks; -using System.Windows.Controls; -using ReactiveUI; -using System.Windows; -using System.Windows.Forms; -using DynamicData; -using Microsoft.WindowsAPICodePack.Dialogs; -using Wabbajack.Common; -using Wabbajack.Paths; -using Wabbajack.Paths.IO; -using Wabbajack.View_Models.Controls; - -namespace Wabbajack -{ - /// - /// Interaction logic for CompilerView.xaml - /// - public partial class CompilerView : ReactiveUserControl - { - public CompilerView() - { - InitializeComponent(); - - this.WhenActivated(disposables => - { - ViewModel.WhenAny(vm => vm.State) - .Select(x => x == CompilerState.Errored) - .BindToStrict(this, x => x.CompilationComplete.AttentionBorder.Failure) - .DisposeWith(disposables); - - ViewModel.WhenAny(vm => vm.State) - .Select(x => x == CompilerState.Errored) - .Select(failed => $"Compilation {(failed ? "Failed" : "Complete")}") - .BindToStrict(this, x => x.CompilationComplete.TitleText.Text) - .DisposeWith(disposables); - - ViewModel.WhenAny(vm => vm.ModListImagePath.TargetPath) - .Where(i => i.FileExists()) - .Select(i => (UIUtils.TryGetBitmapImageFromFile(i, out var img), img)) - .Where(i => i.Item1) - .Select(i => i.img) - .BindToStrict(this, view => view.DetailImage.Image); - - ViewModel.WhenAny(vm => vm.ModListName) - .BindToStrict(this, view => view.DetailImage.Title); - - ViewModel.WhenAny(vm => vm.Author) - .BindToStrict(this, view => view.DetailImage.Author); - - ViewModel.WhenAny(vm => vm.Description) - .BindToStrict(this, view => view.DetailImage.Description); - - CompilationComplete.GoToModlistButton.Command = ReactiveCommand.Create(() => - { - UIUtils.OpenFolder(ViewModel.OutputLocation.TargetPath); - }).DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.BackCommand) - .BindToStrict(this, view => view.CompilationComplete.BackButton.Command) - .DisposeWith(disposables); - - CompilationComplete.CloseWhenCompletedButton.Command = ReactiveCommand.Create(() => - { - Environment.Exit(0); - }).DisposeWith(disposables); - - - ViewModel.WhenAnyValue(vm => vm.ExecuteCommand) - .BindToStrict(this, view => view.BeginButton.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.BackCommand) - .BindToStrict(this, view => view.BackButton.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.ReInferSettingsCommand) - .BindToStrict(this, view => view.ReInferSettings.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.State) - .Select(v => v == CompilerState.Configuration ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.BottomCompilerSettingsGrid.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.State) - .Select(v => v != CompilerState.Configuration ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.LogView.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.State) - .Select(v => v == CompilerState.Compiling ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.CpuView.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.State) - .Select(v => v is CompilerState.Completed or CompilerState.Errored ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.CompilationComplete.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.ModlistLocation) - .BindToStrict(this, view => view.CompilerConfigView.ModListLocation.PickerVM) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.DownloadLocation) - .BindToStrict(this, view => view.CompilerConfigView.DownloadsLocation.PickerVM) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.OutputLocation) - .BindToStrict(this, view => view.CompilerConfigView.OutputLocation.PickerVM) - .DisposeWith(disposables); - - UserInterventionsControl.Visibility = Visibility.Collapsed; - - // Errors - this.WhenAnyValue(view => view.ViewModel.ErrorState) - .Select(x => !x.Failed) - .BindToStrict(this, view => view.BeginButton.IsEnabled) - .DisposeWith(disposables); - - this.WhenAnyValue(view => view.ViewModel.ErrorState) - .Select(x => x.Failed ? Visibility.Visible : Visibility.Hidden) - .BindToStrict(this, view => view.ErrorSummaryIcon.Visibility) - .DisposeWith(disposables); - - this.WhenAnyValue(view => view.ViewModel.ErrorState) - .Select(x => x.Failed ? Visibility.Visible : Visibility.Hidden) - .BindToStrict(this, view => view.ErrorSummaryIconGlow.Visibility) - .DisposeWith(disposables); - - this.WhenAnyValue(view => view.ViewModel.ErrorState) - .Select(x => x.Reason) - .BindToStrict(this, view => view.ErrorSummaryIcon.ToolTip) - .DisposeWith(disposables); - - - - - - // Settings - - this.Bind(ViewModel, vm => vm.ModListName, view => view.ModListNameSetting.Text) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.SelectedProfile, view => view.SelectedProfile.Text) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Author, view => view.AuthorNameSetting.Text) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Version, view => view.VersionSetting.Text) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Description, view => view.DescriptionSetting.Text) - .DisposeWith(disposables); - - - this.Bind(ViewModel, vm => vm.ModListImagePath, view => view.ImageFilePicker.PickerVM) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Website, view => view.WebsiteSetting.Text) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.Readme, view => view.ReadmeSetting.Text) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.IsNSFW, view => view.NSFWSetting.IsChecked) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.PublishUpdate, view => view.PublishUpdate.IsChecked) - .DisposeWith(disposables); - - this.Bind(ViewModel, vm => vm.MachineUrl, view => view.MachineUrl.Text) - .DisposeWith(disposables); - - - ViewModel.WhenAnyValue(vm => vm.StatusText) - .BindToStrict(this, view => view.TopProgressBar.Title) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.StatusProgress) - .Select(d => d.Value) - .BindToStrict(this, view => view.TopProgressBar.ProgressPercent) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.AlwaysEnabled) - .WhereNotNull() - .Select(itms => itms.Select(itm => new RemovableItemViewModel(itm.ToString(), () => ViewModel.RemoveAlwaysEnabled(itm))).ToArray()) - .BindToStrict(this, view => view.AlwaysEnabled.ItemsSource) - .DisposeWith(disposables); - - AddAlwaysEnabled.Command = ReactiveCommand.CreateFromTask(async () => await AddAlwaysEnabledCommand()); - - - ViewModel.WhenAnyValue(vm => vm.OtherProfiles) - .WhereNotNull() - .Select(itms => itms.Select(itm => new RemovableItemViewModel(itm.ToString(), () => ViewModel.RemoveProfile(itm))).ToArray()) - .BindToStrict(this, view => view.OtherProfiles.ItemsSource) - .DisposeWith(disposables); - - AddOtherProfile.Command = ReactiveCommand.CreateFromTask(async () => await AddOtherProfileCommand()); - - ViewModel.WhenAnyValue(vm => vm.NoMatchInclude) - .WhereNotNull() - .Select(itms => itms.Select(itm => new RemovableItemViewModel(itm.ToString(), () => ViewModel.RemoveNoMatchInclude(itm))).ToArray()) - .BindToStrict(this, view => view.NoMatchInclude.ItemsSource) - .DisposeWith(disposables); - - AddNoMatchInclude.Command = ReactiveCommand.CreateFromTask(async () => await AddNoMatchIncludeCommand()); - - ViewModel.WhenAnyValue(vm => vm.Include) - .WhereNotNull() - .Select(itms => itms.Select(itm => new RemovableItemViewModel(itm.ToString(), () => ViewModel.RemoveInclude(itm))).ToArray()) - .BindToStrict(this, view => view.Include.ItemsSource) - .DisposeWith(disposables); - - AddInclude.Command = ReactiveCommand.CreateFromTask(async () => await AddIncludeCommand()); - AddIncludeFiles.Command = ReactiveCommand.CreateFromTask(async () => await AddIncludeFilesCommand()); - - ViewModel.WhenAnyValue(vm => vm.Ignore) - .WhereNotNull() - .Select(itms => itms.Select(itm => new RemovableItemViewModel(itm.ToString(), () => ViewModel.RemoveIgnore(itm))).ToArray()) - .BindToStrict(this, view => view.Ignore.ItemsSource) - .DisposeWith(disposables); - - AddIgnore.Command = ReactiveCommand.CreateFromTask(async () => await AddIgnoreCommand()); - AddIgnoreFiles.Command = ReactiveCommand.CreateFromTask(async () => await AddIgnoreFilesCommand()); - - - }); - - } - - public async Task AddAlwaysEnabledCommand() - { - AbsolutePath dirPath; - - if (ViewModel!.Source != default && ViewModel.Source.Combine("mods").DirectoryExists()) - { - dirPath = ViewModel.Source.Combine("mods"); - } - else - { - dirPath = ViewModel.Source; - } - - var dlg = new CommonOpenFileDialog - { - Title = "Please select a folder", - IsFolderPicker = true, - InitialDirectory = dirPath.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = dirPath.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - foreach (var fileName in dlg.FileNames) - { - var selectedPath = fileName.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source)) continue; - - ViewModel.AddAlwaysEnabled(selectedPath.RelativeTo(ViewModel.Source)); - } - } - - public async Task AddOtherProfileCommand() - { - AbsolutePath dirPath; - - if (ViewModel!.Source != default && ViewModel.Source.Combine("mods").DirectoryExists()) - { - dirPath = ViewModel.Source.Combine("mods"); - } - else - { - dirPath = ViewModel.Source; - } - - var dlg = new CommonOpenFileDialog - { - Title = "Please select a profile folder", - IsFolderPicker = true, - InitialDirectory = dirPath.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = dirPath.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - foreach (var filename in dlg.FileNames) - { - var selectedPath = filename.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source.Combine("profiles"))) continue; - - ViewModel.AddOtherProfile(selectedPath.FileName.ToString()); - } - } - - public Task AddNoMatchIncludeCommand() - { - var dlg = new CommonOpenFileDialog - { - Title = "Please select a folder", - IsFolderPicker = true, - InitialDirectory = ViewModel!.Source.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = ViewModel!.Source.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return Task.CompletedTask; - foreach (var filename in dlg.FileNames) - { - var selectedPath = filename.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source)) continue; - - ViewModel.AddNoMatchInclude(selectedPath.RelativeTo(ViewModel!.Source)); - } - - return Task.CompletedTask; - } - - public async Task AddIncludeCommand() - { - var dlg = new CommonOpenFileDialog - { - Title = "Please select folders to include", - IsFolderPicker = true, - InitialDirectory = ViewModel!.Source.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = ViewModel!.Source.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - foreach (var filename in dlg.FileNames) - { - var selectedPath = filename.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source)) continue; - - ViewModel.AddInclude(selectedPath.RelativeTo(ViewModel!.Source)); - } - } - - public async Task AddIncludeFilesCommand() - { - var dlg = new CommonOpenFileDialog - { - Title = "Please select files to include", - IsFolderPicker = false, - InitialDirectory = ViewModel!.Source.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = ViewModel!.Source.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - foreach (var filename in dlg.FileNames) - { - var selectedPath = filename.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source)) continue; - - ViewModel.AddInclude(selectedPath.RelativeTo(ViewModel!.Source)); - } - } - - public async Task AddIgnoreCommand() - { - var dlg = new CommonOpenFileDialog - { - Title = "Please select folders to ignore", - IsFolderPicker = true, - InitialDirectory = ViewModel!.Source.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = ViewModel!.Source.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - foreach (var filename in dlg.FileNames) - { - var selectedPath = filename.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source)) continue; - - ViewModel.AddIgnore(selectedPath.RelativeTo(ViewModel!.Source)); - } - } - - public async Task AddIgnoreFilesCommand() - { - var dlg = new CommonOpenFileDialog - { - Title = "Please select files to ignore", - IsFolderPicker = false, - InitialDirectory = ViewModel!.Source.ToString(), - AddToMostRecentlyUsedList = false, - AllowNonFileSystemItems = false, - DefaultDirectory = ViewModel!.Source.ToString(), - EnsureFileExists = true, - EnsurePathExists = true, - EnsureReadOnly = false, - EnsureValidNames = true, - Multiselect = true, - ShowPlacesList = true, - }; - - if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - foreach (var filename in dlg.FileNames) - { - var selectedPath = filename.ToAbsolutePath(); - - if (!selectedPath.InFolder(ViewModel.Source)) continue; - - ViewModel.AddIgnore(selectedPath.RelativeTo(ViewModel!.Source)); - } - } - } -} diff --git a/Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml.cs b/Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml.cs deleted file mode 100644 index c67c93991..000000000 --- a/Wabbajack.App.Wpf/Views/Compilers/MO2CompilerConfigView.xaml.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Windows.Controls; - -namespace Wabbajack -{ - /// - /// Interaction logic for MO2CompilerConfigView.xaml - /// - public partial class MO2CompilerConfigView : UserControl - { - public MO2CompilerConfigView() - { - InitializeComponent(); - } - } -} diff --git a/Wabbajack.App.Wpf/Views/HomeView.xaml b/Wabbajack.App.Wpf/Views/HomeView.xaml new file mode 100644 index 000000000..f3ae9f238 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/HomeView.xaml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Go through a series of questions to find a modlist that works for you through our + Wabbakinator quiz, or navigate the gallery yourself and pick something fun. + + + + + + + + + + + + + + + + + Some modlists have steps that you need to take before you install the list, some + don't. Check your list's documentation to see how to get started. + + + + + + + + + + + + + + + + + Pick a destination with enough free space and click the download button. + Heads up; for full automation of Nexus downloads, a premium account is required. + + + + + + + + + + + + + + + + + If your install completed successfully and you're done with the documentation as + well, you're now ready to launch the modlist and play! + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/HomeView.xaml.cs b/Wabbajack.App.Wpf/Views/HomeView.xaml.cs new file mode 100644 index 000000000..d002bfa48 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/HomeView.xaml.cs @@ -0,0 +1,31 @@ +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ReactiveUI; + +namespace Wabbajack; + +/// +/// Interaction logic for ModeSelectionView.xaml +/// +public partial class HomeView : ReactiveUserControl +{ + public HomeView() + { + InitializeComponent(); + var vm = ViewModel; + this.WhenActivated(dispose => + { + this.WhenAnyValue(x => x.ViewModel.Modlists) + .Select(x => x?.Length.ToString() ?? "0") + .BindToStrict(this, x => x.ModlistAmountTextBlock.Text) + .DisposeWith(dispose); + this.WhenAnyValue(x => x.ViewModel.Modlists) + .Select(x => x?.GroupBy(y => y.Game).Count().ToString() ?? "0") + .BindToStrict(this, x => x.GameAmountTextBlock.Text) + .DisposeWith(dispose); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/InfoView.xaml b/Wabbajack.App.Wpf/Views/InfoView.xaml new file mode 100644 index 000000000..c0b7c1220 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/InfoView.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/InfoView.xaml.cs b/Wabbajack.App.Wpf/Views/InfoView.xaml.cs new file mode 100644 index 000000000..7725a9450 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/InfoView.xaml.cs @@ -0,0 +1,21 @@ +using ReactiveUI; +using System.Reactive.Disposables; + +namespace Wabbajack; + +/// +/// Interaction logic for ModeSelectionView.xaml +/// +public partial class InfoView : ReactiveUserControl +{ + public InfoView() + { + InitializeComponent(); + var vm = ViewModel; + this.WhenActivated(dispose => + { + this.BindCommand(ViewModel, x => x.BackCommand, x => x.PrevButton) + .DisposeWith(dispose); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml b/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml index f76f9d150..c9369c62a 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml @@ -9,7 +9,7 @@ xmlns:rxui="http://reactiveui.net" d:DesignHeight="450" d:DesignWidth="800" - x:TypeArguments="local:InstallerVM" + x:TypeArguments="local:InstallationVM" mc:Ignorable="d"> @@ -29,7 +29,7 @@ x:Name="TitleText" HorizontalAlignment="Center" VerticalAlignment="Bottom" - FontFamily="Lucida Sans" + FontFamily="{StaticResource PrimaryFont}" FontSize="22" FontWeight="Black"> diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs index b4000b86d..5a2e3f40b 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs @@ -1,61 +1,44 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for InstallationCompleteView.xaml +/// +public partial class InstallationCompleteView : ReactiveUserControl { - /// - /// Interaction logic for InstallationCompleteView.xaml - /// - public partial class InstallationCompleteView : ReactiveUserControl + public InstallationCompleteView() { - public InstallationCompleteView() + InitializeComponent(); + this.WhenActivated(dispose => { - InitializeComponent(); - this.WhenActivated(dispose => - { - this.WhenAny(x => x.ViewModel.InstallState) - .Select(x => x == InstallState.Failure) - .BindToStrict(this, x => x.AttentionBorder.Failure) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.InstallState) - .Select(x => x == InstallState.Failure) - .Select(failed => $"Installation {(failed ? "Failed" : "Complete")}") - .BindToStrict(this, x => x.TitleText.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.BackCommand) - .BindToStrict(this, x => x.BackButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.GoToInstallCommand) - .BindToStrict(this, x => x.GoToInstallButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.OpenReadmeCommand) - .BindToStrict(this, x => x.OpenReadmeButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.OpenWikiCommand) - .BindToStrict(this, x => x.OpenWikiButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.CloseWhenCompleteCommand) - .BindToStrict(this, x => x.CloseButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.OpenLogsCommand) - .BindToStrict(this, x => x.OpenLogsButton.Command) - .DisposeWith(dispose); - }); - } + this.WhenAny(x => x.ViewModel.InstallState) + .Select(x => x == InstallState.Failure) + .BindToStrict(this, x => x.AttentionBorder.Failure) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.InstallState) + .Select(x => x == InstallState.Failure) + .Select(failed => $"Installation {(failed ? "Failed" : "Complete")}") + .BindToStrict(this, x => x.TitleText.Text) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.GoToInstallCommand) + .BindToStrict(this, x => x.GoToInstallButton.Command) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.OpenReadmeCommand) + .BindToStrict(this, x => x.OpenReadmeButton.Command) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.OpenWikiCommand) + .BindToStrict(this, x => x.OpenWikiButton.Command) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.CloseWhenCompleteCommand) + .BindToStrict(this, x => x.CloseButton.Command) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.OpenLogsCommand) + .BindToStrict(this, x => x.OpenLogsButton.Command) + .DisposeWith(dispose); + }); } } diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml index a7a2e2b5a..9293028ea 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml @@ -9,7 +9,7 @@ xmlns:rxui="http://reactiveui.net" d:DesignHeight="450" d:DesignWidth="800" - x:TypeArguments="local:InstallerVM" + x:TypeArguments="local:InstallationVM" mc:Ignorable="d"> @@ -36,12 +36,12 @@ + FontSize="13" /> diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs index 64e60a205..fe31afbfb 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs @@ -1,82 +1,62 @@ -using System; -using System.Collections.Generic; -using System.Reactive.Disposables; +using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Text; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for InstallationConfigurationView.xaml +/// +public partial class InstallationConfigurationView : ReactiveUserControl { - /// - /// Interaction logic for InstallationConfigurationView.xaml - /// - public partial class InstallationConfigurationView : ReactiveUserControl + public InstallationConfigurationView() { - public InstallationConfigurationView() + InitializeComponent(); + this.WhenActivated(dispose => { - InitializeComponent(); - this.WhenActivated(dispose => - { - this.WhenAny(x => x.ViewModel.Installer.ConfigVisualVerticalOffset) - .Select(i => (double)i) - .BindToStrict(this, x => x.InstallConfigSpacer.Height) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.ModListLocation) - .BindToStrict(this, x => x.ModListLocationPicker.PickerVM) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.Installer) - .BindToStrict(this, x => x.InstallerCustomizationContent.Content) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.BeginCommand) - .BindToStrict(this, x => x.BeginButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.VerifyCommand) - .BindToStrict(this, x => x.VerifyButton.Command) - .DisposeWith(dispose); - this.BindStrict(ViewModel, vm => vm.OverwriteFiles, x => x.OverwriteCheckBox.IsChecked) - .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.Installer.ConfigVisualVerticalOffset) + .Select(i => (double)i) + .BindToStrict(this, x => x.InstallConfigSpacer.Height) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.WabbajackFileLocation) + .BindToStrict(this, x => x.ModListLocationPicker.PickerVM) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.Installer) + .BindToStrict(this, x => x.InstallerCustomizationContent.Content) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.BeginCommand) + .BindToStrict(this, x => x.BeginButton.Command) + .DisposeWith(dispose); + this.BindStrict(ViewModel, vm => vm.OverwriteFiles, x => x.OverwriteCheckBox.IsChecked) + .DisposeWith(dispose); - // Error handling + // Error handling - this.WhenAnyValue(x => x.ViewModel.ErrorState) - .Select(v => !v.Failed) - .BindToStrict(this, view => view.BeginButton.IsEnabled) - .DisposeWith(dispose); - - this.WhenAnyValue(x => x.ViewModel.ErrorState) - .Select(v => !v.Failed) - .BindToStrict(this, view => view.VerifyButton.IsEnabled) - .DisposeWith(dispose); + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => !v.Failed) + .BindToStrict(this, view => view.BeginButton.IsEnabled) + .DisposeWith(dispose); - this.WhenAnyValue(x => x.ViewModel.ErrorState) - .Select(v => v.Reason) - .BindToStrict(this, view => view.errorTextBox.Text) - .DisposeWith(dispose); + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => v.Reason) + .BindToStrict(this, view => view.errorTextBox.Text) + .DisposeWith(dispose); - this.WhenAnyValue(x => x.ViewModel.ErrorState) - .Select(v => v.Failed ? Visibility.Visible : Visibility.Hidden) - .BindToStrict(this, view => view.ErrorSummaryIcon.Visibility) - .DisposeWith(dispose); - - this.WhenAnyValue(x => x.ViewModel.ErrorState) - .Select(v => v.Failed ? Visibility.Visible : Visibility.Hidden) - .BindToStrict(this, view => view.ErrorSummaryIconGlow.Visibility) - .DisposeWith(dispose); - - this.WhenAnyValue(x => x.ViewModel.ErrorState) - .Select(v => v.Reason) - .BindToStrict(this, view => view.ErrorSummaryIcon.ToolTip) - .DisposeWith(dispose); - }); - } + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => v.Failed ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.ErrorSummaryIcon.Visibility) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => v.Failed ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.ErrorSummaryIconGlow.Visibility) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => v.Reason) + .BindToStrict(this, view => view.ErrorSummaryIcon.ToolTip) + .DisposeWith(dispose); + }); } } diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml index 39fc63eed..9a421d77d 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml @@ -11,10 +11,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:rxui="http://reactiveui.net" xmlns:lib1="clr-namespace:Wabbajack" - d:DataContext="{d:DesignInstance local:InstallerVM}" + d:DataContext="{d:DesignInstance local:InstallationVM}" d:DesignHeight="500" d:DesignWidth="800" - x:TypeArguments="local:InstallerVM" + x:TypeArguments="local:InstallationVM" mc:Ignorable="d"> + + + + + + + + + + + + + + + + + + + + + + + + The folder where the list will be installed into. + Choose an empty folder outside Windows-protected areas. + Using an SSD is highly recommended for optimal performance. + + + + + + + The folder where the downloads will be stored. + By default these are stored in a subdirectory of the installation folder, but you can also use a shared folder so previous downloads are reused. + Downloads can be deleted after installation. + + + + + + + - + + - + + + diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs index 871ba5c03..b45594736 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs @@ -1,108 +1,147 @@ using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Windows.Controls; using ReactiveUI; using System.Windows; +using Microsoft.Toolkit.HighPerformance; +using Humanizer; +using System; +using System.IO; +using System.Linq; +using Microsoft.VisualBasic.Devices; +using System.Management; +using System.Text.RegularExpressions; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; +using System.Threading.Tasks; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for InstallationView.xaml +/// +public partial class InstallationView : ReactiveUserControl { - /// - /// Interaction logic for InstallationView.xaml - /// - public partial class InstallationView : ReactiveUserControl + public InstallationView() { - public InstallationView() + InitializeComponent(); + this.WhenActivated(disposables => { - InitializeComponent(); - this.WhenActivated(disposables => - { - //MidInstallDisplayGrid.Visibility = Visibility.Collapsed; - //LogView.Visibility = Visibility.Collapsed; - //CpuView.Visibility = Visibility.Collapsed; - - ViewModel.WhenAnyValue(vm => vm.InstallState) - .Select(v => v != InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.MidInstallDisplayGrid.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.InstallState) - .Select(v => v == InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.BottomButtonInputGrid.Visibility) - .DisposeWith(disposables); + //MidInstallDisplayGrid.Visibility = Visibility.Collapsed; + //LogView.Visibility = Visibility.Collapsed; + //CpuView.Visibility = Visibility.Collapsed; - ViewModel.WhenAnyValue(vm => vm.InstallState) - .Select(es => es is InstallState.Success or InstallState.Failure ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.InstallComplete.Visibility) - .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Installer.Location, view => view.InstallationLocationPicker.PickerVM) + .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.BackCommand) - .BindToStrict(this, view => view.BackButton.Command) - .DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.Installer.DownloadLocation, view => view.DownloadLocationPicker.PickerVM) + .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.InstallState) - .Select(v => v == InstallState.Installing ? Visibility.Collapsed : Visibility.Visible) - .BindToStrict(this, view => view.BackButton.Visibility) + ViewModel.WhenAnyValue(vm => vm.SuggestedInstallFolder) + .ObserveOnGuiThread() + .Subscribe(x => + { + InstallationLocationPicker.Watermark = x; + if (ViewModel?.Installer?.Location != null) + ViewModel.Installer.Location.TargetPath = (AbsolutePath)x; + }) .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.OpenReadmeCommand) - .BindToStrict(this, view => view.OpenReadmePreInstallButton.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.OpenDiscordButton) - .BindToStrict(this, view => view.OpenDiscordPreInstallButton.Command) + ViewModel.WhenAnyValue(vm => vm.SuggestedDownloadFolder) + .ObserveOnGuiThread() + .Subscribe(x => + { + DownloadLocationPicker.Watermark = x; + if (ViewModel?.Installer?.Location != null) + ViewModel.Installer.DownloadLocation.TargetPath = (AbsolutePath)x; + }) .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand) - .BindToStrict(this, view => view.OpenWebsite.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand) - .BindToStrict(this, view => view.VisitWebsitePreInstallButton.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.ShowManifestCommand) - .BindToStrict(this, view => view.ShowManifestPreInstallButton.Command) - .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.LoadingLock.IsLoading) - .Select(loading => loading ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.ModlistLoadingRing.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(vm => vm.BeginCommand) - .BindToStrict(this, view => view.InstallationConfigurationView.BeginButton.Command) - .DisposeWith(disposables); - - // Status - ViewModel.WhenAnyValue(vm => vm.StatusText) - .ObserveOnGuiThread() - .BindToStrict(this, view => view.TopProgressBar.Title) - .DisposeWith(disposables); + /* + ViewModel.WhenAnyValue(vm => vm.Installer) + .Subscribe(x => { + x.Location.TargetPath = (AbsolutePath)InstallationLocationPicker.Watermark; + }) + .DisposeWith(disposables); + */ - ViewModel.WhenAnyValue(vm => vm.StatusProgress) - .ObserveOnGuiThread() - .Select(p => p.Value) - .BindToStrict(this, view => view.TopProgressBar.ProgressPercent) - .DisposeWith(disposables); + /* + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(v => v != InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.MidInstallDisplayGrid.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(v => v == InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.BottomButtonInputGrid.Visibility) + .DisposeWith(disposables); + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(es => es is InstallState.Success or InstallState.Failure ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.InstallComplete.Visibility) + .DisposeWith(disposables); - // Slideshow - ViewModel.WhenAnyValue(vm => vm.SlideShowTitle) - .Select(f => f) - .BindToStrict(this, view => view.DetailImage.Title) - .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.SlideShowAuthor) - .BindToStrict(this, view => view.DetailImage.Author) - .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.SlideShowDescription) - .BindToStrict(this, view => view.DetailImage.Description) - .DisposeWith(disposables); + ViewModel.WhenAnyValue(vm => vm.InstallState) + .Select(v => v == InstallState.Installing ? Visibility.Collapsed : Visibility.Visible) + .BindToStrict(this, view => view.BackButton.Visibility) + .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.SlideShowImage) - .BindToStrict(this, view => view.DetailImage.Image) - .DisposeWith(disposables); + ViewModel.WhenAnyValue(vm => vm.OpenReadmeCommand) + .BindToStrict(this, view => view.OpenReadmePreInstallButton.Command) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.OpenDiscordButton) + .BindToStrict(this, view => view.OpenDiscordPreInstallButton.Command) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand) + .BindToStrict(this, view => view.OpenWebsite.Command) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.VisitModListWebsiteCommand) + .BindToStrict(this, view => view.VisitWebsitePreInstallButton.Command) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ShowManifestCommand) + .BindToStrict(this, view => view.ShowManifestPreInstallButton.Command) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.LoadingLock.IsLoading) + .Select(loading => loading ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.ModlistLoadingRing.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.BeginCommand) + .BindToStrict(this, view => view.InstallationConfigurationView.BeginButton.Command) + .DisposeWith(disposables); + + // Status + ViewModel.WhenAnyValue(vm => vm.ProgressText) + .ObserveOnGuiThread() + .BindToStrict(this, view => view.TopProgressBar.Title) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ProgressPercent) + .ObserveOnGuiThread() + .Select(p => p.Value) + .BindToStrict(this, view => view.TopProgressBar.ProgressPercent) + .DisposeWith(disposables); + + + // Slideshow + ViewModel.WhenAnyValue(vm => vm.SlideShowTitle) + .Select(f => f) + .BindToStrict(this, view => view.DetailImage.Title) + .DisposeWith(disposables); + ViewModel.WhenAnyValue(vm => vm.SlideShowAuthor) + .BindToStrict(this, view => view.DetailImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.SlideShowImage) + .BindToStrict(this, view => view.DetailImage.Image) + .DisposeWith(disposables); + */ - }); - } + }); } } diff --git a/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml b/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml index f47095f33..000f106c4 100644 --- a/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml @@ -23,27 +23,27 @@ diff --git a/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml.cs index fc8607e88..96ab78228 100644 --- a/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/MO2InstallerConfigView.xaml.cs @@ -1,28 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using System.Windows.Controls; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for MO2InstallerConfigView.xaml +/// +public partial class MO2InstallerConfigView : UserControl { - /// - /// Interaction logic for MO2InstallerConfigView.xaml - /// - public partial class MO2InstallerConfigView : UserControl + public MO2InstallerConfigView() { - public MO2InstallerConfigView() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml.cs b/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml.cs index 48cbda2f0..c0e7bdcd5 100644 --- a/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml.cs @@ -1,13 +1,12 @@ using System.Windows.Controls; -namespace Wabbajack +namespace Wabbajack; + +public partial class BethesdaNetLoginView : UserControl { - public partial class BethesdaNetLoginView : UserControl + public BethesdaNetLoginView() { - public BethesdaNetLoginView() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Interventions/ConfirmationInterventionView.xaml b/Wabbajack.App.Wpf/Views/Interventions/ConfirmationInterventionView.xaml index 8437459e1..a740f1d02 100644 --- a/Wabbajack.App.Wpf/Views/Interventions/ConfirmationInterventionView.xaml +++ b/Wabbajack.App.Wpf/Views/Interventions/ConfirmationInterventionView.xaml @@ -24,8 +24,8 @@ +/// Interaction logic for ConfirmationInterventionView.xaml +/// +public partial class ConfirmationInterventionView : ReactiveUserControl { - /// - /// Interaction logic for ConfirmationInterventionView.xaml - /// - public partial class ConfirmationInterventionView : ReactiveUserControl + public ConfirmationInterventionView() { - public ConfirmationInterventionView() + InitializeComponent(); + this.WhenActivated(dispose => { - InitializeComponent(); - this.WhenActivated(dispose => - { - this.WhenAny(x => x.ViewModel.ShortDescription) - .BindToStrict(this, x => x.ShortDescription.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.ExtendedDescription) - .BindToStrict(this, x => x.ExtendedDescription.Text) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.ConfirmCommand) - .BindToStrict(this, x => x.ConfirmButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.CancelCommand) - .BindToStrict(this, x => x.CancelButton.Command) - .DisposeWith(dispose); - }); - } + this.WhenAny(x => x.ViewModel.ShortDescription) + .BindToStrict(this, x => x.ShortDescription.Text) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.ExtendedDescription) + .BindToStrict(this, x => x.ExtendedDescription.Text) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.ConfirmCommand) + .BindToStrict(this, x => x.ConfirmButton.Command) + .DisposeWith(dispose); + this.WhenAny(x => x.ViewModel.CancelCommand) + .BindToStrict(this, x => x.CancelButton.Command) + .DisposeWith(dispose); + }); } } diff --git a/Wabbajack.App.Wpf/Views/LinksView.xaml b/Wabbajack.App.Wpf/Views/LinksView.xaml index c2dd022b9..5d5e38843 100644 --- a/Wabbajack.App.Wpf/Views/LinksView.xaml +++ b/Wabbajack.App.Wpf/Views/LinksView.xaml @@ -6,51 +6,108 @@ xmlns:icon="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:local="clr-namespace:Wabbajack" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ic="clr-namespace:FluentIcons.WPF;assembly=FluentIcons.WPF" mc:Ignorable="d"> - - - - - + + + + diff --git a/Wabbajack.App.Wpf/Views/LinksView.xaml.cs b/Wabbajack.App.Wpf/Views/LinksView.xaml.cs index cd86e41d5..ec26e39e9 100644 --- a/Wabbajack.App.Wpf/Views/LinksView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/LinksView.xaml.cs @@ -1,34 +1,27 @@ -using System; -using System.Diagnostics; -using System.Windows; +using System.Windows; using System.Windows.Controls; -using Wabbajack.Common; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for LinksView.xaml +/// +public partial class LinksView : UserControl { - /// - /// Interaction logic for LinksView.xaml - /// - public partial class LinksView : UserControl + public LinksView() { - public LinksView() - { - InitializeComponent(); - } + InitializeComponent(); + } - private void GitHub_Click(object sender, RoutedEventArgs e) - { - UIUtils.OpenWebsite(new Uri("https://github.com/wabbajack-tools/wabbajack")); - } + private void GitHub_Click(object sender, RoutedEventArgs e) + => UIUtils.OpenWebsite(Consts.WabbajackGithubUri); - private void Discord_Click(object sender, RoutedEventArgs e) - { - UIUtils.OpenWebsite(new Uri("https://discord.gg/wabbajack")); - } + private void Discord_Click(object sender, RoutedEventArgs e) + => UIUtils.OpenWebsite(Consts.WabbajackDiscordUri); - private void Patreon_Click(object sender, RoutedEventArgs e) - { - UIUtils.OpenWebsite(new Uri("https://www.patreon.com/user?u=11907933")); - } - } + private void Patreon_Click(object sender, RoutedEventArgs e) + => UIUtils.OpenWebsite(Consts.WabbajackPatreonUri); + + private void Wiki_Click(object sender, RoutedEventArgs e) + => UIUtils.OpenWebsite(Consts.WabbajackWikiUri); } diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml b/Wabbajack.App.Wpf/Views/MainWindow.xaml index 6f508e366..4601677c1 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml @@ -7,92 +7,180 @@ xmlns:local="clr-namespace:Wabbajack" xmlns:mahapps="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:viewModels="clr-namespace:Wabbajack.View_Models" + xmlns:viewModels="clr-namespace:Wabbajack.ViewModels" xmlns:views="clr-namespace:Wabbajack.Views" + xmlns:ic="clr-namespace:FluentIcons.WPF;assembly=FluentIcons.WPF" ShowTitleBar="False" - Title="WABBAJACK" - Width="1280" - Height="960" - MinWidth="850" - MinHeight="650" + ShowCloseButton="False" + ShowMinButton="False" + ShowMaxRestoreButton="False" + Title="Wabbajack" + Width="1441" + Height="695" + MinWidth="1100" + MinHeight="500" Closing="Window_Closing" RenderOptions.BitmapScalingMode="HighQuality" ResizeMode="CanResize" Style="{StaticResource {x:Type Window}}" - TitleBarHeight="25" + TitleBarHeight="64" UseLayoutRounding="True" - WindowTitleBrush="{StaticResource MahApps.Brushes.Accent}" + WindowTitleBrush="{StaticResource BackgroundBrush}" mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Views/ModListDetailsView.xaml.cs b/Wabbajack.App.Wpf/Views/ModListDetailsView.xaml.cs new file mode 100644 index 000000000..a71db44f4 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/ModListDetailsView.xaml.cs @@ -0,0 +1,147 @@ +using System.Reactive.Disposables; +using ReactiveUI; +using ReactiveMarbles.ObservableEvents; +using System.Windows; +using System.Windows.Controls.Primitives; +using System; +using System.Windows.Input; +using System.Diagnostics; +using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; +using System.Reactive.Linq; +using System.Reactive.Concurrency; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Web.WebView2.Wpf; +using System.Windows.Controls; +using Wabbajack.RateLimiter; +using ModListStatus = Wabbajack.BaseModListMetadataVM.ModListStatus; + +namespace Wabbajack; + +public partial class ModListDetailsView +{ + public ModListDetailsView() + { + InitializeComponent(); + this.WhenActivated(disposables => + { + this.BindStrict(ViewModel, x => x.Archives, x => x.ArchivesDataGrid.ItemsSource) + .DisposeWith(disposables); + + this.BindStrict(ViewModel, x => x.Search, x => x.SearchBox.Text) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.BackCommand, x => x.BackButton) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ArchivesButton.IsChecked) + .Select(x => !x) + .BindToStrict(this, x => x.ReadmeButton.IsChecked) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ReadmeButton.IsChecked) + .Select(x => !x) + .BindToStrict(this, x => x.ArchivesButton.IsChecked) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ArchivesButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.ArchivesDataGrid.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ReadmeButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.ViewModel.Browser.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ArchivesButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.SearchBox.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ArchivesButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.SearchBoxBackground.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ReadmeButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.OpenReadmeButton.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ViewModel.MetadataVM.Metadata.Links.Readme) + .Select(readme => + { + try + { + if(readme.Contains("raw.githubusercontent.com") && readme.EndsWith(".md")) + { + var urlParts = readme.Split('/'); + var user = urlParts[3]; + var repository = urlParts[4]; + var branch = urlParts[5]; + var fileName = urlParts[6]; + return new Uri($"https://github.com/{user}/{repository}/blob/{branch}/{fileName}#{repository}"); + } + return new Uri(readme); + } + catch (Exception) + { + return default; + } + }) + .BindToStrict(this, x => x.ViewModel.Browser.Source) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ViewModel.MetadataVM.ProgressPercent) + .BindToStrict(this, x => x.InstallButton.ProgressPercentage) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ViewModel.MetadataVM.Status) + .Select(x => x == ModListStatus.NotDownloaded ? "Download & Install" : x == ModListStatus.Downloading ? "Downloading..." : "Install") + .BindToStrict(this, x => x.InstallButton.Text) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.OpenReadmeCommand, x => x.OpenReadmeButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.OpenWebsiteCommand, x => x.WebsiteButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.OpenDiscordCommand, x => x.DiscordButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, x => x.MetadataVM.InstallCommand, x => x.InstallButton) + .DisposeWith(disposables); + + RxApp.MainThreadScheduler.Schedule(() => + { + if (ViewModel.Browser.Parent != null) + { + ((Panel)ViewModel.Browser.Parent).Children.Remove(ViewModel.Browser); + } + MainContentGrid.Children.Add(ViewModel.Browser); + }); + + }); + } + + private void DataGridRow_GotFocus(object sender, RoutedEventArgs e) + { + var presenter = ((DataGridCellsPresenter)e.Source); + var archive = (Archive)presenter.Item; + if(archive.State is Nexus nexusState) + { + Process.Start(new ProcessStartInfo(nexusState.LinkUrl.ToString()) { UseShellExecute = true }); + } + RxApp.MainThreadScheduler.Schedule(0, (_, _) => + { + FocusManager.SetFocusedElement(FocusManager.GetFocusScope(presenter), null); + Keyboard.ClearFocus(); + ArchivesDataGrid.SelectedItem = null; + ArchivesDataGrid.CurrentItem = null; + return Disposable.Empty; + }); + } +} + diff --git a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml index 415af5e82..29007f5a7 100644 --- a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml +++ b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml @@ -9,31 +9,170 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:rxui="http://reactiveui.net" xmlns:system="clr-namespace:System;assembly=mscorlib" + xmlns:ic="clr-namespace:FluentIcons.WPF;assembly=FluentIcons.WPF" + xmlns:sdl="http://schemas.sdl.com/xaml" d:DesignHeight="450" d:DesignWidth="900" x:TypeArguments="local:ModListGalleryVM" mc:Ignorable="d"> - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -49,112 +188,36 @@ - - - + + + VerticalAlignment="Top" + Symbol="DismissCircle" + IsFilled="False" + FontSize="72" /> + Text="No modlists matching specified criteria" /> - - - - - + diff --git a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs index db0073a25..dc2e25bb1 100644 --- a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs @@ -1,61 +1,120 @@ -using System.Reactive.Disposables; +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Windows; +using ReactiveMarbles.ObservableEvents; using ReactiveUI; +using static System.Windows.Visibility; -namespace Wabbajack +namespace Wabbajack; + +public partial class ModListGalleryView : ReactiveUserControl { - public partial class ModListGalleryView : ReactiveUserControl + public ModListGalleryView() { - public ModListGalleryView() + InitializeComponent(); + + this.WhenActivated(dispose => { - InitializeComponent(); - - this.WhenActivated(dispose => - { - this.WhenAny(x => x.ViewModel.BackCommand) - .BindToStrict(this, x => x.BackButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.ModLists) - .BindToStrict(this, x => x.ModListGalleryControl.ItemsSource) - .DisposeWith(dispose); - - this.WhenAny(x => x.ViewModel.LoadingLock.IsLoading) - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) - .StartWith(Visibility.Collapsed) - .BindTo(this, x => x.LoadingRing.Visibility) - .DisposeWith(dispose); - - this.WhenAny(x => x.ViewModel.LoadingLock.ErrorState) - .Select(e => (e?.Succeeded ?? true) ? Visibility.Collapsed : Visibility.Visible) - .StartWith(Visibility.Collapsed) - .BindToStrict(this, x => x.ErrorIcon.Visibility) - .DisposeWith(dispose); - - this.WhenAny(x => x.ViewModel.ModLists.Count) - .CombineLatest(this.WhenAnyValue(x => x.ViewModel.LoadingLock.IsLoading)) - .Select(x => x.First == 0 && !x.Second) - .DistinctUntilChanged() - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) - .StartWith(Visibility.Collapsed) - .BindToStrict(this, x => x.NoneFound.Visibility) - .DisposeWith(dispose); - - - this.BindStrict(ViewModel, vm => vm.Search, x => x.SearchBox.Text) - .DisposeWith(dispose); - - this.BindStrict(ViewModel, vm => vm.OnlyInstalled, x => x.OnlyInstalledCheckbox.IsChecked) - .DisposeWith(dispose); - this.BindStrict(ViewModel, vm => vm.ShowNSFW, x => x.ShowNSFW.IsChecked) - .DisposeWith(dispose); - this.BindStrict(ViewModel, vm => vm.ShowUnofficialLists, x => x.ShowUnofficialLists.IsChecked) - .DisposeWith(dispose); - - this.WhenAny(x => x.ViewModel.ClearFiltersCommand) - .BindToStrict(this, x => x.ClearFiltersButton.Command) - .DisposeWith(dispose); - }); - } + this.WhenAny(x => x.ViewModel.ModLists) + .BindToStrict(this, x => x.ModListGalleryControl.ItemsSource) + .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.SmallestSizedModlist) + .Where(x => x != null) + .Select(x => x.Metadata.DownloadMetadata.TotalSize / Math.Pow(1024, 3)) + .BindToStrict(this, x => x.SizeSliderFilter.Minimum) + .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.LargestSizedModlist) + .Where(x => x != null) + .Select(x => x.Metadata.DownloadMetadata.TotalSize / Math.Pow(1024, 3)) + .BindToStrict(this, x => x.SizeSliderFilter.Maximum) + .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.LoadingLock.IsLoading) + .Select(x => x ? Visible : Collapsed) + .StartWith(Collapsed) + .BindTo(this, x => x.LoadingRing.Visibility) + .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.LoadingLock.ErrorState) + .Select(e => (e?.Succeeded ?? true) ? Collapsed : Visible) + .StartWith(Collapsed) + .BindToStrict(this, x => x.ErrorIcon.Visibility) + .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.ModLists.Count) + .CombineLatest(this.WhenAnyValue(x => x.ViewModel.LoadingLock.IsLoading)) + .Select(x => x.First == 0 && !x.Second) + .DistinctUntilChanged() + .Select(x => x ? Visible : Collapsed) + .StartWith(Collapsed) + .BindToStrict(this, x => x.NoneFound.Visibility) + .DisposeWith(dispose); + + this.BindStrict(ViewModel, vm => vm.Search, x => x.SearchBox.Text) + .DisposeWith(dispose); + this.BindStrict(ViewModel, vm => vm.OnlyInstalled, x => x.OnlyInstalledCheckbox.IsChecked) + .DisposeWith(dispose); + this.BindStrict(ViewModel, vm => vm.IncludeNSFW, x => x.IncludeNSFW.IsChecked) + .DisposeWith(dispose); + this.BindStrict(ViewModel, vm => vm.IncludeUnofficial, x => x.IncludeUnofficial.IsChecked) + .DisposeWith(dispose); + + this.BindStrict(ViewModel, + vm => vm.MinModlistSize, + view => view.SizeSliderFilter.LowerValue, + vmProp => vmProp / Math.Pow(1024, 3), + vProp => vProp * Math.Pow(1024, 3)) + .DisposeWith(dispose); + + this.BindStrict(ViewModel, + vm => vm.MaxModlistSize, + view => view.SizeSliderFilter.UpperValue, + vmProp => vmProp / Math.Pow(1024, 3), + vProp => vProp * Math.Pow(1024, 3)) + .DisposeWith(dispose); + + this.BindStrict(ViewModel, + vm => vm.HasMods, + v => v.HasModsFilter.SelectedItems) + .DisposeWith(dispose); + + this.BindStrict(ViewModel, + vm => vm.HasTags, + v => v.HasTagsFilter.SelectedItems) + .DisposeWith(dispose); + + this.OneWayBindStrict(ViewModel, + vm => vm.AllMods, + v => v.HasModsFilter.ItemsSource, + mods => new ObservableCollection(mods)) + .DisposeWith(dispose); + + this.OneWayBindStrict(ViewModel, + vm => vm.AllTags, + v => v.HasTagsFilter.ItemsSource, + tags => new ObservableCollection(tags)) + .DisposeWith(dispose); + + HasTagsFilter.Events().SelectedItemsChanged + .Subscribe(_ => + { + ViewModel.HasTags = new ObservableCollection(HasTagsFilter.SelectedItems.Cast()); + }) + .DisposeWith(dispose); + + HasModsFilter.Events().SelectedItemsChanged + .Subscribe(_ => + { + ViewModel.HasMods = new ObservableCollection(HasModsFilter.SelectedItems.Cast()); + }) + .DisposeWith(dispose); + + this.BindCommand(ViewModel, x => x.ResetFiltersCommand, x => x.ResetFiltersButton) + .DisposeWith(dispose); + }); } } diff --git a/Wabbajack.App.Wpf/Views/ModListTileView.xaml b/Wabbajack.App.Wpf/Views/ModListTileView.xaml index ccf5386e6..29ab6331b 100644 --- a/Wabbajack.App.Wpf/Views/ModListTileView.xaml +++ b/Wabbajack.App.Wpf/Views/ModListTileView.xaml @@ -10,7 +10,7 @@ xmlns:rxui="http://reactiveui.net" d:DesignHeight="450" d:DesignWidth="800" - x:TypeArguments="local:ModListMetadataVM" + x:TypeArguments="local:GalleryModListMetadataVM" mc:Ignorable="d"> #92000000 @@ -46,53 +46,79 @@ - + - - + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + - - - - - - - - + - + - + - + - - - + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/ModListTileView.xaml.cs b/Wabbajack.App.Wpf/Views/ModListTileView.xaml.cs index 1b7adb875..4a24cd1a2 100644 --- a/Wabbajack.App.Wpf/Views/ModListTileView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/ModListTileView.xaml.cs @@ -1,120 +1,41 @@ -using System; -using System.Reactive.Disposables; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Windows; -using System.Windows.Media.Media3D; -using MahApps.Metro.IconPacks; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for ModListTileView.xaml +/// +public partial class ModListTileView : ReactiveUserControl { - /// - /// Interaction logic for ModListTileView.xaml - /// - public partial class ModListTileView : ReactiveUserControl + public ModListTileView() { - public ModListTileView() + InitializeComponent(); + this.WhenActivated(disposables => { - InitializeComponent(); - this.WhenActivated(disposables => - { - ViewModel.WhenAnyValue(vm => vm.Image) - .BindToStrict(this, view => view.ModListImage.Source) - .DisposeWith(disposables); - - var textXformed = ViewModel.WhenAnyValue(vm => vm.Metadata.Title) - .CombineLatest(ViewModel.WhenAnyValue(vm => vm.Metadata.ImageContainsTitle), - ViewModel.WhenAnyValue(vm => vm.IsBroken)) - .Select(x => x.Second && !x.Third ? "" : x.First); - - textXformed - .BindToStrict(this, view => view.ModListTitle.Text) - .DisposeWith(disposables); - - textXformed - .BindToStrict(this, view => view.ModListTitleShadow.Text) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.Metadata.Description) - .BindToStrict(this, x => x.MetadataDescription.Text) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.ModListTagList) - .BindToStrict(this, x => x.TagsList.ItemsSource) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.LoadingImageLock.IsLoading) - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, x => x.LoadingProgress.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.IsBroken) - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, view => view.Overlay.Visibility) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.OpenWebsiteCommand) - .BindToStrict(this, x => x.OpenWebsiteButton.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.ModListContentsCommend) - .BindToStrict(this, x => x.ModListContentsButton.Command) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.ExecuteCommand) - .BindToStrict(this, x => x.ExecuteButton.Command) - .DisposeWith(disposables); - - - ViewModel.WhenAnyValue(x => x.ProgressPercent) - .ObserveOnDispatcher() - .Select(p => p.Value) - .BindTo(this, x => x.DownloadProgressBar.Value) - .DisposeWith(disposables); - - ViewModel.WhenAnyValue(x => x.Status) - .ObserveOnGuiThread() - .Subscribe(x => - { - IconContainer.Children.Clear(); - IconContainer.Children.Add(new PackIconMaterial - { - Width = 20, - Height = 20, - Kind = x switch - { - ModListMetadataVM.ModListStatus.Downloaded => PackIconMaterialKind.Play, - ModListMetadataVM.ModListStatus.Downloading => PackIconMaterialKind.Network, - ModListMetadataVM.ModListStatus.NotDownloaded => PackIconMaterialKind.Download, - _ => throw new ArgumentOutOfRangeException(nameof(x), x, null) - } - }); - }) - .DisposeWith(disposables); - - /* - this.MarkAsNeeded(this.ViewModel, x => x.IsBroken); - this.MarkAsNeeded(this.ViewModel, x => x.Exists); - this.MarkAsNeeded(this.ViewModel, x => x.Metadata.Links.ImageUri); - this.WhenAny(x => x.ViewModel.ProgressPercent) - .Select(p => p.Value) - .BindToStrict(this, x => x.DownloadProgressBar.Value) - .DisposeWith(dispose); - - - this.WhenAny(x => x.ViewModel.ModListContentsCommend) - .BindToStrict(this, x => x.ModListContentsButton.Command) - .DisposeWith(dispose); - - this.WhenAny(x => x.ViewModel.Image) - .BindToStrict(this, x => x.ModListImage.Source) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.LoadingImage) - .Select(x => x ? Visibility.Visible : Visibility.Collapsed) - .BindToStrict(this, x => x.LoadingProgress.Visibility) - .DisposeWith(dispose); - */ - }); - } + ViewModel.WhenAnyValue(vm => vm.Image) + .BindToStrict(this, v => v.ModlistImage.ImageSource) + .DisposeWith(disposables); + + var textXformed = ViewModel.WhenAnyValue(vm => vm.Metadata.Title) + .CombineLatest(ViewModel.WhenAnyValue(vm => vm.Metadata.ImageContainsTitle), + ViewModel.WhenAnyValue(vm => vm.IsBroken)) + .Select(x => x.Second && !x.Third ? "" : x.First); + + ViewModel.WhenAnyValue(x => x.LoadingImageLock.IsLoading) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, x => x.LoadingProgress.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(x => x.IsBroken) + .Select(x => x ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, view => view.Overlay.Visibility) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.DetailsCommand, v => v.ModlistButton) + .DisposeWith(disposables); + }); } } diff --git a/Wabbajack.App.Wpf/Views/ModeSelectionView.xaml b/Wabbajack.App.Wpf/Views/ModeSelectionView.xaml deleted file mode 100644 index 78c8a3d6d..000000000 --- a/Wabbajack.App.Wpf/Views/ModeSelectionView.xaml +++ /dev/null @@ -1,504 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wabbajack.App.Wpf/Views/ModeSelectionView.xaml.cs b/Wabbajack.App.Wpf/Views/ModeSelectionView.xaml.cs deleted file mode 100644 index 58dbd7125..000000000 --- a/Wabbajack.App.Wpf/Views/ModeSelectionView.xaml.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; -using ReactiveUI; - -namespace Wabbajack -{ - /// - /// Interaction logic for ModeSelectionView.xaml - /// - public partial class ModeSelectionView : ReactiveUserControl - { - public ModeSelectionView() - { - InitializeComponent(); - this.WhenActivated(dispose => - { - this.WhenAny(x => x.ViewModel.BrowseCommand) - .BindToStrict(this, x => x.BrowseButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.InstallCommand) - .BindToStrict(this, x => x.InstallButton.Command) - .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.CompileCommand) - .BindToStrict(this, x => x.CompileButton.Command) - .DisposeWith(dispose); - }); - } - } -} diff --git a/Wabbajack.App.Wpf/Views/NavigationView.xaml b/Wabbajack.App.Wpf/Views/NavigationView.xaml new file mode 100644 index 000000000..a0744c8c2 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/NavigationView.xaml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/NavigationView.xaml.cs b/Wabbajack.App.Wpf/Views/NavigationView.xaml.cs new file mode 100644 index 000000000..57e5c65f1 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/NavigationView.xaml.cs @@ -0,0 +1,64 @@ +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Windows; +using System.Windows.Controls; +using Wabbajack.Common; +using Wabbajack.Messages; + +namespace Wabbajack; + +/// +/// Interaction logic for NavigationView.xaml +/// +public partial class NavigationView : ReactiveUserControl +{ + public Dictionary> ButtonScreensDictionary { get; set; } + public NavigationView() + { + InitializeComponent(); + ButtonScreensDictionary = new() { + { HomeButton, [ScreenType.Home] }, + { BrowseButton, [ScreenType.ModListGallery] }, + { CompileButton, [ScreenType.CompilerHome, ScreenType.CompilerMain] }, + { SettingsButton, [ScreenType.Settings] }, + }; + this.WhenActivated(dispose => + { + this.BindCommand(ViewModel, vm => vm.BrowseCommand, v => v.BrowseButton) + .DisposeWith(dispose); + this.BindCommand(ViewModel, vm => vm.HomeCommand, v => v.HomeButton) + .DisposeWith(dispose); + this.BindCommand(ViewModel, vm => vm.CompileModListCommand, v => v.CompileButton) + .DisposeWith(dispose); + this.BindCommand(ViewModel, vm => vm.SettingsCommand, v => v.SettingsButton) + .DisposeWith(dispose); + + this.WhenAny(x => x.ViewModel.Version) + .Select(version => $"v{version}") + .BindToStrict(this, v => v.VersionTextBlock.Text) + .DisposeWith(dispose); + + + this.WhenAny(x => x.ViewModel.ActiveScreen) + .Subscribe(x => SetButtonActive(x)) + .DisposeWith(dispose); + }); + } + + private void SetButtonActive(ScreenType activeScreen) + { + var activeButtonStyle = (Style)Application.Current.Resources["ActiveNavButtonStyle"]; + var mainButtonStyle = (Style)Application.Current.Resources["MainNavButtonStyle"]; + foreach(var (button, screens) in ButtonScreensDictionary) + { + if (screens.Contains(activeScreen)) + button.Style = activeButtonStyle; + else + button.Style = mainButtonStyle; + } + } +} diff --git a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml b/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml index fe2efd4b0..ddd04b88b 100644 --- a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml @@ -6,7 +6,7 @@ xmlns:local="clr-namespace:Wabbajack" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:rxui="http://reactiveui.net" - xmlns:settings="clr-namespace:Wabbajack.View_Models.Settings" + xmlns:settings="clr-namespace:Wabbajack.ViewModels.Settings" d:DesignHeight="450" d:DesignWidth="800" x:TypeArguments="settings:AuthorFilesVM" @@ -33,7 +33,7 @@ diff --git a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs index e9a0f67a8..ecd2b749e 100644 --- a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs @@ -1,15 +1,13 @@ -using System.Windows.Controls; -using ReactiveUI; -using Wabbajack.View_Models.Settings; +using ReactiveUI; +using Wabbajack.ViewModels.Settings; -namespace Wabbajack +namespace Wabbajack; + +public partial class AuthorFilesView : ReactiveUserControl { - public partial class AuthorFilesView : ReactiveUserControl + public AuthorFilesView() { - public AuthorFilesView() - { - InitializeComponent(); - } + InitializeComponent(); } } diff --git a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml index 77ecef267..9c25dbe0f 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml @@ -35,6 +35,6 @@ Content="Logout" /> + FontSize="13" /> diff --git a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml.cs index f69071f27..9c6b575c2 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml.cs @@ -1,32 +1,29 @@ -using System; -using System.Reactive.Disposables; -using System.Windows.Forms; +using System.Reactive.Disposables; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +public partial class LoginItemView : IViewFor { - public partial class LoginItemView : IViewFor + public LoginItemView() { - public LoginItemView() + InitializeComponent(); + this.WhenActivated(disposable => { - InitializeComponent(); - this.WhenActivated(disposable => - { - ViewModel.WhenAny(x => x.Login.Icon) - .BindToStrict(this, view => view.Favicon.Source) - .DisposeWith(disposable); + ViewModel.WhenAny(x => x.Login.Icon) + .BindToStrict(this, view => view.Favicon.Source) + .DisposeWith(disposable); - ViewModel.WhenAnyValue(vm => vm.Login.SiteName) - .BindToStrict(this, view => view.SiteNameText.Text) - .DisposeWith(disposable); + ViewModel.WhenAnyValue(vm => vm.Login.SiteName) + .BindToStrict(this, view => view.SiteNameText.Text) + .DisposeWith(disposable); - this.BindCommand(ViewModel, vm => vm.Login.TriggerLogin, view => view.LoginButton) - .DisposeWith(disposable); - - this.BindCommand(ViewModel, vm => vm.Login.ClearLogin, view => view.LogoutButton) - .DisposeWith(disposable); + this.BindCommand(ViewModel, vm => vm.Login.TriggerLogin, view => view.LoginButton) + .DisposeWith(disposable); + + this.BindCommand(ViewModel, vm => vm.Login.ClearLogin, view => view.LogoutButton) + .DisposeWith(disposable); - }); - } + }); } } diff --git a/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml index 7d56b5780..7e6fb0e00 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml @@ -34,7 +34,7 @@ diff --git a/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml.cs index 932349f78..f30f301b5 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/LoginSettingsView.xaml.cs @@ -1,35 +1,20 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using System.Reactive.Disposables; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for LoginSettingsView.xaml +/// +public partial class LoginSettingsView : ReactiveUserControl { - /// - /// Interaction logic for LoginSettingsView.xaml - /// - public partial class LoginSettingsView : ReactiveUserControl + public LoginSettingsView() { - public LoginSettingsView() + InitializeComponent(); + this.WhenActivated(disposable => { - InitializeComponent(); - this.WhenActivated(disposable => - { - this.OneWayBindStrict(this.ViewModel, x => x.Logins, x => x.DownloadersList.ItemsSource) - .DisposeWith(disposable); - }); - } + this.OneWayBindStrict(this.ViewModel, x => x.Logins, x => x.DownloadersList.ItemsSource) + .DisposeWith(disposable); + }); } } diff --git a/Wabbajack.App.Wpf/Views/Settings/LoginWindowView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/LoginWindowView.xaml.cs index 03f377da5..7839adfd7 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginWindowView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/LoginWindowView.xaml.cs @@ -1,22 +1,19 @@ - +namespace Wabbajack; -namespace Wabbajack +public partial class LoginWindowView { - public partial class LoginWindowView - { - /* - public INeedsLoginCredentials Downloader { get; set; } + /* + public INeedsLoginCredentials Downloader { get; set; } - public LoginWindowView(INeedsLoginCredentials downloader) - { - Downloader = downloader; + public LoginWindowView(INeedsLoginCredentials downloader) + { + Downloader = downloader; - InitializeComponent(); + InitializeComponent(); - var loginView = new CredentialsLoginView(downloader); + var loginView = new CredentialsLoginView(downloader); - Grid.Children.Add(loginView); - } - */ + Grid.Children.Add(loginView); } + */ } diff --git a/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml index 4e5180445..848bcf221 100644 --- a/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml @@ -34,7 +34,7 @@ diff --git a/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml.cs index 52a1d5786..7347c6995 100644 --- a/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/MiscSettingsView.xaml.cs @@ -1,24 +1,23 @@ using System.Reactive.Disposables; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for MiscSettingsView.xaml +/// +public partial class MiscSettingsView : ReactiveUserControl { - /// - /// Interaction logic for MiscSettingsView.xaml - /// - public partial class MiscSettingsView : ReactiveUserControl + public MiscSettingsView() { - public MiscSettingsView() - { - InitializeComponent(); + InitializeComponent(); - this.WhenActivated(disposable => - { - // Bind Values - this.WhenAnyValue(x => x.ViewModel.OpenTerminalCommand) - .BindToStrict(this, x => x.OpenTerminal.Command) - .DisposeWith(disposable); - }); - } + this.WhenActivated(disposable => + { + // Bind Values + this.WhenAnyValue(x => x.ViewModel.OpenTerminalCommand) + .BindToStrict(this, x => x.OpenTerminal.Command) + .DisposeWith(disposable); + }); } } diff --git a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml index d976f2b94..65c55658b 100644 --- a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml @@ -40,16 +40,16 @@ @@ -69,7 +69,7 @@ Margin="20,0,0,0" Padding="10,0" HorizontalAlignment="Left"> - Reset + Reset diff --git a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs index d2a0ee5c4..6951e8d77 100644 --- a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs @@ -3,35 +3,34 @@ using ReactiveUI; using Wabbajack.Paths.IO; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for PerformanceSettingsView.xaml +/// +public partial class PerformanceSettingsView : ReactiveUserControl { - /// - /// Interaction logic for PerformanceSettingsView.xaml - /// - public partial class PerformanceSettingsView : ReactiveUserControl + public PerformanceSettingsView() { - public PerformanceSettingsView() - { - InitializeComponent(); + InitializeComponent(); - this.WhenActivated(disposable => + this.WhenActivated(disposable => + { + this.BindStrict( + ViewModel, + x => x.MaximumMemoryPerDownloadThreadMb, + x => x.MaximumMemoryPerDownloadThreadIntegerUpDown.Value) + .DisposeWith(disposable); + this.EditResourceSettings.Command = ReactiveCommand.Create(() => + { + UIUtils.OpenFile( + KnownFolders.WabbajackAppLocal.Combine("saved_settings", "resource_settings.json")); + Environment.Exit(0); + }); + ResetMaximumMemoryPerDownloadThread.Command = ReactiveCommand.Create(() => { - this.BindStrict( - ViewModel, - x => x.MaximumMemoryPerDownloadThreadMb, - x => x.MaximumMemoryPerDownloadThreadIntegerUpDown.Value) - .DisposeWith(disposable); - this.EditResourceSettings.Command = ReactiveCommand.Create(() => - { - UIUtils.OpenFile( - KnownFolders.WabbajackAppLocal.Combine("saved_settings", "resource_settings.json")); - Environment.Exit(0); - }); - ResetMaximumMemoryPerDownloadThread.Command = ReactiveCommand.Create(() => - { - ViewModel.ResetMaximumMemoryPerDownloadThreadMb(); - }); + ViewModel.ResetMaximumMemoryPerDownloadThreadMb(); }); - } + }); } } diff --git a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml index 479a98035..752511d0d 100644 --- a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml @@ -13,7 +13,7 @@ mc:Ignorable="d"> diff --git a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs index 23b3f3311..3506bf872 100644 --- a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs @@ -1,43 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using System.Reactive.Disposables; using ReactiveUI; -namespace Wabbajack +namespace Wabbajack; + +/// +/// Interaction logic for SettingsView.xaml +/// +public partial class SettingsView : ReactiveUserControl { - /// - /// Interaction logic for SettingsView.xaml - /// - public partial class SettingsView : ReactiveUserControl + public SettingsView() { - public SettingsView() + InitializeComponent(); + this.WhenActivated(disposable => { - InitializeComponent(); - this.WhenActivated(disposable => - { - this.OneWayBindStrict(this.ViewModel, x => x.BackCommand, x => x.BackButton.Command) - .DisposeWith(disposable); - this.OneWayBindStrict(this.ViewModel, x => x.Login, x => x.LoginView.ViewModel) - .DisposeWith(disposable); - this.OneWayBindStrict(this.ViewModel, x => x.Performance, x => x.PerformanceView.ViewModel) - .DisposeWith(disposable); - this.OneWayBindStrict(this.ViewModel, x => x.AuthorFile, x => x.AuthorFilesView.ViewModel) - .DisposeWith(disposable); - this.MiscGalleryView.ViewModel = this.ViewModel; - }); - } + this.OneWayBindStrict(this.ViewModel, x => x.BackCommand, x => x.BackButton.Command) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, x => x.Login, x => x.LoginView.ViewModel) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, x => x.Performance, x => x.PerformanceView.ViewModel) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, x => x.AuthorFile, x => x.AuthorFilesView.ViewModel) + .DisposeWith(disposable); + this.MiscGalleryView.ViewModel = this.ViewModel; + }); } } diff --git a/Wabbajack.App.Wpf/Views/UserControlRx.cs b/Wabbajack.App.Wpf/Views/UserControlRx.cs index db5f3029a..ca23f47a5 100644 --- a/Wabbajack.App.Wpf/Views/UserControlRx.cs +++ b/Wabbajack.App.Wpf/Views/UserControlRx.cs @@ -1,33 +1,29 @@ using ReactiveUI; -using System; using System.ComponentModel; -using System.Reactive.Disposables; using System.Windows; -using System.Windows.Controls; -namespace Wabbajack +namespace Wabbajack; + +public class UserControlRx : ReactiveUserControl, IReactiveObject + where TViewModel : class { - public class UserControlRx : ReactiveUserControl, IReactiveObject - where TViewModel : class - { - public event PropertyChangedEventHandler PropertyChanged; - public event PropertyChangingEventHandler PropertyChanging; + public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangingEventHandler PropertyChanging; - public void RaisePropertyChanging(PropertyChangingEventArgs args) - { - PropertyChanging?.Invoke(this, args); - } + public void RaisePropertyChanging(PropertyChangingEventArgs args) + { + PropertyChanging?.Invoke(this, args); + } - public void RaisePropertyChanged(PropertyChangedEventArgs args) - { - PropertyChanged?.Invoke(this, args); - } + public void RaisePropertyChanged(PropertyChangedEventArgs args) + { + PropertyChanged?.Invoke(this, args); + } - protected static void WireNotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (!(d is UserControlRx control)) return; - if (Equals(e.OldValue, e.NewValue)) return; - control.RaisePropertyChanged(e.Property.Name); - } + protected static void WireNotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (!(d is UserControlRx control)) return; + if (Equals(e.OldValue, e.NewValue)) return; + control.RaisePropertyChanged(e.Property.Name); } } diff --git a/Wabbajack.App.Wpf/Views/WebBrowserView.xaml.cs b/Wabbajack.App.Wpf/Views/WebBrowserView.xaml.cs index b43d7839f..7f047f277 100644 --- a/Wabbajack.App.Wpf/Views/WebBrowserView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/WebBrowserView.xaml.cs @@ -1,17 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Navigation; -using System.Windows.Shapes; +using System.Windows.Controls; namespace Wabbajack { diff --git a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj index 5c0aa72be..8b5cefa23 100644 --- a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj +++ b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj @@ -50,6 +50,13 @@ + + + + + + + @@ -59,8 +66,14 @@ - - + + + + Always + + + Always + TextTemplatingFileGenerator VerbRegistration.cs @@ -73,12 +86,13 @@ - + NU1701 - + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -87,8 +101,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + @@ -96,21 +112,29 @@ - - - + + + + + - + + + + + + Never + diff --git a/Wabbajack.CLI/Verbs/ValidateLists.cs b/Wabbajack.CLI/Verbs/ValidateLists.cs index 4cd72cf5a..d9dcf0f22 100644 --- a/Wabbajack.CLI/Verbs/ValidateLists.cs +++ b/Wabbajack.CLI/Verbs/ValidateLists.cs @@ -115,9 +115,7 @@ public async Task Run(AbsolutePath reports, AbsolutePath otherArchives) _logger.LogInformation("Validating {MachineUrl} - {Version}", list.NamespacedName, list.Version); } - // MachineURL - HashSet of mods per list ConcurrentDictionary> modsPerList = new(); - // HashSet of all searchable mods HashSet allMods = new(); var validatedLists = await listData.PMapAll(async modList => @@ -500,7 +498,10 @@ await w.WriteLineAsync( try { - var oldSummary = await _wjClient.GetDetailedStatus(validatedList.MachineURL); + var namespacedName = validatedList.MachineURL.Split('/'); + var machineURL = namespacedName[0]; + var repository = namespacedName[1]; + var oldSummary = await _wjClient.GetDetailedStatus(repository, machineURL); if (oldSummary.ModListHash != validatedList.ModListHash) { @@ -718,4 +719,4 @@ private async Task DownloadWabbajackFile(ModlistMetadata modList, ArchiveM await archiveManager.Ingest(tempFile.Path, token); return hash; } -} +} diff --git a/Wabbajack.Common/Ext.cs b/Wabbajack.Common/Ext.cs index 18c8a2fc5..e758c1aba 100644 --- a/Wabbajack.Common/Ext.cs +++ b/Wabbajack.Common/Ext.cs @@ -27,4 +27,5 @@ public static class Ext public static Extension Txt = new(".txt"); public static Extension Webp = new(".webp"); public static Extension Png = new(".png"); + public static Extension Jpg = new (".jpg"); } \ No newline at end of file diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj index 06e0b4723..4a2802b82 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -34,7 +34,7 @@ - + diff --git a/Wabbajack.Compiler/CompilerSettingsInferencer.cs b/Wabbajack.Compiler/CompilerSettingsInferencer.cs index b56dc000d..a6de3a6f3 100644 --- a/Wabbajack.Compiler/CompilerSettingsInferencer.cs +++ b/Wabbajack.Compiler/CompilerSettingsInferencer.cs @@ -139,8 +139,6 @@ public CompilerSettingsInferencer(ILogger logger) { cs.AdditionalProfiles = await otherProfilesFile.ReadAllLinesAsync().ToArray(); } - - cs.OutputFile = cs.Source.Parent.Combine(cs.Profile).WithExtension(Ext.Wabbajack); } return cs; diff --git a/Wabbajack.DTOs/Game/GameMetaData.cs b/Wabbajack.DTOs/Game/GameMetaData.cs index 79b9e81fa..ff5f0b765 100644 --- a/Wabbajack.DTOs/Game/GameMetaData.cs +++ b/Wabbajack.DTOs/Game/GameMetaData.cs @@ -54,4 +54,8 @@ public class GameMetaData public Game[] CanSourceFrom { get; set; } = Array.Empty(); public string HumanFriendlyGameName => Game.GetDescription(); + /// + /// URI to an ICO / PNG, preferred size 32x32 + /// + public string IconSource { get; set; } = @"Resources/Icons/wabbajack.ico"; } \ No newline at end of file diff --git a/Wabbajack.DTOs/Game/GameRegistry.cs b/Wabbajack.DTOs/Game/GameRegistry.cs index f7174dcf7..a31fc3fe1 100644 --- a/Wabbajack.DTOs/Game/GameRegistry.cs +++ b/Wabbajack.DTOs/Game/GameRegistry.cs @@ -26,7 +26,8 @@ public static class GameRegistry { "Morrowind.exe".ToRelativePath() }, - MainExecutable = "Morrowind.exe".ToRelativePath() + MainExecutable = "Morrowind.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/661c1c090ff5831a647202397c61d73c/24/32x32.png" } }, { @@ -43,7 +44,8 @@ public static class GameRegistry { "oblivion.exe".ToRelativePath() }, - MainExecutable = "Oblivion.exe".ToRelativePath() + MainExecutable = "Oblivion.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/e403262769f74b83009bffb6e3c0a3b7/32/32x32.png" } }, @@ -61,7 +63,8 @@ public static class GameRegistry { "Fallout3.exe".ToRelativePath() }, - MainExecutable = "Fallout3.exe".ToRelativePath() + MainExecutable = "Fallout3.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/ac7ed855f313b05391de74046180fb34.png" } }, { @@ -79,7 +82,8 @@ public static class GameRegistry { "FalloutNV.exe".ToRelativePath() }, - MainExecutable = "FalloutNV.exe".ToRelativePath() + MainExecutable = "FalloutNV.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/c706723a17a2b2acec4f9ebc9f572e31.png" } }, { @@ -96,7 +100,8 @@ public static class GameRegistry "tesv.exe".ToRelativePath() }, MainExecutable = "TESV.exe".ToRelativePath(), - CommonlyConfusedWith = new[] {Game.SkyrimSpecialEdition, Game.SkyrimVR} + CommonlyConfusedWith = new[] {Game.SkyrimSpecialEdition, Game.SkyrimVR}, + IconSource = "https://cdn2.steamgriddb.com/icon/58ee2794cc87707943624dc8db2ff5a0/8/32x32.png" } }, { @@ -119,7 +124,8 @@ public static class GameRegistry "SkyrimSE.exe".ToRelativePath() }, MainExecutable = "SkyrimSE.exe".ToRelativePath(), - CommonlyConfusedWith = new[] {Game.Skyrim, Game.SkyrimVR} + CommonlyConfusedWith = new[] {Game.Skyrim, Game.SkyrimVR}, + IconSource = "https://cdn2.steamgriddb.com/icon/e1b90346c92331860b1391257a106bb1/32/32x32.png" } }, { @@ -137,7 +143,8 @@ public static class GameRegistry "Fallout4.exe".ToRelativePath() }, MainExecutable = "Fallout4.exe".ToRelativePath(), - CommonlyConfusedWith = new[] {Game.Fallout4VR} + CommonlyConfusedWith = new[] {Game.Fallout4VR}, + IconSource = "https://cdn2.steamgriddb.com/icon/578d9dd532e0be0cdd050b5bec4967a1.png" } }, { @@ -155,7 +162,8 @@ public static class GameRegistry }, MainExecutable = "SkyrimVR.exe".ToRelativePath(), CommonlyConfusedWith = new[] {Game.Skyrim, Game.SkyrimSpecialEdition}, - CanSourceFrom = new[] {Game.SkyrimSpecialEdition} + CanSourceFrom = new[] {Game.SkyrimSpecialEdition}, + IconSource = "https://cdn2.steamgriddb.com/icon/75b3f26dde5a6c2a415464b05bd46fbc.png" } }, { @@ -172,7 +180,8 @@ public static class GameRegistry "TESV.exe".ToRelativePath() }, MainExecutable = "TESV.exe".ToRelativePath(), - CommonlyConfusedWith = new[] {Game.EnderalSpecialEdition} + CommonlyConfusedWith = new[] {Game.EnderalSpecialEdition}, + IconSource = "https://cdn2.steamgriddb.com/icon/6505e8a0c0e1a90d8da8879e49a437f0.png" } }, { @@ -190,7 +199,8 @@ public static class GameRegistry "SkyrimSE.exe".ToRelativePath() }, MainExecutable = "SkyrimSE.exe".ToRelativePath(), - CommonlyConfusedWith = new[] {Game.Enderal} + CommonlyConfusedWith = new[] {Game.Enderal}, + IconSource = "https://cdn2.steamgriddb.com/icon/104c6f99020b85465ae361a92d09a8d1.png" } }, { @@ -207,7 +217,8 @@ public static class GameRegistry }, MainExecutable = "Fallout4VR.exe".ToRelativePath(), CommonlyConfusedWith = new[] {Game.Fallout4}, - CanSourceFrom = new[] {Game.Fallout4} + CanSourceFrom = new[] {Game.Fallout4}, + IconSource = "https://cdn2.steamgriddb.com/icon/9058c666789874c718d1976270cee814.png" } }, { @@ -225,7 +236,8 @@ public static class GameRegistry { @"_windowsnosteam\Darkest.exe".ToRelativePath() }, - MainExecutable = @"_windowsnosteam\Darkest.exe".ToRelativePath() + MainExecutable = @"_windowsnosteam\Darkest.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/b1d2128cee734a257c5e0d5c73bbdd1b.png" } }, { @@ -242,7 +254,8 @@ public static class GameRegistry { @"Binaries\Win32\Dishonored.exe".ToRelativePath() }, - MainExecutable = @"Binaries\Win32\Dishonored.exe".ToRelativePath() + MainExecutable = @"Binaries\Win32\Dishonored.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/6fcd734d28ae00944f8f7c68a219bbc5/32/32x32.png" } }, { @@ -259,7 +272,8 @@ public static class GameRegistry { @"System\witcher.exe".ToRelativePath() }, - MainExecutable = @"System\witcher.exe".ToRelativePath() + MainExecutable = @"System\witcher.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/fd72ecaa23aa0a514a53c6a16eabb9c6.png" } }, { @@ -277,7 +291,8 @@ public static class GameRegistry { @"bin\x64\witcher3.exe".ToRelativePath() }, - MainExecutable = @"bin\x64\witcher3.exe".ToRelativePath() + MainExecutable = @"bin\x64\witcher3.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/2af9b1a840b4ecd522fe1cda88c8385e/32/32x32.png" } }, { @@ -295,7 +310,8 @@ public static class GameRegistry { "Stardew Valley.exe".ToRelativePath() }, - MainExecutable = "Stardew Valley.exe".ToRelativePath() + MainExecutable = "Stardew Valley.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/f6c4718557e1197ecdbe1b7ff52975d2.png" } }, { @@ -313,7 +329,8 @@ public static class GameRegistry { @"bin\Win64\KingdomCome.exe".ToRelativePath() }, - MainExecutable = @"bin\Win64\KingdomCome.exe".ToRelativePath() + MainExecutable = @"bin\Win64\KingdomCome.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/1bdde90ebfdef547440410e79b1877bf.png" } }, { @@ -330,7 +347,8 @@ public static class GameRegistry { @"MW5Mercs\Binaries\Win64\MechWarrior-Win64-Shipping.exe".ToRelativePath() }, - MainExecutable = @"MW5Mercs\Binaries\Win64\MechWarrior-Win64-Shipping.exe".ToRelativePath() + MainExecutable = @"MW5Mercs\Binaries\Win64\MechWarrior-Win64-Shipping.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/c59bb6bab3096620efe78bdeb031f027/8/32x32.png" } }, { @@ -346,7 +364,8 @@ public static class GameRegistry { @"Binaries\NMS.exe".ToRelativePath() }, - MainExecutable = @"Binaries\NMS.exe".ToRelativePath() + MainExecutable = @"Binaries\NMS.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/970e789e0a92eab99bcabf36dfa6050c/32/32x32.png" } }, { @@ -379,7 +398,8 @@ public static class GameRegistry { @"bin_ship\daorigins.exe".ToRelativePath() }, - MainExecutable = @"bin_ship\daorigins.exe".ToRelativePath() + MainExecutable = @"bin_ship\daorigins.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/b55d7ce2adb9449fc4dae6115cbbe30f/32/32x32.png" } }, { @@ -411,7 +431,8 @@ public static class GameRegistry { @"bin_ship\DragonAge2.exe".ToRelativePath() }, - MainExecutable = @"bin_ship\DragonAge2.exe".ToRelativePath() + MainExecutable = @"bin_ship\DragonAge2.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/a6a946f7265ed7f28a6425ee76621c3a/32/32x32.png" } }, { @@ -427,7 +448,8 @@ public static class GameRegistry { @"DragonAgeInquisition.exe".ToRelativePath() }, - MainExecutable = @"DragonAgeInquisition.exe".ToRelativePath() + MainExecutable = @"DragonAgeInquisition.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/b98004311446c60521a8831075423c20.png" } }, { @@ -444,7 +466,8 @@ public static class GameRegistry { @"KSP_x64.exe".ToRelativePath() }, - MainExecutable = @"KSP_x64.exe".ToRelativePath() + MainExecutable = @"KSP_x64.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/2ee4162f4a89db5fa43b3b08900ee370.png" } }, { @@ -458,7 +481,8 @@ public static class GameRegistry { @"tModLoader.exe".ToRelativePath() }, - MainExecutable = @"tModLoader.exe".ToRelativePath() + MainExecutable = @"tModLoader.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/e658047c67a80c47b5ba982ab520b59a.png" } }, { @@ -476,7 +500,8 @@ public static class GameRegistry { @"bin\x64\Cyberpunk2077.exe".ToRelativePath() }, - MainExecutable = @"bin\x64\Cyberpunk2077.exe".ToRelativePath() + MainExecutable = @"bin\x64\Cyberpunk2077.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/2d45da15db966ba887cf4e573989fcc8/32/32x32.png" } }, { @@ -492,7 +517,8 @@ public static class GameRegistry { @"Game\Bin\TS4_x64.exe".ToRelativePath() }, - MainExecutable = @"Game\Bin\TS4_x64.exe".ToRelativePath() + MainExecutable = @"Game\Bin\TS4_x64.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/9fc664916bce863561527f06a96f5ff3/32/32x32.png" } }, { @@ -510,7 +536,8 @@ public static class GameRegistry { @"DDDA.exe".ToRelativePath() }, - MainExecutable = @"DDDA.exe".ToRelativePath() + MainExecutable = @"DDDA.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/a830839bbb4a4022a84ff2b8af5c46e0.png" } }, { @@ -525,7 +552,8 @@ public static class GameRegistry { "nw.exe".ToRelativePath() }, - MainExecutable = "nw.exe".ToRelativePath() + MainExecutable = "nw.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/37286bc401299e97a564f6b42792eb6d.png" } }, { @@ -542,7 +570,8 @@ public static class GameRegistry { "valheim.exe".ToRelativePath() }, - MainExecutable = "valheim.exe".ToRelativePath() + MainExecutable = "valheim.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/dd055f53a45702fe05e449c30ac80df9/32/32x32.png" } }, { @@ -564,7 +593,8 @@ public static class GameRegistry { @"bin\Win64_Shipping_Client\Bannerlord.exe".ToRelativePath() }, - MainExecutable = @"bin\Win64_Shipping_Client\Bannerlord.exe".ToRelativePath() + MainExecutable = @"bin\Win64_Shipping_Client\Bannerlord.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/811cf46d61c9ae564bf7fa4b5ac639b.png" } }, { @@ -582,7 +612,7 @@ public static class GameRegistry @"End\Binaries\Win64\ff7remake_.exe".ToRelativePath(), @"ff7remake_.exe".ToRelativePath() }, - MainExecutable = @"End\Binaries\Win64\ff7remake_.exe".ToRelativePath() + MainExecutable = @"End\Binaries\Win64\ff7remake_.exe".ToRelativePath(), } }, { @@ -600,7 +630,9 @@ public static class GameRegistry { @"bin/bg3.exe".ToRelativePath() }, - MainExecutable = @"bin/bg3.exe".ToRelativePath() + MainExecutable = @"bin/bg3.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/cdb3fcd3d3fde62fe3b549a90793467e.png" + } }, { @@ -616,7 +648,8 @@ public static class GameRegistry { @"Starfield.exe".ToRelativePath() }, - MainExecutable = @"Starfield.exe".ToRelativePath() + MainExecutable = @"Starfield.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/1a495bc86abe171f690e27192ea6c367.png" } }, { diff --git a/Wabbajack.DTOs/ModList/DownloadMetadata.cs b/Wabbajack.DTOs/ModList/DownloadMetadata.cs index de692dbd2..ddbdf97e2 100644 --- a/Wabbajack.DTOs/ModList/DownloadMetadata.cs +++ b/Wabbajack.DTOs/ModList/DownloadMetadata.cs @@ -1,3 +1,4 @@ +using System; using Wabbajack.Hashing.xxHash64; namespace Wabbajack.DTOs; @@ -10,4 +11,6 @@ public class DownloadMetadata public long SizeOfArchives { get; set; } public long NumberOfInstalledFiles { get; set; } public long SizeOfInstalledFiles { get; set; } + + public long TotalSize => SizeOfArchives + SizeOfInstalledFiles; } \ No newline at end of file diff --git a/Wabbajack.DTOs/ModList/Links.cs b/Wabbajack.DTOs/ModList/Links.cs index 1b933b74c..2652b5886 100644 --- a/Wabbajack.DTOs/ModList/Links.cs +++ b/Wabbajack.DTOs/ModList/Links.cs @@ -15,4 +15,5 @@ public class LinksObject [JsonPropertyName("machineURL")] public string MachineURL { get; set; } = string.Empty; [JsonPropertyName("discordURL")] public string DiscordURL { get; set; } = string.Empty; + [JsonPropertyName("websiteURL")] public string WebsiteURL { get; set; } = string.Empty; } \ No newline at end of file diff --git a/Wabbajack.DTOs/SearchIndex.cs b/Wabbajack.DTOs/SearchIndex.cs index 1d93f3a1a..fa877416d 100644 --- a/Wabbajack.DTOs/SearchIndex.cs +++ b/Wabbajack.DTOs/SearchIndex.cs @@ -4,6 +4,7 @@ namespace Wabbajack.DTOs; public class SearchIndex { + /// /// All unique mods across all modlists /// diff --git a/Wabbajack.Hashing.xxHash64/ByteArrayExtensions.cs b/Wabbajack.Hashing.xxHash64/ByteArrayExtensions.cs index d93a8e14c..f5a650ad4 100644 --- a/Wabbajack.Hashing.xxHash64/ByteArrayExtensions.cs +++ b/Wabbajack.Hashing.xxHash64/ByteArrayExtensions.cs @@ -9,6 +9,7 @@ public static class ByteArrayExtensions { public static async ValueTask Hash(this byte[] data, IJob? job = null) { - return await new MemoryStream(data).HashingCopy(Stream.Null, CancellationToken.None, job); + using var ms = new MemoryStream(data); + return await ms.HashingCopy(Stream.Null, CancellationToken.None, job); } } \ No newline at end of file diff --git a/Wabbajack.Hashing.xxHash64/StringExtensions.cs b/Wabbajack.Hashing.xxHash64/StringExtensions.cs index a09f80d52..c3c4f42c6 100644 --- a/Wabbajack.Hashing.xxHash64/StringExtensions.cs +++ b/Wabbajack.Hashing.xxHash64/StringExtensions.cs @@ -9,7 +9,7 @@ public static class StringExtensions { public static string ToHex(this byte[] bytes) { - var builder = new StringBuilder(); + var builder = new StringBuilder(bytes.Length * 2); for (var i = 0; i < bytes.Length; i++) builder.Append(bytes[i].ToString("x2")); return builder.ToString(); } diff --git a/Wabbajack.Launcher/Views/MainWindow.axaml b/Wabbajack.Launcher/Views/MainWindow.axaml index 266d05749..1bfd268fc 100644 --- a/Wabbajack.Launcher/Views/MainWindow.axaml +++ b/Wabbajack.Launcher/Views/MainWindow.axaml @@ -7,7 +7,7 @@ Icon="/Assets/wabbajack.ico" Title="Wabbajack Launcher" Height="320" Width="600" - Background="#121212" + Background="#222531" BorderThickness="0" WindowStartupLocation="CenterScreen" ExtendClientAreaToDecorationsHint="True" diff --git a/Wabbajack.Launcher/Wabbajack.Launcher.csproj b/Wabbajack.Launcher/Wabbajack.Launcher.csproj index 10305545f..9dcf2609e 100644 --- a/Wabbajack.Launcher/Wabbajack.Launcher.csproj +++ b/Wabbajack.Launcher/Wabbajack.Launcher.csproj @@ -19,19 +19,19 @@ net9.0-windows - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - + diff --git a/Wabbajack.Networking.WabbajackClientApi/Client.cs b/Wabbajack.Networking.WabbajackClientApi/Client.cs index 1b7aff4f0..7350816ed 100644 --- a/Wabbajack.Networking.WabbajackClientApi/Client.cs +++ b/Wabbajack.Networking.WabbajackClientApi/Client.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using System.Web; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Octokit; using Wabbajack.Common; using Wabbajack.DTOs; @@ -167,10 +168,10 @@ public async Task GetListStatuses() _dtos.Options) ?? Array.Empty(); } - public async Task GetDetailedStatus(string machineURL) + public async Task GetDetailedStatus(string repository, string machineURL) { return (await _client.GetFromJsonAsync( - $"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/reports/{machineURL}/status.json", + $"https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/reports/{repository}/{machineURL}/status.json", _dtos.Options))!; } @@ -227,14 +228,14 @@ public async Task LoadLists() _dtos.Options))!.Select(meta => { meta.RepositoryName = url.Key; - meta.Official = (meta.RepositoryName == "wj-featured" || - featured.Contains(meta.NamespacedName)); + meta.Official = meta.RepositoryName == "wj-featured" || + featured.Contains(meta.NamespacedName); return meta; }); } catch (JsonException ex) { - _logger.LogError(ex, "While loading {List} from {Url}", url.Key, url.Value); + _logger.LogError(ex, "Failed loading json for repository {List} from {Url}", url.Key, url.Value); return Enumerable.Empty(); } }) @@ -259,12 +260,21 @@ public async Task> LoadRepositories() return repositories!; } + public async Task> LoadAllowedTags() + { + var data = await _client.GetFromJsonAsync(_limiter, + new HttpRequestMessage(HttpMethod.Get, + "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/refs/heads/master/allowed_tags.json"), + _dtos.Options); + return data!.ToHashSet(StringComparer.CurrentCultureIgnoreCase); + } + public async Task LoadSearchIndex() { return await _client.GetFromJsonAsync(_limiter, - new HttpRequestMessage(HttpMethod.Get, + new HttpRequestMessage(HttpMethod.Get, "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/refs/heads/master/reports/searchIndex.json"), - _dtos.Options); + _dtos.Options); } public Uri GetPatchUrl(Hash upgradeHash, Hash archiveHash) diff --git a/Wabbajack.RateLimiter/Percent.cs b/Wabbajack.RateLimiter/Percent.cs index ecb2d55f1..10d1fd728 100644 --- a/Wabbajack.RateLimiter/Percent.cs +++ b/Wabbajack.RateLimiter/Percent.cs @@ -4,7 +4,10 @@ namespace Wabbajack.RateLimiter; public readonly struct Percent : IComparable, IEquatable { + // 100% public static readonly Percent One = new(1d); + + // 0% public static readonly Percent Zero = new(0d); public readonly double Value;