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..974c2b48e 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Runtime.InteropServices; @@ -26,176 +27,195 @@ 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 webview2 = _host.Services.GetRequiredService(); + var currentDir = (AbsolutePath)Directory.GetCurrentDirectory(); + var webViewDir = currentDir.Combine("WebView2"); + if(webViewDir.DirectoryExists()) + { + var logger = _host.Services.GetRequiredService>(); + logger.LogInformation("Local WebView2 executable folder found. Using folder {0} instead of system binaries!", currentDir.Combine("WebView2")); + webview2.CreationProperties = new CoreWebView2CreationProperties() { BrowserExecutableFolder = currentDir.Combine("WebView2").ToString() }; + } - 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; - var principle = new WindowsPrincipal(identity); - return principle.IsInRole(WindowsBuiltInRole.Administrator); + try + { + var identity = WindowsIdentity.GetCurrent(); + var owner = identity.Owner; + if (owner is not null) return owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); - } - catch (Exception) - { - return false; - } - } + var principle = new WindowsPrincipal(identity); + return principle.IsInRole(WindowsBuiltInRole.Administrator); - 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.AddFilter("System.Net.Http.HttpClient", LogLevel.Warning); + 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()); + + // Singletons + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + var currentDir = (AbsolutePath)Directory.GetCurrentDirectory(); + var webViewDir = currentDir.Combine("webview2"); + services.AddSingleton(); + services.AddSingleton(); + + // ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + 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..f57f11132 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; @@ -29,14 +27,14 @@ public void Raise(IUserIntervention intervention) { var provider = _serviceProvider.GetRequiredService(); provider.Intervention = md; - MessageBus.Current.SendMessage(new SpawnBrowserWindow(provider)); + MessageBus.Current.SendMessage(new ShowBrowserWindow(provider)); break; } case ManualBlobDownload bd: { var provider = _serviceProvider.GetRequiredService(); provider.Intervention = bd; - MessageBus.Current.SendMessage(new SpawnBrowserWindow(provider)); + MessageBus.Current.SendMessage(new ShowBrowserWindow(provider)); break; } default: 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..6923e25e5 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; @@ -66,11 +61,9 @@ public LoversLabLoginManager(ILogger logger, ITokenProvid private void StartLogin() { - var view = new BrowserWindow(_serviceProvider); - view.Closed += (sender, args) => { RefreshTokenState(); }; - var provider = _serviceProvider.GetRequiredService(); - view.DataContext = provider; - view.Show(); + var handler = _serviceProvider.GetRequiredService(); + handler.Closed += (sender, args) => { RefreshTokenState(); }; + ShowBrowserWindow.Send(handler); } private void RefreshTokenState() diff --git a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs index 27ff83543..67f370b29 100644 --- a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs @@ -10,6 +10,7 @@ using ReactiveUI.Fody.Helpers; using Wabbajack.Downloaders; using Wabbajack.DTOs.Logins; +using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.UserIntervention; @@ -39,7 +40,7 @@ public NexusLoginManager(ILogger logger, ITokenProvider await RefreshTokenState()); + Task.Run(RefreshTokenState); ClearLogin = ReactiveCommand.CreateFromTask(async () => { @@ -66,11 +67,9 @@ private async Task ClearLoginToken() private void StartLogin() { - var view = new BrowserWindow(_serviceProvider); - view.Closed += async (sender, args) => { await RefreshTokenState(); }; - var provider = _serviceProvider.GetRequiredService(); - view.DataContext = provider; - view.Show(); + var handler = _serviceProvider.GetRequiredService(); + handler.Closed += async (sender, args) => { await RefreshTokenState(); }; + ShowBrowserWindow.Send(handler); } private async Task RefreshTokenState() diff --git a/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs index 62a13e260..e0c469bab 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; @@ -66,11 +62,9 @@ public VectorPlexusLoginManager(ILogger logger, IToken private void StartLogin() { - var view = new BrowserWindow(_serviceProvider); - view.Closed += (sender, args) => { RefreshTokenState(); }; - var provider = _serviceProvider.GetRequiredService(); - view.DataContext = provider; - view.Show(); + var browserView = _serviceProvider.GetRequiredService(); + browserView.ViewModel.Closed += (_, _) => RefreshTokenState(); + ShowBrowserWindow.Send(_serviceProvider.GetRequiredService()); } 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/ShowBrowserWindow.cs b/Wabbajack.App.Wpf/Messages/ShowBrowserWindow.cs new file mode 100644 index 000000000..70f54556a --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ShowBrowserWindow.cs @@ -0,0 +1,16 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; + +public class ShowBrowserWindow +{ + public BrowserWindowViewModel ViewModel { get; set; } + public ShowBrowserWindow(BrowserWindowViewModel viewModel) + { + ViewModel = viewModel; + } + public static void Send(BrowserWindowViewModel viewModel) + { + MessageBus.Current.SendMessage(new ShowBrowserWindow(viewModel)); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs b/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs new file mode 100644 index 000000000..fcefd66f8 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs @@ -0,0 +1,25 @@ +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/Messages/SpawnBrowserWindow.cs b/Wabbajack.App.Wpf/Messages/SpawnBrowserWindow.cs deleted file mode 100644 index 840d54864..000000000 --- a/Wabbajack.App.Wpf/Messages/SpawnBrowserWindow.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Wabbajack.Messages; - -public record SpawnBrowserWindow (BrowserWindowViewModel Vm) -{ -} \ 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..591c93916 100644 --- a/Wabbajack.App.Wpf/Models/ResourceMonitor.cs +++ b/Wabbajack.App.Wpf/Models/ResourceMonitor.cs @@ -14,11 +14,11 @@ 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; - private readonly Subject<(string Name, long Througput)[]> _updates = new (); + private readonly Subject<(string Name, long Throughput)[]> _updates = new (); private (string Name, long Throughput)[] _prev; public IObservable<(string Name, long Throughput)[]> Updates => _updates; @@ -27,18 +27,17 @@ public class ResourceMonitor : IDisposable public readonly ReadOnlyObservableCollection _tasksFiltered; private readonly CompositeDisposable _compositeDisposable; private readonly ILogger _logger; + private DateTime _lastMeasuredDateTime; public ReadOnlyObservableCollection Tasks => _tasksFiltered; - - - public ResourceMonitor(ILogger logger, IEnumerable resources) { _logger = logger; _compositeDisposable = new CompositeDisposable(); _resources = resources.ToArray(); + _lastMeasuredDateTime = DateTime.Now; _prev = _resources.Select(x => (x.Name, (long)0)).ToArray(); - + RxApp.MainThreadScheduler.ScheduleRecurringAction(_pollInterval, Elapsed) .DisposeWith(_compositeDisposable); @@ -51,9 +50,10 @@ public ResourceMonitor(ILogger logger, IEnumerable r private void Elapsed() { + var elapsedTime = DateTime.Now - _lastMeasuredDateTime; var current = _resources.Select(x => (x.Name, x.StatusReport.Transferred)).ToArray(); var diff = _prev.Zip(current) - .Select(t => (t.First.Name, (long)((t.Second.Transferred - t.First.Throughput) / _pollInterval.TotalSeconds))) + .Select(t => (t.First.Name, (long)((t.Second.Transferred - t.First.Throughput) / elapsedTime.TotalSeconds))) .ToArray(); _prev = current; _updates.OnNext(diff); @@ -61,18 +61,20 @@ private void Elapsed() _tasks.Edit(l => { var used = new HashSet(); + var now = DateTime.Now; foreach (var resource in _resources) { foreach (var job in resource.Jobs.Where(j => j.Current > 0)) { used.Add(job.ID); var tsk = l.Lookup(job.ID); + var jobProgress = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long)job.Size); // Update if (tsk != Optional.None) { var t = tsk.Value; t.Msg = job.Description; - t.ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long)job.Size); + t.ProgressPercent = jobProgress; t.IsWorking = job.Current > 0; } @@ -82,9 +84,9 @@ private void Elapsed() var vm = new CPUDisplayVM { ID = job.ID, - StartTime = DateTime.Now, + StartTime = now, Msg = job.Description, - ProgressPercent = job.Size == 0 ? Percent.Zero : Percent.FactoryPutInRange(job.Current, (long) job.Size), + ProgressPercent = jobProgress, IsWorking = job.Current > 0, }; l.AddOrUpdate(vm); @@ -96,6 +98,7 @@ private void Elapsed() foreach (var itm in l.Items.Where(v => !used.Contains(v.ID))) l.Remove(itm); }); + _lastMeasuredDateTime = DateTime.Now; } public void Dispose() 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 629500d6c..521878f48 100644 --- a/Wabbajack.App.Wpf/Settings.cs +++ b/Wabbajack.App.Wpf/Settings.cs @@ -4,25 +4,33 @@ 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; +public class PerformanceSettings : ViewModel +{ + private readonly Configuration.MainSettings _settings; - 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; - } + _settings = settings; } + +} +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..b1c9fa1a7 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 + #40E5E5E8 - #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 - - - + - - + + + + + + + + + + + + + + + @@ -137,6 +180,9 @@ + + + @@ -146,42 +192,56 @@ - + - - + + - - + + - + - + - - + + - - + + + - - + + + + + + + + + + + + + + - - - + + + - + + - + @@ -191,13 +251,13 @@ - - - - + + + + - - + + @@ -209,16 +269,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 +507,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 +534,7 @@ + + --> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + x:Name="Border" + Background="{TemplateBinding Background}" + BorderBrush="{TemplateBinding BorderBrush}" + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="8"> + + + + + + + + + + + + + + + + + + + + + + + - + + + + + @@ -1299,7 +1701,7 @@ - + + + + - - + + @@ -1333,33 +1744,113 @@ + + + - - + + - + + + + + + + + + + + + + - - + + @@ -1889,14 +2389,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..56613d7c5 100644 --- a/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs @@ -1,17 +1,15 @@ +using System; 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; -public class LoversLabLoginHandler : OAuth2LoginHandler +public class LoversLabLoginHandler : OAuth2LoginHandler { - public LoversLabLoginHandler(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider tokenProvider) - : base(logger, httpClient, tokenProvider) + public LoversLabLoginHandler(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider tokenProvider, IServiceProvider serviceProvider) + : base(logger, httpClient, tokenProvider, serviceProvider) { } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs b/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs index 2c99cc234..4f965b10a 100644 --- a/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/ManualBlobDownloadHandler.cs @@ -1,3 +1,4 @@ +using System; using System.Threading; using System.Threading.Tasks; using Wabbajack.DTOs.DownloadStates; @@ -9,6 +10,8 @@ public class ManualBlobDownloadHandler : BrowserWindowViewModel { public ManualBlobDownload Intervention { get; set; } + public ManualBlobDownloadHandler(IServiceProvider serviceProvider) : base(serviceProvider) { } + protected override async Task Run(CancellationToken token) { //await WaitForReady(); diff --git a/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs b/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs index 2c21a3206..346d2251a 100644 --- a/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs @@ -1,14 +1,17 @@ +using System; using System.Threading; using System.Threading.Tasks; using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.Interventions; -namespace Wabbajack.UserIntervention; +namespace Wabbajack; public class ManualDownloadHandler : BrowserWindowViewModel { public ManualDownload Intervention { get; set; } + public ManualDownloadHandler(IServiceProvider serviceProvider) : base(serviceProvider) { } + protected override async Task Run(CancellationToken token) { //await WaitForReady(); diff --git a/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs index 7bf069bf6..9e3644075 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; @@ -34,21 +26,13 @@ public class NexusLoginHandler : BrowserWindowViewModel private readonly ILogger _logger; private readonly HttpClient _client; - public NexusLoginHandler(ILogger logger, HttpClient client, EncryptedJsonTokenProvider tokenProvider) + public NexusLoginHandler(ILogger logger, HttpClient client, EncryptedJsonTokenProvider tokenProvider, IServiceProvider serviceProvider) : base(serviceProvider) { _logger = logger; _client = client; HeaderText = "Nexus Login"; _tokenProvider = tokenProvider; } - - private string Base64Id() - { - var bytes = new byte[32]; - using var rng = RandomNumberGenerator.Create(); - rng.GetBytes(bytes); - return Convert.ToBase64String(bytes); - } protected override async Task Run(CancellationToken token) { @@ -69,7 +53,7 @@ protected override async Task Run(CancellationToken token) await NavigateTo(new Uri("https://nexusmods.com")); var codeCompletionSource = new TaskCompletionSource>(); - Browser!.Browser.CoreWebView2.NewWindowRequested += (sender, args) => + Browser.CoreWebView2.NewWindowRequested += (sender, args) => { var uri = new Uri(args.Uri); _logger.LogInformation("New Window Requested {Uri}", args.Uri); diff --git a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs index a54ac5449..d601c39b9 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; @@ -27,7 +21,7 @@ public abstract class OAuth2LoginHandler : BrowserWindowViewModel private readonly ILogger _logger; public OAuth2LoginHandler(ILogger logger, HttpClient httpClient, - EncryptedJsonTokenProvider tokenProvider) + EncryptedJsonTokenProvider tokenProvider, IServiceProvider serviceProvider) : base(serviceProvider) { var tlogin = new TLoginType(); HeaderText = $"{tlogin.SiteName} Login"; @@ -43,8 +37,8 @@ protected override async Task Run(CancellationToken token) var tcs = new TaskCompletionSource(); await NavigateTo(tlogin.AuthorizationEndpoint); - Browser!.Browser.CoreWebView2.Settings.UserAgent = "Wabbajack"; - Browser!.Browser.NavigationStarting += (sender, args) => + Browser.CoreWebView2.Settings.UserAgent = "Wabbajack"; + Browser.NavigationStarting += (sender, args) => { var uri = new Uri(args.Uri); if (uri.Scheme == "wabbajack") diff --git a/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs index b41e736cf..693fd4ffc 100644 --- a/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs @@ -1,16 +1,15 @@ +using System; 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; -public class VectorPlexusLoginHandler : OAuth2LoginHandler +public class VectorPlexusLoginHandler : OAuth2LoginHandler { - public VectorPlexusLoginHandler(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider tokenProvider) - : base(logger, httpClient, tokenProvider) + public VectorPlexusLoginHandler(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider tokenProvider, IServiceProvider serviceProvider) + : base(logger, httpClient, tokenProvider, serviceProvider) { } } \ No newline at end of file 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..53160ed0d --- /dev/null +++ b/Wabbajack.App.Wpf/Util/DriveHelper.cs @@ -0,0 +1,413 @@ +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 _cachedDisks = new Dictionary(); + private static Dictionary _cachedPartitions = new Dictionary(); + private static DriveInfo[]? _cachedDrives = null; + + /// + /// All the physical disks by disk number + /// + public static Dictionary PhysicalDisks + { + get + { + if (_cachedDisks.Count == 0) + _cachedDisks = GetPhysicalDisks(); + return _cachedDisks; + } + } + + /// + /// All the physical disks by partition (drive letter) + /// + public static Dictionary Partitions + { + get + { + if (_cachedPartitions.Count == 0) + _cachedPartitions = GetPartitions(); + return _cachedPartitions; + } + } + + public static DriveInfo[] Drives + { + get + { + if (_cachedDrives == null) + _cachedDrives = DriveInfo.GetDrives(); + return _cachedDrives; + } + } + + public static void ReloadPhysicalDisks() + { + if (_cachedDisks.Count > 0) + _cachedDisks.Clear(); + _cachedDisks = 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..7de530146 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; @@ -30,6 +29,9 @@ public enum CheckOptions On } + public delegate AbsolutePath TransformPath(AbsolutePath targetPath); + public TransformPath PathTransformer { get; set; } + public object Parent { get; } [Reactive] @@ -271,7 +273,10 @@ public ICommand ConstructTypicalPickerCommand(IObservable canExecute = nul dlg.Filters.Add(filter); } if (dlg.ShowDialog() != CommonFileDialogResult.Ok) return; - TargetPath = (AbsolutePath)dlg.FileName; + + var path = (AbsolutePath)dlg.FileName; + TargetPath = PathTransformer == null ? path : PathTransformer(path); + }, canExecute: canExecute); } } 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/InstallResultHelper.cs b/Wabbajack.App.Wpf/Util/InstallResultHelper.cs new file mode 100644 index 000000000..2b2fd9fc9 --- /dev/null +++ b/Wabbajack.App.Wpf/Util/InstallResultHelper.cs @@ -0,0 +1,35 @@ +using Wabbajack.Installer; + +namespace Wabbajack; + +public static class InstallResultHelper +{ + public static string GetTitle(this InstallResult result) + { + return result switch + { + InstallResult.Succeeded => "Modlist installed", + InstallResult.Cancelled => "Cancelled", + InstallResult.Errored => "An error occurred", + InstallResult.GameMissing => "Game not found", + InstallResult.GameInvalid => "Game installation invalid", + InstallResult.DownloadFailed => "Download failed", + InstallResult.NotEnoughSpace => "Not enough space", + _ => "" + }; + } + public static string GetDescription(this InstallResult result) + { + return result switch + { + InstallResult.Succeeded => "The modlist installation completed successfully. Start up Mod Organizer in the installation directory, hit run on the top right and enjoy playing!", + InstallResult.Cancelled => "The modlist installation was cancelled.", + InstallResult.Errored => "The modlist installation has failed because of an unknown error. Check the log for more information.", + InstallResult.GameMissing => "The modlist installation has failed because the game could not be found. Please make sure a valid copy of the game is installed.", + InstallResult.GameInvalid => "The modlist installation has failed because not all required game files could be found. Verify all game files are present and retry installation.", + InstallResult.DownloadFailed => "The modlist installation has failed because one or more required files could not be downloaded. Try manually placing these files in the downloads directory.", + InstallResult.NotEnoughSpace => "The modlist installation has failed because not enough free space was available on the disk. Please free up enough space and retry the installation.", + _ => "" + }; + } +} 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..b4875e8ca 100644 --- a/Wabbajack.App.Wpf/Util/UIUtils.cs +++ b/Wabbajack.App.Wpf/Util/UIUtils.cs @@ -1,190 +1,186 @@ -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; } - - public static void OpenWebsite(Uri url) + catch (Exception) { - Process.Start(new ProcessStartInfo("cmd.exe", $"/c start {url}") - { - CreateNoWindow = true, - }); + bitmapImage = default; + return false; } + } + - public static void OpenFolder(AbsolutePath path) + public static void OpenWebsite(Uri url) + { + Process.Start(new ProcessStartInfo("cmd.exe", $"/c start {url}") { - string folderPath = path.ToString(); - if (!folderPath.EndsWith(Path.DirectorySeparatorChar.ToString())) - { - folderPath += Path.DirectorySeparatorChar.ToString(); - } + CreateNoWindow = true, + }); + } - Process.Start(new ProcessStartInfo() - { - FileName = folderPath, - UseShellExecute = true, - Verb = "open" - }); - } + public static void OpenWebsite(string url) + { + Process.Start(new ProcessStartInfo("cmd.exe", $"/c start {url}") + { + CreateNoWindow = true, + }); + } - public static AbsolutePath OpenFileDialog(string filter, string initialDirectory = null) + public static void OpenFolder(AbsolutePath path) + { + string folderPath = path.ToString(); + if (!folderPath.EndsWith(Path.DirectorySeparatorChar.ToString())) { - OpenFileDialog ofd = new OpenFileDialog(); - ofd.Filter = filter; - ofd.InitialDirectory = initialDirectory; - if (ofd.ShowDialog() == DialogResult.OK) - return (AbsolutePath)ofd.FileName; - return default; + folderPath += Path.DirectorySeparatorChar.ToString(); } - public static IObservable DownloadBitmapImage(this IObservable obs, Action exceptionHandler, - LoadingLock loadingLock) + Process.Start(new ProcessStartInfo() { - return obs - .ObserveOn(RxApp.TaskpoolScheduler) - .SelectTask(async url => + FileName = folderPath, + UseShellExecute = true, + Verb = "open" + }); + } + + public static void OpenFolderAndSelectFile(AbsolutePath pathToFile) + { + Process.Start(new ProcessStartInfo() { FileName = "explorer.exe ", Arguments = $"/select, \"{pathToFile}\"" }); + } + + 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); - } - - ret.Seek(0, SeekOrigin.Begin); - - await WriteCachedImage(url, ret.ToArray()); - return (ll, ret); - } - catch (Exception ex) + var (cached, cachedImg) = await icm.Get(url); + if (cached) return cachedImg; + + await using var stream = await client.GetStreamAsync(url); + + 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; - } + CreateNoWindow = true, + }); + } - return String.Format("{0:0.##} {1}", dblSByte, Suffix[i]); - } + 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}"; + } - public static void OpenFile(AbsolutePath file) + public static string GetHumanReadableReadmeLink(string uri) + { + if (uri.Contains("raw.githubusercontent.com") && uri.EndsWith(".md")) { - Process.Start(new ProcessStartInfo("cmd.exe", $"/c start \"\" \"{file}\"") - { - CreateNoWindow = true, - }); + var urlParts = uri.Split('/'); + var user = urlParts[3]; + var repository = urlParts[4]; + var branch = urlParts[5]; + var fileName = urlParts[6]; + return $"https://github.com/{user}/{repository}/blob/{branch}/{fileName}#{repository}"; } + return uri; } -} +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Verbs/NexusLogin.cs b/Wabbajack.App.Wpf/Verbs/NexusLogin.cs index b91148fe3..ef79c8570 100644 --- a/Wabbajack.App.Wpf/Verbs/NexusLogin.cs +++ b/Wabbajack.App.Wpf/Verbs/NexusLogin.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Wabbajack.CLI.Builder; +using Wabbajack.Messages; using Wabbajack.UserIntervention; namespace Wabbajack.Verbs; @@ -25,11 +26,9 @@ public NexusLogin(ILogger logger, IServiceProvider services) public async Task Run(CancellationToken token) { var tcs = new TaskCompletionSource(); - var view = new BrowserWindow(_services); - view.Closed += (sender, args) => { tcs.TrySetResult(0); }; - var provider = _services.GetRequiredService(); - view.DataContext = provider; - view.Show(); + var handler = _services.GetRequiredService(); + handler.Closed += (sender, args) => { tcs.TrySetResult(0); }; + ShowBrowserWindow.Send(handler); return await tcs.Task; } 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/InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs deleted file mode 100644 index 99918e1bc..000000000 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ /dev/null @@ -1,643 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.IO; -using System.Linq; -using System.Net.Http; -using ReactiveUI; -using System.Reactive.Disposables; -using System.Windows.Media.Imaging; -using ReactiveUI.Fody.Helpers; -using DynamicData; -using System.Reactive; -using System.Reactive.Linq; -using System.Text.Json; -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; -using Wabbajack.Downloaders; -using Wabbajack.Downloaders.GameFile; -using Wabbajack.DTOs; -using Wabbajack.DTOs.DownloadStates; -using Wabbajack.DTOs.JsonConverters; -using Wabbajack.Hashing.xxHash64; -using Wabbajack.Installer; -using Wabbajack.LoginManagers; -using Wabbajack.Messages; -using Wabbajack.Models; -using Wabbajack.Paths; -using Wabbajack.RateLimiter; -using Wabbajack.Paths.IO; -using Wabbajack.Services.OSIntegrated; -using Wabbajack.Util; -using System.Windows.Forms; -using Microsoft.Extensions.DependencyInjection; -using Wabbajack.CLI.Verbs; -using Wabbajack.VFS; - -namespace Wabbajack; - -public enum ModManager -{ - Standard -} - -public enum InstallState -{ - Configuration, - Installing, - Success, - Failure -} - -public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM -{ - private const string LastLoadedModlist = "last-loaded-modlist"; - private const string InstallSettingsPrefix = "install-settings-"; - private 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; } - - [Reactive] - public MO2InstallerVM Installer { get; set; } - - [Reactive] - public BitmapFrame ModListImage { get; set; } - - [Reactive] - - public BitmapFrame SlideShowImage { get; set; } - - - [Reactive] - public InstallState InstallState { get; set; } - - [Reactive] - protected ErrorResponse[] Errors { get; private set; } - - [Reactive] - public ErrorResponse Error { get; private set; } - - /// - /// Slideshow Data - /// - [Reactive] - public string SlideShowTitle { get; set; } - - [Reactive] - public string SlideShowAuthor { get; set; } - - [Reactive] - public string SlideShowDescription { get; set; } - - - private readonly DTOSerializer _dtos; - private readonly ILogger _logger; - private readonly SettingsManager _settingsManager; - private readonly IServiceProvider _serviceProvider; - private readonly SystemParametersConstructor _parametersConstructor; - private readonly IGameLocator _gameLocator; - private readonly ResourceMonitor _resourceMonitor; - private readonly Services.OSIntegrated.Configuration _configuration; - private readonly HttpClient _client; - private readonly DownloadDispatcher _downloadDispatcher; - private readonly IEnumerable _logins; - private readonly CancellationToken _cancellationToken; - public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; - - [Reactive] - public bool Installing { get; set; } - - [Reactive] - public ErrorResponse ErrorState { get; set; } - - [Reactive] - public bool ShowNSFWSlides { get; set; } - - public LogStream LoggerProvider { get; } - - private AbsolutePath LastInstallPath { get; set; } - - [Reactive] public bool OverwriteFiles { get; set; } - - - // Command properties - public ReactiveCommand ShowManifestCommand { get; } - public ReactiveCommand OpenReadmeCommand { get; } - public ReactiveCommand OpenWikiCommand { get; } - public ReactiveCommand OpenDiscordButton { get; } - public ReactiveCommand VisitModListWebsiteCommand { get; } - - public ReactiveCommand CloseWhenCompleteCommand { get; } - 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, - SystemParametersConstructor parametersConstructor, IGameLocator gameLocator, LogStream loggerProvider, ResourceMonitor resourceMonitor, - Wabbajack.Services.OSIntegrated.Configuration configuration, HttpClient client, DownloadDispatcher dispatcher, IEnumerable logins, - CancellationToken cancellationToken) : base(logger) - { - _logger = logger; - _configuration = configuration; - LoggerProvider = loggerProvider; - _settingsManager = settingsManager; - _dtos = dtos; - _serviceProvider = serviceProvider; - _parametersConstructor = parametersConstructor; - _gameLocator = gameLocator; - _resourceMonitor = resourceMonitor; - _client = client; - _downloadDispatcher = dispatcher; - _logins = logins; - _cancellationToken = cancellationToken; - - Installer = new MO2InstallerVM(this); - - BackCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView)); - - BeginCommand = ReactiveCommand.Create(() => BeginInstall().FireAndForget()); - - VerifyCommand = ReactiveCommand.Create(() => Verify().FireAndForget()); - - OpenReadmeCommand = ReactiveCommand.Create(() => - { - UIUtils.OpenWebsite(new Uri(ModList!.Readme)); - }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading, vm => vm.ModList.Readme, (isNotLoading, readme) => isNotLoading && !string.IsNullOrWhiteSpace(readme))); - - OpenWikiCommand = ReactiveCommand.Create(() => - { - UIUtils.OpenWebsite(new Uri("https://wiki.wabbajack.org/index.html")); - }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading)); - - VisitModListWebsiteCommand = ReactiveCommand.Create(() => - { - UIUtils.OpenWebsite(ModList!.Website); - }, LoadingLock.IsNotLoadingObservable); - - ModListLocation = new FilePickerVM - { - ExistCheckOption = FilePickerVM.CheckOptions.On, - PathType = FilePickerVM.PathTypeOptions.File, - PromptTitle = "Select a ModList to install" - }; - ModListLocation.Filters.Add(new CommonFileDialogFilter("Wabbajack Modlist", "*.wabbajack")); - - OpenLogsCommand = ReactiveCommand.Create(() => - { - UIUtils.OpenFolder(_configuration.LogLocation); - }); - - OpenDiscordButton = ReactiveCommand.Create(() => - { - UIUtils.OpenWebsite(new Uri(ModlistMetadata.Links.DiscordURL)); - }, this.WhenAnyValue(x => x.ModlistMetadata) - .WhereNotNull() - .Select(md => !string.IsNullOrWhiteSpace(md.Links.DiscordURL))); - - ShowManifestCommand = ReactiveCommand.Create(() => - { - UIUtils.OpenWebsite(new Uri("https://www.wabbajack.org/search/" + ModlistMetadata.NamespacedName)); - }, this.WhenAnyValue(x => x.ModlistMetadata) - .WhereNotNull() - .Select(md => !string.IsNullOrWhiteSpace(md.Links.MachineURL))); - - CloseWhenCompleteCommand = ReactiveCommand.Create(() => - { - Environment.Exit(0); - }); - - GoToInstallCommand = ReactiveCommand.Create(() => - { - UIUtils.OpenFolder(Installer.Location.TargetPath); - }); - - this.WhenAnyValue(x => x.OverwriteFiles) - .Subscribe(x => ConfirmOverwrite()); - - MessageBus.Current.Listen() - .Subscribe(msg => LoadModlistFromGallery(msg.Path, msg.Metadata).FireAndForget()) - .DisposeWith(CompositeDisposable); - - MessageBus.Current.Listen() - .Subscribe(msg => - { - LoadLastModlist().FireAndForget(); - }); - - this.WhenActivated(disposables => - { - ModListLocation.WhenAnyValue(l => l.TargetPath) - .Subscribe(p => LoadModlist(p, null).FireAndForget()) - .DisposeWith(disposables); - - var token = new CancellationTokenSource(); - BeginSlideShow(token.Token).FireAndForget(); - Disposable.Create(() => token.Cancel()) - .DisposeWith(disposables); - - this.WhenAny(vm => vm.ModListLocation.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.Installer.Location.TargetPath), - this.WhenAny(vm => vm.Installer.DownloadLocation.TargetPath)) - .Select(t => - { - var errors = new[] {t.First, t.Second, t.Third} - .Where(t => t.Failed) - .Concat(Validate()) - .ToArray(); - if (!errors.Any()) return ErrorResponse.Success; - return ErrorResponse.Fail(string.Join("\n", errors.Select(e => e.Reason))); - }) - .BindTo(this, vm => vm.ErrorState) - .DisposeWith(disposables); - }); - - } - - private IEnumerable Validate() - { - if (!ModListLocation.TargetPath.FileExists()) - yield return ErrorResponse.Fail("Mod list source does not exist"); - - var downloadPath = Installer.DownloadLocation.TargetPath; - if (downloadPath.Depth <= 1) - yield return ErrorResponse.Fail("Download path isn't set to a folder"); - - var installPath = Installer.Location.TargetPath; - if (installPath.Depth <= 1) - yield return ErrorResponse.Fail("Install path isn't set to a folder"); - if (installPath.InFolder(KnownFolders.Windows)) - yield return ErrorResponse.Fail("Don't install modlists into your Windows folder"); - if( installPath.ToString().Length > 0 && downloadPath.ToString().Length > 0 && installPath == downloadPath) - { - yield return ErrorResponse.Fail("Can't have identical install and download folders"); - } - if (installPath.ToString().Length > 0 && downloadPath.ToString().Length > 0 && KnownFolders.IsSubDirectoryOf(installPath.ToString(), downloadPath.ToString())) - { - yield return ErrorResponse.Fail("Can't put the install folder inside the download folder"); - } - foreach (var game in GameRegistry.Games) - { - if (!_gameLocator.TryFindLocation(game.Key, out var location)) - continue; - - if (installPath.InFolder(location)) - yield return ErrorResponse.Fail("Can't install a modlist into a game folder"); - - if (location.ThisAndAllParents().Any(path => installPath == path)) - { - yield return ErrorResponse.Fail( - "Can't install in this path, installed files may overwrite important game files"); - } - } - - if (installPath.InFolder(KnownFolders.EntryPoint)) - yield return ErrorResponse.Fail("Can't install a modlist into the Wabbajack.exe path"); - if (downloadPath.InFolder(KnownFolders.EntryPoint)) - yield return ErrorResponse.Fail("Can't download a modlist into the Wabbajack.exe path"); - if (KnownFolders.EntryPoint.ThisAndAllParents().Any(path => installPath == path)) - { - yield return ErrorResponse.Fail("Installing in this folder may overwrite Wabbajack"); - } - - if (installPath.ToString().Length != 0 && installPath != LastInstallPath && !OverwriteFiles && - 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 + - "if you are updating an existing modlist, then this is expected and can be overwritten."); - } - - if (KnownFolders.IsInSpecialFolder(installPath) || KnownFolders.IsInSpecialFolder(downloadPath)) - { - yield return ErrorResponse.Fail("Can't install into Windows locations such as Documents etc, please make a new folder for the modlist - C:\\ModList\\ for example."); - } - // Disabled Because it was causing issues for people trying to update lists. - //if (installPath.ToString().Length > 0 && downloadPath.ToString().Length > 0 && !HasEnoughSpace(installPath, downloadPath)){ - // yield return ErrorResponse.Fail("Can't install modlist due to lack of free hard drive space, please read the modlist Readme to learn more."); - //} - } - - /* - private bool HasEnoughSpace(AbsolutePath inpath, AbsolutePath downpath) - { - string driveLetterInPath = inpath.ToString().Substring(0,1); - string driveLetterDownPath = inpath.ToString().Substring(0,1); - DriveInfo driveUsedInPath = new DriveInfo(driveLetterInPath); - DriveInfo driveUsedDownPath = new DriveInfo(driveLetterDownPath); - long spaceRequiredforInstall = ModlistMetadata.DownloadMetadata.SizeOfInstalledFiles; - long spaceRequiredforDownload = ModlistMetadata.DownloadMetadata.SizeOfArchives; - long spaceInstRemaining = driveUsedInPath.AvailableFreeSpace; - long spaceDownRemaining = driveUsedDownPath.AvailableFreeSpace; - if ( driveLetterInPath == driveLetterDownPath) - { - long totalSpaceRequired = spaceRequiredforInstall + spaceRequiredforDownload; - if (spaceInstRemaining < totalSpaceRequired) - { - return false; - } - - } else - { - if( spaceInstRemaining < spaceRequiredforInstall || spaceDownRemaining < spaceRequiredforDownload) - { - return false; - } - } - return true; - - }*/ - - private async Task BeginSlideShow(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - await Task.Delay(5000, token); - if (InstallState == InstallState.Installing) - { - await PopulateNextModSlide(ModList); - } - } - } - - private async Task LoadLastModlist() - { - var lst = await _settingsManager.Load(LastLoadedModlist); - if (lst.FileExists()) - { - ModListLocation.TargetPath = lst; - } - } - - private async Task LoadModlistFromGallery(AbsolutePath path, ModlistMetadata metadata) - { - ModListLocation.TargetPath = path; - ModlistMetadata = metadata; - } - - private async Task LoadModlist(AbsolutePath path, ModlistMetadata? metadata) - { - using var ll = LoadingLock.WithLoading(); - InstallState = InstallState.Configuration; - ModListLocation.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}"; - TaskBarUpdate.Send($"Loaded {ModList.Name}", TaskbarItemProgressState.Normal); - - var hex = (await ModListLocation.TargetPath.ToString().Hash()).ToHex(); - var prevSettings = await _settingsManager.Load(InstallSettingsPrefix + hex); - - if (path.WithExtension(Ext.MetaData).FileExists()) - { - try - { - metadata = JsonSerializer.Deserialize(await path.WithExtension(Ext.MetaData) - .ReadAllTextAsync()); - ModlistMetadata = metadata; - } - catch (Exception ex) - { - _logger.LogInformation(ex, "Can't load metadata cached next to file"); - } - } - - if (prevSettings.ModListLocation == path) - { - ModListLocation.TargetPath = prevSettings.ModListLocation; - LastInstallPath = prevSettings.InstallLocation; - Installer.Location.TargetPath = prevSettings.InstallLocation; - Installer.DownloadLocation.TargetPath = prevSettings.DownloadLoadction; - ModlistMetadata = metadata ?? prevSettings.Metadata; - } - - PopulateSlideShow(ModList); - - ll.Succeed(); - await _settingsManager.Save(LastLoadedModlist, path); - } - catch (Exception ex) - { - _logger.LogError(ex, "While loading modlist"); - ll.Fail(); - } - } - - private void ConfirmOverwrite() - { - AbsolutePath prev = Installer.Location.TargetPath; - Installer.Location.TargetPath = "".ToAbsolutePath(); - Installer.Location.TargetPath = prev; - } - - private async Task Verify() - { - await Task.Run(async () => - { - InstallState = InstallState.Installing; - - StatusText = $"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); - - 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; - } - else - { - TaskBarUpdate.Send($"Finished verification of {ModList.Name}", TaskbarItemProgressState.Normal); - InstallState = InstallState.Success; - } - - }); - } - - private async Task BeginInstall() - { - await Task.Run(async () => - { - InstallState = InstallState.Installing; - - foreach (var downloader in await _downloadDispatcher.AllDownloaders(ModList.Archives.Select(a => a.State))) - { - _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 postfix = (await ModListLocation.TargetPath.ToString().Hash()).ToHex(); - await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings - { - ModListLocation = ModListLocation.TargetPath, - InstallLocation = Installer.Location.TargetPath, - DownloadLoadction = Installer.DownloadLocation.TargetPath, - Metadata = ModlistMetadata - }); - await _settingsManager.Save(LastLoadedModlist, ModListLocation.TargetPath); - - try - { - var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration - { - Game = ModList.GameType, - Downloads = Installer.DownloadLocation.TargetPath, - Install = Installer.Location.TargetPath, - ModList = ModList, - ModlistArchive = ModListLocation.TargetPath, - SystemParameters = _parametersConstructor.Create(), - GameFolder = _gameLocator.GameLocation(ModList.GameType) - }); - - - installer.OnStatusUpdate = update => - { - StatusText = update.StatusText; - StatusProgress = update.StepsProgress; - - TaskBarUpdate.Send(update.StatusText, TaskbarItemProgressState.Indeterminate, - update.StepsProgress.Value); - }; - - if (!await installer.Begin(_cancellationToken)) - { - TaskBarUpdate.Send($"Error during install of {ModList.Name}", TaskbarItemProgressState.Error); - InstallState = InstallState.Failure; - StatusText = $"Error during install of {ModList.Name}"; - StatusProgress = Percent.Zero; - } - else - { - TaskBarUpdate.Send($"Finished install of {ModList.Name}", TaskbarItemProgressState.Normal); - InstallState = InstallState.Success; - - if (!string.IsNullOrWhiteSpace(ModList.Readme)) - UIUtils.OpenWebsite(new Uri(ModList.Readme)); - - } - } - catch (Exception ex) - { - 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; - } - }); - - } - - - class SavedInstallSettings - { - public AbsolutePath ModListLocation { get; set; } - public AbsolutePath InstallLocation { get; set; } - public AbsolutePath DownloadLoadction { get; set; } - - public ModlistMetadata Metadata { get; set; } - } - - private void PopulateSlideShow(ModList modList) - { - if (ModlistMetadata.ImageContainsTitle && ModlistMetadata.DisplayVersionOnlyInInstallerView) - { - SlideShowTitle = "v" + ModlistMetadata.Version.ToString(); - } - else - { - SlideShowTitle = modList.Name; - } - SlideShowAuthor = modList.Author; - SlideShowDescription = modList.Description; - SlideShowImage = ModListImage; - } - - - private async Task PopulateNextModSlide(ModList modList) - { - try - { - var mods = modList.Archives.Select(a => a.State) - .OfType() - .Where(t => ShowNSFWSlides || !t.IsNSFW) - .Where(t => t.ImageURL != null) - .ToArray(); - var thisMod = mods[_random.Next(0, mods.Length)]; - var data = await _client.GetByteArrayAsync(thisMod.ImageURL!); - var image = BitmapFrame.Create(new MemoryStream(data)); - SlideShowTitle = thisMod.Name; - SlideShowAuthor = thisMod.Author; - SlideShowDescription = thisMod.Description; - SlideShowImage = image; - } - catch (Exception ex) - { - _logger.LogTrace(ex, "While loading slide"); - } - } - -} 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..7edb2f249 --- /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 CloseCommand { get; } + + Subject IsBackEnabledSubject { get; } + IObservable IsBackEnabled { get; } +} + +public class BackNavigatingVM : ViewModel, IBackNavigatingVM +{ + [Reactive] + public ViewModel NavigateBackTarget { get; set; } + public ReactiveCommand CloseCommand { 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); + CloseCommand = 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 69% rename from Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs rename to Wabbajack.App.Wpf/ViewModels/BrowserWindowViewModel.cs index 5daee154b..76a8f5840 100644 --- a/Wabbajack.App.Wpf/View Models/BrowserWindowViewModel.cs +++ b/Wabbajack.App.Wpf/ViewModels/BrowserWindowViewModel.cs @@ -1,10 +1,13 @@ using System; -using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; +using System.Windows.Threading; using HtmlAgilityPack; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; using ReactiveUI; @@ -14,21 +17,46 @@ using Wabbajack.Hashing.xxHash64; using Wabbajack.Messages; using Wabbajack.Paths; -using Wabbajack.Views; namespace Wabbajack; public abstract class BrowserWindowViewModel : ViewModel { + private IServiceProvider _serviceProvider { get; set; } + [Reactive] public WebView2 Browser { get; set; } [Reactive] public string HeaderText { get; set; } - [Reactive] public string Instructions { get; set; } - [Reactive] public string Address { get; set; } + [Reactive] public ICommand CloseCommand { get; set; } + [Reactive] public ICommand BackCommand { get; set; } + public event EventHandler Closed; + + public BrowserWindowViewModel(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + BackCommand = ReactiveCommand.Create(() => Browser.GoBack()); + CloseCommand = ReactiveCommand.Create(() => Close()); + this.WhenActivated(disposable => + { + Browser = _serviceProvider.GetRequiredService(); - public BrowserWindow? Browser { get; set; } + RunWrapper(CancellationToken.None).ContinueWith((_) => Close()); + Disposable.Empty.DisposeWith(disposable); + }); + } - private Microsoft.Web.WebView2.Wpf.WebView2 _browser => Browser!.Browser; + private void Close() + { + ShowFloatingWindow.Send(FloatingScreenType.None); + if(Closed != null) + { + foreach(var delegateMethod in Closed.GetInvocationList()) + { + Closed -= delegateMethod as EventHandler; + } + } + //Activator.Deactivate(); + } public async Task RunWrapper(CancellationToken token) { @@ -40,7 +68,7 @@ public async Task RunWrapper(CancellationToken token) protected async Task WaitForReady() { - while (Browser?.Browser.CoreWebView2 == null) + while (Browser.CoreWebView2 == null) { await Task.Delay(250); } @@ -70,15 +98,15 @@ void Completed(object? o, CoreWebView2NavigationCompletedEventArgs a) } } - _browser.NavigationCompleted += Completed; - _browser.Source = uri; + Browser.NavigationCompleted += Completed; + Browser.Source = uri; await tcs.Task; - _browser.NavigationCompleted -= Completed; + Browser.NavigationCompleted -= Completed; } public async Task RunJavaScript(string script) { - await _browser.ExecuteScriptAsync(script); + await Browser.ExecuteScriptAsync(script); } public async Task GetCookies(string domainEnding, CancellationToken token) @@ -88,7 +116,7 @@ public async Task GetCookies(string domainEnding, CancellationToken to { domainEnding = domainEnding[4..]; } - var cookies = (await _browser.CoreWebView2.CookieManager.GetCookiesAsync("")) + var cookies = (await Browser.CoreWebView2.CookieManager.GetCookiesAsync("")) .Where(c => c.Domain.EndsWith(domainEnding)); return cookies.Select(c => new Cookie { @@ -101,7 +129,7 @@ public async Task GetCookies(string domainEnding, CancellationToken to public async Task EvaluateJavaScript(string js) { - return await _browser.ExecuteScriptAsync(js); + return await Browser.ExecuteScriptAsync(js); } public async Task GetDom(CancellationToken token) @@ -116,8 +144,8 @@ public async Task GetDom(CancellationToken token) public async Task WaitForDownloadUri(CancellationToken token, Func? whileWaiting) { var source = new TaskCompletionSource(); - var referer = _browser.Source; - while (_browser.CoreWebView2 == null) + var referer = Browser.Source; + while (Browser.CoreWebView2 == null) await Task.Delay(10, token); EventHandler handler = null!; @@ -127,19 +155,19 @@ public async Task GetDom(CancellationToken token) try { source.SetResult(new Uri(args.DownloadOperation.Uri)); - _browser.CoreWebView2.DownloadStarting -= handler; + Browser.CoreWebView2.DownloadStarting -= handler; } catch (Exception) { source.SetCanceled(token); - _browser.CoreWebView2.DownloadStarting -= handler; + Browser.CoreWebView2.DownloadStarting -= handler; } args.Cancel = true; args.Handled = true; }; - _browser.CoreWebView2.DownloadStarting += handler; + Browser.CoreWebView2.DownloadStarting += handler; Uri uri; @@ -165,17 +193,17 @@ public async Task GetDom(CancellationToken token) { ("Referer", referer?.ToString() ?? uri.ToString()) }, - _browser.CoreWebView2.Settings.UserAgent); + Browser.CoreWebView2.Settings.UserAgent); } public async Task WaitForDownload(AbsolutePath path, CancellationToken token) { var source = new TaskCompletionSource(); - var referer = _browser.Source; - while (_browser.CoreWebView2 == null) + var referer = Browser.Source; + while (Browser.CoreWebView2 == null) await Task.Delay(10, token); - _browser.CoreWebView2.DownloadStarting += (sender, args) => + Browser.CoreWebView2.DownloadStarting += (sender, args) => { try { 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..a0bd1ced9 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerDetailsVM.cs @@ -0,0 +1,280 @@ +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", + PathTransformer = (folder) => folder.DirectoryExists() ? folder.Combine(!string.IsNullOrWhiteSpace(Settings?.ModListName) ? Settings.ModListName : "Default").WithExtension(Ext.Wabbajack) : folder + }; + + 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..bbe664166 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs @@ -0,0 +1,123 @@ +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 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..e0b0fd961 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerMainVM.cs @@ -0,0 +1,305 @@ +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; +using System.Reactive.Concurrency; + +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, DownloadDispatcher downloadDispatcher) : base(dtos, settingsManager, logger, wjClient) + { + _serviceProvider = serviceProvider; + _resourceMonitor = resourceMonitor; + _logins = logins; + _downloadDispatcher = downloadDispatcher; + + LoggerProvider = loggerProvider; + CompilerDetailsVM = compilerDetailsVM; + CompilerFileManagerVM = compilerFileManagerVM; + + CancellationTokenSource = new CancellationTokenSource(); + + InfoCommand = ReactiveCommand.Create(Info); + StartCommand = ReactiveCommand.Create(StartCompilation, + this.WhenAnyValue(vm => vm.Settings.ModListName, + vm => vm.Settings.ModListAuthor, + vm => vm.Settings.ModListDescription, + vm => vm.Settings.ModListImage, + vm => vm.Settings.OutputFile, + vm => vm.Settings.Version, (name, author, desc, img, output, version) => + !string.IsNullOrWhiteSpace(name) && + !string.IsNullOrWhiteSpace(author) && + !string.IsNullOrWhiteSpace(desc) && + img.FileExists() && + !string.IsNullOrWhiteSpace(output.ToString()) && + Version.TryParse(version, out _))); + + 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() => UIUtils.OpenFolderAndSelectFile(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; + + var compiler = MO2Compiler.Create(_serviceProvider, Settings.ToCompilerSettings()); + + var events = Observable.FromEventPattern(h => compiler.OnStatusUpdate += h, + h => compiler.OnStatusUpdate -= h) + .ObserveOnGuiThread() + .Subscribe(update => + { + RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => + { + var s = update.EventArgs; + ProgressText = $"{s.StatusText}"; + ProgressPercent = s.StepsProgress; + return Disposable.Empty; + }); + }); + + + 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])) + { + _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); + } + } + } + + 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 86% rename from Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml rename to Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml index 54c7ffd2f..17c1490e2 100644 --- a/Wabbajack.App.Wpf/View Models/Controls/RemovableItemView.xaml +++ b/Wabbajack.App.Wpf/ViewModels/Controls/RemovableItemView.xaml @@ -1,13 +1,13 @@ - @@ -17,7 +17,7 @@ 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..e472d2594 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/BaseModListMetadataVM.cs @@ -0,0 +1,197 @@ +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 DetailsCommand { get; 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)); + + DetailsCommand = ReactiveCommand.Create(() => { + LoadModlistForDetails.Send(this); + ShowFloatingWindow.Send(FloatingScreenType.ModListDetails); + }); + } + + 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..7ce84ef0f --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/GalleryModListMetadataVM.cs @@ -0,0 +1,56 @@ +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 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)); + + 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..18d2dec70 --- /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; + CloseCommand = 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/ViewModels/Installers/InstallationVM.cs b/Wabbajack.App.Wpf/ViewModels/Installers/InstallationVM.cs new file mode 100644 index 000000000..df4282b64 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Installers/InstallationVM.cs @@ -0,0 +1,817 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Net.Http; +using ReactiveUI; +using System.Reactive.Disposables; +using System.Windows.Media.Imaging; +using ReactiveUI.Fody.Helpers; +using DynamicData; +using System.Reactive; +using System.Reactive.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Shell; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAPICodePack.Dialogs; +using Wabbajack.Common; +using Wabbajack.Downloaders; +using Wabbajack.Downloaders.GameFile; +using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; +using Wabbajack.DTOs.JsonConverters; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Installer; +using Wabbajack.LoginManagers; +using Wabbajack.Messages; +using Wabbajack.Models; +using Wabbajack.Paths; +using Wabbajack.RateLimiter; +using Wabbajack.Paths.IO; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Util; +using Wabbajack.CLI.Verbs; +using Microsoft.Extensions.DependencyInjection; +using Wabbajack.VFS; +using Humanizer; +using System.Text.RegularExpressions; +using System.Windows.Input; +using Microsoft.Web.WebView2.Wpf; +using System.Diagnostics; +using System.Reactive.Concurrency; + +namespace Wabbajack; + +public enum InstallState +{ + Configuration, + Installing, + Success, + Failure +} + +public class InstallationVM : ProgressViewModel, ICpuStatusVM +{ + private const string LastLoadedModlist = "last-loaded-modlist"; + private const string InstallSettingsPrefix = "install-settings-"; + private readonly Random _random = new(); + + + [Reactive] public ModList ModList { get; set; } + [Reactive] public ModlistMetadata ModlistMetadata { get; set; } + [Reactive] public FilePickerVM WabbajackFileLocation { get; set; } + [Reactive] public MO2InstallerVM Installer { get; set; } + [Reactive] public StandardInstaller StandardInstaller { get; set; } + [Reactive] public BitmapImage ModListImage { get; set; } + [Reactive] public InstallState InstallState { get; set; } + + /// + /// Don't use the Reactive attribute on nullable enum values + /// This causes InvalidProgramExceptions on requesting this service via DependencyInjection + /// + private InstallResult? _installResult = null; + public InstallResult? InstallResult + { + get => _installResult; + set + { + RaiseAndSetIfChanged(ref _installResult, value); + _installResult = value; + } + } + + /// + /// Slideshow Data + /// + [Reactive] public BitmapFrame SlideShowImage { get; set; } + [Reactive] public string SlideShowTitle { get; set; } + [Reactive] public string SlideShowAuthor { get; set; } + [Reactive] public string SlideShowDescription { get; set; } + [Reactive] public string SuggestedInstallFolder { get; set; } + [Reactive] public string SuggestedDownloadFolder { get; set; } + + public WebView2 ReadmeBrowser { get; set; } + + private readonly DTOSerializer _dtos; + private readonly ILogger _logger; + private readonly SettingsManager _settingsManager; + private readonly IServiceProvider _serviceProvider; + private readonly SystemParametersConstructor _parametersConstructor; + private readonly IGameLocator _gameLocator; + private readonly ResourceMonitor _resourceMonitor; + private readonly Services.OSIntegrated.Configuration _configuration; + private readonly HttpClient _client; + private readonly DownloadDispatcher _downloadDispatcher; + private readonly IEnumerable _logins; + private readonly CancellationTokenSource _cancellationTokenSource; + public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; + + [Reactive] public bool Installing { get; set; } + + [Reactive] public ErrorResponse ErrorState { get; set; } + + [Reactive] public bool ShowNSFWSlides { get; set; } + + public LogStream LoggerProvider { get; } + + private AbsolutePath LastInstallPath { get; set; } + + [Reactive] public bool OverwriteFiles { get; set; } + + [Reactive] public string HashingSpeed { get; set; } + [Reactive] public string ExtractingSpeed { get; set; } + [Reactive] public string DownloadingSpeed { get; set; } + + + // Command properties + public ICommand OpenManifestCommand { get; } + public ICommand OpenReadmeCommand { get; } + public ICommand OpenWikiCommand { get; } + public ICommand OpenDiscordButton { get; } + public ICommand OpenWebsiteCommand { get; } + public ICommand OpenMissingArchivesCommand { get; } + public ICommand BackToGalleryCommand { get; } + public ICommand OpenLogFolderCommand { get; } + public ICommand OpenInstallFolderCommand { get; } + public ICommand InstallCommand { get; } + public ICommand CancelCommand { get; } + public ICommand EditInstallDetailsCommand { get; } + public ICommand VerifyCommand { get; } + public ICommand PlayCommand { get; } + + 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, + CancellationTokenSource cancellationTokenSource) + { + _logger = logger; + _configuration = configuration; + LoggerProvider = loggerProvider; + _settingsManager = settingsManager; + _dtos = dtos; + _serviceProvider = serviceProvider; + _parametersConstructor = parametersConstructor; + _gameLocator = gameLocator; + _resourceMonitor = resourceMonitor; + _client = client; + _downloadDispatcher = dispatcher; + _logins = logins; + _cancellationTokenSource = cancellationTokenSource; + + ConfigurationText = $"Loading... Please wait"; + ProgressText = $"Installation"; + + Installer = new MO2InstallerVM(this); + ReadmeBrowser = serviceProvider.GetRequiredService(); + + CancelCommand = ReactiveCommand.Create(CancelInstall, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading)); + EditInstallDetailsCommand = ReactiveCommand.Create(() => + { + ConfigurationText = "Preparation"; + ProgressText = $"Installation"; + CurrentStep = Step.Configuration; + InstallState = InstallState.Configuration; + ProgressState = ProgressState.Normal; + this.Activator.Activate(); + }); + InstallCommand = ReactiveCommand.Create(() => BeginInstall().FireAndForget(), this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading)); + + OpenReadmeCommand = ReactiveCommand.Create(() => + { + UIUtils.OpenWebsite(ModList!.Readme); + }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading, vm => vm.ModList.Readme, (isNotLoading, readme) => isNotLoading && !string.IsNullOrWhiteSpace(readme))); + + OpenWebsiteCommand = ReactiveCommand.Create(() => + { + UIUtils.OpenWebsite(ModlistMetadata.Links.WebsiteURL); + }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading, vm => vm.ModlistMetadata, + (isNotLoading, metadata) => isNotLoading && !string.IsNullOrWhiteSpace(metadata?.Links.WebsiteURL))); + + WabbajackFileLocation = new FilePickerVM + { + ExistCheckOption = FilePickerVM.CheckOptions.On, + PathType = FilePickerVM.PathTypeOptions.File, + PromptTitle = "Select a modlist to install" + }; + WabbajackFileLocation.Filters.Add(new CommonFileDialogFilter("Wabbajack modlist", "*.wabbajack")); + + OpenLogFolderCommand = ReactiveCommand.Create(() => + { + UIUtils.OpenFolderAndSelectFile(_configuration.LogLocation.Combine("Wabbajack.current.log")); + }); + + OpenDiscordButton = ReactiveCommand.Create(() => + { + UIUtils.OpenWebsite(new Uri(ModlistMetadata.Links.DiscordURL)); + }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading, vm => vm.ModlistMetadata, + (isNotLoading, metadata) => isNotLoading && !string.IsNullOrWhiteSpace(metadata?.Links?.DiscordURL))); + + OpenManifestCommand = ReactiveCommand.Create(() => + { + // TODO: Open modlist archives in modal dialog + UIUtils.OpenWebsite(new Uri("https://www.wabbajack.org/search/" + ModlistMetadata.NamespacedName)); + }, this.WhenAnyValue(x => x.LoadingLock.IsNotLoading)); + + OpenInstallFolderCommand = ReactiveCommand.Create(() => + { + UIUtils.OpenFolderAndSelectFile(Installer.Location.TargetPath.Combine("ModOrganizer.exe")); + }); + + OpenMissingArchivesCommand = ReactiveCommand.Create(() => + { + var missing = ModList.Archives.Where(a => !StandardInstaller.HashedArchives.ContainsKey(a.Hash)).ToArray(); + ShowMissingManualReport(missing); + }); + + BackToGalleryCommand = ReactiveCommand.Create(() => NavigateToGlobal.Send(ScreenType.ModListGallery)); + + PlayCommand = ReactiveCommand.Create(() => + { + Process.Start(new ProcessStartInfo(Installer.Location.TargetPath.Combine("ModOrganizer.exe").ToString()) { UseShellExecute = true }); + }, this.WhenAnyValue(vm => vm.LoadingLock.IsNotLoading, vm => vm.InstallState, + (isNotLoading, installState) => isNotLoading && installState == InstallState.Success)); + + this.WhenAnyValue(x => x.OverwriteFiles) + .Subscribe(x => ConfirmOverwrite()); + + MessageBus.Current.Listen() + .Subscribe(msg => LoadModlistFromGallery(msg.Path, msg.Metadata).FireAndForget()) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .Subscribe(msg => + { + LoadLastModlist().FireAndForget(); + }); + + this.WhenActivated(disposables => + { + + WabbajackFileLocation.WhenAnyValue(l => l.TargetPath) + .Subscribe(p => LoadModlist(p, null).FireAndForget()) + .DisposeWith(disposables); + + _resourceMonitor.Updates + .Subscribe(updates => + { + foreach (var update in updates) + { + switch (update.Name) + { + case "Downloads": + DownloadingSpeed = $"{update.Throughput.ToFileSizeString()}/s"; + break; + case "File Hashing": + HashingSpeed = $"{update.Throughput.ToFileSizeString()}/s"; + break; + case "File Extractor": + ExtractingSpeed = $"{update.Throughput.ToFileSizeString()}/s"; + break; + } + } + }) + .DisposeWith(disposables); + + var token = new CancellationTokenSource(); + BeginSlideShow(token.Token).FireAndForget(); + Disposable.Create(() => token.Cancel()) + .DisposeWith(disposables); + + 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.WabbajackFileLocation.TargetPath), + this.WhenAny(vm => vm.Installer.Location.TargetPath), + this.WhenAny(vm => vm.Installer.DownloadLocation.TargetPath)) + .Select(t => + { + var errors = (new[] { t.First, t.Second, t.Third}) + .Where(t => t.Failed) + .Concat(Validate()) + .ToArray(); + if (!errors.Any()) return ErrorResponse.Success; + return ErrorResponse.Fail(string.Join("\n", errors.Select(e => e.Reason))); + }) + .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); + + this.WhenAnyValue(vm => vm.Installer.Location.TargetPath) + .Select(x => x.PathParts.Any() ? x.Combine("downloads") : x) + .Subscribe(x => Installer.DownloadLocation.TargetPath = x) + .DisposeWith(disposables); + }); + + } + + private static string GetSuggestedInstallFolder(ModlistMetadata 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()}\\"; + } + + private async void CancelInstall() + { + switch(InstallState) + { + case InstallState.Configuration: + NavigateToGlobal.Send(ScreenType.ModListGallery); + break; + + case InstallState.Installing: + // TODO - Cancel installation + await _cancellationTokenSource.CancelAsync(); + _cancellationTokenSource.TryReset(); + break; + + default: + break; + } + } + + private IEnumerable Validate() + { + if (!WabbajackFileLocation.TargetPath.FileExists()) + yield return ErrorResponse.Fail("Mod list source does not exist"); + + var downloadPath = Installer.DownloadLocation.TargetPath; + if (downloadPath.Depth <= 1) + yield return ErrorResponse.Fail("Download path isn't set to a folder"); + + var installPath = Installer.Location.TargetPath; + if (installPath.Depth <= 1) + yield return ErrorResponse.Fail("Install path isn't set to a folder"); + if (installPath.InFolder(KnownFolders.Windows)) + yield return ErrorResponse.Fail("Don't install modlists into your Windows folder"); + if( installPath.ToString().Length > 0 && downloadPath.ToString().Length > 0 && installPath == downloadPath) + { + yield return ErrorResponse.Fail("Can't have identical install and download folders"); + } + if (installPath.ToString().Length > 0 && downloadPath.ToString().Length > 0 && KnownFolders.IsSubDirectoryOf(installPath.ToString(), downloadPath.ToString())) + { + yield return ErrorResponse.Fail("Can't put the install folder inside the download folder"); + } + foreach (var game in GameRegistry.Games) + { + if (!_gameLocator.TryFindLocation(game.Key, out var location)) + continue; + + if (installPath.InFolder(location)) + yield return ErrorResponse.Fail("Can't install a modlist into a game folder"); + + if (location.ThisAndAllParents().Any(path => installPath == path)) + { + yield return ErrorResponse.Fail( + "Can't install in this path, installed files may overwrite important game files"); + } + } + + if (installPath.InFolder(KnownFolders.EntryPoint)) + yield return ErrorResponse.Fail("Can't install a modlist into the Wabbajack.exe path"); + if (downloadPath.InFolder(KnownFolders.EntryPoint)) + yield return ErrorResponse.Fail("Can't download a modlist into the Wabbajack.exe path"); + if (KnownFolders.EntryPoint.ThisAndAllParents().Any(path => installPath == path)) + { + yield return ErrorResponse.Fail("Installing in this folder may overwrite Wabbajack"); + } + + 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 + + "if you are updating an existing modlist, then this is expected and can be overwritten."); + } + + if (KnownFolders.IsInSpecialFolder(installPath) || KnownFolders.IsInSpecialFolder(downloadPath)) + { + yield return ErrorResponse.Fail("Can't install into Windows locations such as Documents etc, please make a new folder for the modlist - C:\\ModList\\ for example."); + } + // Disabled Because it was causing issues for people trying to update lists. + //if (installPath.ToString().Length > 0 && downloadPath.ToString().Length > 0 && !HasEnoughSpace(installPath, downloadPath)){ + // yield return InstallResponse.Fail("Can't install modlist due to lack of free hard drive space, please read the modlist Readme to learn more."); + //} + } + + /* + private bool HasEnoughSpace(AbsolutePath inpath, AbsolutePath downpath) + { + string driveLetterInPath = inpath.ToString().Substring(0,1); + string driveLetterDownPath = inpath.ToString().Substring(0,1); + DriveInfo driveUsedInPath = new DriveInfo(driveLetterInPath); + DriveInfo driveUsedDownPath = new DriveInfo(driveLetterDownPath); + long spaceRequiredforInstall = ModlistMetadata.DownloadMetadata.SizeOfInstalledFiles; + long spaceRequiredforDownload = ModlistMetadata.DownloadMetadata.SizeOfArchives; + long spaceInstRemaining = driveUsedInPath.AvailableFreeSpace; + long spaceDownRemaining = driveUsedDownPath.AvailableFreeSpace; + if ( driveLetterInPath == driveLetterDownPath) + { + long totalSpaceRequired = spaceRequiredforInstall + spaceRequiredforDownload; + if (spaceInstRemaining < totalSpaceRequired) + { + return false; + } + + } else + { + if( spaceInstRemaining < spaceRequiredforInstall || spaceDownRemaining < spaceRequiredforDownload) + { + return false; + } + } + return true; + + }*/ + + private async Task BeginSlideShow(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + await Task.Delay(5000, token); + if (InstallState == InstallState.Installing) + { + await PopulateNextModSlide(ModList); + } + } + } + + private async Task LoadLastModlist() + { + var lst = await _settingsManager.Load(LastLoadedModlist); + if (lst.FileExists()) + { + WabbajackFileLocation.TargetPath = lst; + } + } + + private async Task LoadModlistFromGallery(AbsolutePath path, ModlistMetadata metadata) + { + WabbajackFileLocation.TargetPath = path; + ModlistMetadata = metadata; + } + + private async Task LoadModlist(AbsolutePath path, ModlistMetadata? metadata) + { + using var ll = LoadingLock.WithLoading(); + InstallState = InstallState.Configuration; + WabbajackFileLocation.TargetPath = path; + try + { + ModList = await StandardInstaller.LoadFromFile(_dtos, path); + var stream = await StandardInstaller.ModListImageStream(path); + if(stream != null) ModListImage = UIUtils.BitmapImageFromStream(stream); + + ConfigurationText = $"Preparing to install {ModlistMetadata.Title}"; + ProgressText = $"Installation"; + + var hex = (await WabbajackFileLocation.TargetPath.ToString().Hash()).ToHex(); + var prevSettings = await _settingsManager.Load(InstallSettingsPrefix + hex); + + if (path.WithExtension(Ext.MetaData).FileExists()) + { + try + { + metadata = JsonSerializer.Deserialize(await path.WithExtension(Ext.MetaData) + .ReadAllTextAsync()); + ModlistMetadata = metadata; + SuggestedInstallFolder = GetSuggestedInstallFolder(metadata); + SuggestedDownloadFolder = SuggestedInstallFolder + "\\downloads"; + } + catch (Exception ex) + { + _logger.LogInformation(ex, "Can't load metadata cached next to file"); + } + } + + if (prevSettings.ModListLocation == path) + { + WabbajackFileLocation.TargetPath = prevSettings.ModListLocation; + LastInstallPath = prevSettings.InstallLocation; + Installer.Location.TargetPath = prevSettings.InstallLocation; + Installer.DownloadLocation.TargetPath = prevSettings.DownloadLocation; + ModlistMetadata = metadata ?? prevSettings.Metadata; + } + + PopulateSlideShow(ModList); + + ll.Succeed(); + await _settingsManager.Save(LastLoadedModlist, path); + } + catch (Exception ex) + { + _logger.LogError(ex, "While loading modlist"); + ll.Fail(); + ProgressText = "Failed to load modlist"; + } + } + + private void ConfirmOverwrite() + { + AbsolutePath prev = Installer.Location.TargetPath; + Installer.Location.TargetPath = "".ToAbsolutePath(); + Installer.Location.TargetPath = prev; + } + + private async Task Verify() + { + await Task.Run(async () => + { + InstallState = InstallState.Installing; + + ProgressText = $"Verifying {ModList.Name}"; + + + var cmd = new VerifyModlistInstall(_serviceProvider.GetRequiredService>(), _dtos, + _serviceProvider.GetRequiredService>(), + _serviceProvider.GetRequiredService()); + + var result = await cmd.Run(WabbajackFileLocation.TargetPath, Installer.Location.TargetPath, _cancellationTokenSource.Token); + + if (result != 0) + { + TaskBarUpdate.Send($"Error during verification of {ModList.Name}", TaskbarItemProgressState.Error); + InstallState = InstallState.Failure; + ProgressText = $"Error during install of {ModList.Name}"; + ProgressPercent = Percent.Zero; + } + else + { + TaskBarUpdate.Send($"Finished verification of {ModList.Name}", TaskbarItemProgressState.Normal); + InstallState = InstallState.Success; + } + }); + } + + private async Task BeginInstall() + { + await Task.Run(async () => + { + RxApp.MainThreadScheduler.Schedule(() => + { + ConfigurationText = "Preparation"; + ProgressText = $"Installing {ModList.Name}"; + CurrentStep = Step.Busy; + InstallState = InstallState.Installing; + ProgressState = ProgressState.Normal; + }); + + await PrepareDownloaders(); + + var postfix = (await WabbajackFileLocation.TargetPath.ToString().Hash()).ToHex(); + await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings + { + ModListLocation = WabbajackFileLocation.TargetPath, + InstallLocation = Installer.Location.TargetPath, + DownloadLocation = Installer.DownloadLocation.TargetPath, + Metadata = ModlistMetadata + }); + await _settingsManager.Save(LastLoadedModlist, WabbajackFileLocation.TargetPath); + + try + { + StandardInstaller = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration + { + Game = ModList.GameType, + Downloads = Installer.DownloadLocation.TargetPath, + Install = Installer.Location.TargetPath, + ModList = ModList, + ModlistArchive = WabbajackFileLocation.TargetPath, + SystemParameters = _parametersConstructor.Create(), + GameFolder = _gameLocator.GameLocation(ModList.GameType) + }); + + + StandardInstaller.OnStatusUpdate = update => + { + RxApp.MainThreadScheduler.Schedule(() => + { + ProgressText = update.StatusText; + ProgressPercent = update.StepsProgress; + }); + }; + + var result = await StandardInstaller.Begin(_cancellationTokenSource.Token); + if (result == Wabbajack.Installer.InstallResult.Succeeded) + { + RxApp.MainThreadScheduler.Schedule(() => + { + InstallResult = result; + ProgressText = $"Finished installing {ModList.Name}"; + InstallState = InstallState.Success; + }); + } + else + { + RxApp.MainThreadScheduler.Schedule(() => + { + InstallResult = result; + InstallState = InstallState.Failure; + ProgressText = $"Error during installation of {ModList.Name}"; + ProgressPercent = Percent.Zero; + ProgressState = ProgressState.Error; + }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + RxApp.MainThreadScheduler.Schedule(() => + { + InstallState = InstallState.Failure; + ProgressText = $"Error during installation of {ModList.Name}"; + ProgressPercent = Percent.Zero; + ProgressState = ProgressState.Error; + InstallResult = Wabbajack.Installer.InstallResult.Errored; + }); + } + }); + + } + + private async Task PrepareDownloaders() + { + foreach (var downloader in await _downloadDispatcher.AllDownloaders(ModList.Archives.Select(a => a.State))) + { + _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); + } + } + } + + private void ShowMissingManualReport(Archive[] toArray) + { + _logger.LogInformation("Writing Manual helper report"); + var report = Installer.DownloadLocation.TargetPath.Combine("MissingManuals.html"); + { + using var writer = new StreamWriter(report.Open(FileMode.Create, FileAccess.Write, FileShare.None)); + writer.Write("Missing Files"); + writer.Write("

Missing Files

"); + writer.Write( + "

Wabbajack was unable to download the following files automatically. Please download them manually and place them in the downloads folder you chose during the install configuration.

"); + foreach (var archive in toArray) + { + switch (archive.State) + { + case Manual manual: + writer.Write($"

{archive.Name}

"); + writer.Write($"

{manual.Prompt}

"); + writer.Write($"

Download URL: {manual.Url}

"); + break; + case MediaFire mediaFire: + writer.Write($"

{archive.Name}

"); + writer.Write($"

Download URL: {mediaFire.Url}

"); + break; + case GameFileSource gameFile: + writer.Write($"

{archive.Name}

"); + if(archive.Name.Contains("CreationKit")) + { + writer.Write($"

This modlist requires the Creation Kit to function.

"); + if (ModList.GameType == Game.SkyrimSpecialEdition || ModList.GameType == Game.SkyrimVR) + { + writer.Write(@$"

Click here to install it via Steam.

"); + } + else if(ModList.GameType == Game.Fallout4 || ModList.GameType == Game.Fallout4VR) + { + writer.Write(@$"

Click here to install it via Steam.

"); + } + else if(ModList.GameType == Game.Starfield) + { + writer.Write(@$"

Click here to install it via Steam.

"); + } + } + else if(ModList.GameType == Game.SkyrimSpecialEdition && archive.Name.Contains("curios", StringComparison.OrdinalIgnoreCase)) + { + writer.Write("

This is a game file that commonly causes issues.

"); + writer.Write(@"

Click here for more information on how to resolve the issue.

"); + } + else if(ModList.GameType == Game.SkyrimSpecialEdition && archive.Name.StartsWith("Data_cc", StringComparison.OrdinalIgnoreCase)) + { + writer.Write("

This is a Creation Club file that could not be found. Check if the Anniversary Edition DLC is installed before installing this modlist.

"); + } + else + { + writer.Write("

This is a game file that could not be found. Validate the game is installed properly in the same language as that of the modlist author.

"); + } + break; + + default: + writer.Write($"

{archive.Name}

"); + writer.Write($"

Unknown download type

"); + writer.Write($"

Primary Key (may not be helpful): {archive.State.PrimaryKeyString}

"); + break; + } + } + + writer.Write(""); + } + + Process.Start(new ProcessStartInfo("cmd.exe", $"start /c \"{report}\"") + { + CreateNoWindow = true, + }); + } + + class SavedInstallSettings + { + public AbsolutePath ModListLocation { get; set; } + public AbsolutePath InstallLocation { get; set; } + public AbsolutePath DownloadLocation { get; set; } + + public ModlistMetadata Metadata { get; set; } + } + + private void PopulateSlideShow(ModList modList) + { + return; + + if (ModlistMetadata.ImageContainsTitle && ModlistMetadata.DisplayVersionOnlyInInstallerView) + { + SlideShowTitle = "v" + ModlistMetadata.Version.ToString(); + } + else + { + SlideShowTitle = modList.Name; + } + SlideShowAuthor = modList.Author; + SlideShowDescription = modList.Description; + //SlideShowImage = ModListImage; + } + + + private async Task PopulateNextModSlide(ModList modList) + { + try + { + var mods = modList.Archives.Select(a => a.State) + .OfType() + .Where(t => ShowNSFWSlides || !t.IsNSFW) + .Where(t => t.ImageURL != null) + .ToArray(); + var thisMod = mods[_random.Next(0, mods.Length)]; + var data = await _client.GetByteArrayAsync(thisMod.ImageURL!); + var image = BitmapFrame.Create(new MemoryStream(data)); + SlideShowTitle = thisMod.Name; + SlideShowAuthor = thisMod.Author; + SlideShowDescription = thisMod.Description; + SlideShowImage = image; + } + catch (Exception ex) + { + _logger.LogTrace(ex, "While loading slide"); + } + } + +} 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..bf9614cb4 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs @@ -0,0 +1,351 @@ +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; +using System.Reactive.Concurrency; + +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); + + this.WhenAnyValue(x => x.ActiveFloatingPane) + .Buffer(2, 1) + .Select(b => (Previous: b[0], Current: b[1])) + .Subscribe(x => + { + x.Previous?.Activator.Deactivate(); + x.Current?.Activator.Activate(); + }); + + 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(HandleShowBrowserWindow) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .ObserveOnGuiThread() + .Subscribe((_) => NavigationVisible = true) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .ObserveOnGuiThread() + .Subscribe((_) => NavigationVisible = false) + .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .ObserveOnGuiThread() + .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(ToggleMaximized); + CloseCommand = ReactiveCommand.Create(Close); + } + + private void ShowInfo() + { + if (ActivePane is IHasInfoVM) ((IHasInfoVM)ActivePane).InfoCommand.Execute(null); + } + + private void Minimize() + { + Application.Current.MainWindow.WindowState = WindowState.Minimized; + } + + private void ToggleMaximized() + { + var currentWindowState = Application.Current.MainWindow.WindowState; + var desiredWindowState = currentWindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + + /* + var mainWindow = _serviceProvider.GetRequiredService(); + mainWindow.WindowState = desiredWindowState; + */ + Application.Current.MainWindow.WindowState = desiredWindowState; + } + + 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 HandleShowBrowserWindow(ShowBrowserWindow msg) + { + var browserWindow = _serviceProvider.GetRequiredService(); + ActiveFloatingPane = browserWindow.ViewModel = msg.ViewModel; + browserWindow.DataContext = ActiveFloatingPane; + RxApp.MainThreadScheduler.Schedule(() => browserWindow.ViewModel.Activator.Activate()); + ((BrowserWindowViewModel)ActiveFloatingPane).Closed += (_, _) => ActiveFloatingPane.Activator.Deactivate(); + } + + 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..6bc4a9572 --- /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()); + + CloseCommand = 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..44e8d0dc7 --- /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()); + CloseCommand = 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..4a0fd59e3 --- /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.CloseCommand = 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..9aca71481 --- /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 CloseCommand { 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"; + + CloseCommand = 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 b/Wabbajack.App.Wpf/Views/BrowserView.xaml index 5d57984e0..fd4fa9203 100644 --- a/Wabbajack.App.Wpf/Views/BrowserView.xaml +++ b/Wabbajack.App.Wpf/Views/BrowserView.xaml @@ -22,10 +22,10 @@ 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..534420cc9 100644 --- a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml +++ b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml @@ -1,50 +1,43 @@ - - + + - - - - + + + + - - - + + + + - - - - - - - + + + + + + + - + diff --git a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs index d9fb66456..7c2eabadb 100644 --- a/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs +++ b/Wabbajack.App.Wpf/Views/BrowserWindow.xaml.cs @@ -1,84 +1,56 @@ 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; -using MahApps.Metro.Controls; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Web.WebView2.Wpf; +using System.Windows; using ReactiveUI; -using Wabbajack.Common; namespace Wabbajack; -public partial class BrowserWindow : MetroWindow +public partial class BrowserWindow : ReactiveUserControl { - private readonly CompositeDisposable _disposable; - private readonly IServiceProvider _serviceProvider; - public WebView2 Browser { get; set; } - - public BrowserWindow(IServiceProvider serviceProvider) + public BrowserWindow() { InitializeComponent(); - _disposable = new CompositeDisposable(); - _serviceProvider = serviceProvider; - Browser = _serviceProvider.GetRequiredService(); - RxApp.MainThreadScheduler.Schedule(() => + this.WhenActivated(disposables => { - if(Browser.Parent != null) - { - ((Panel)Browser.Parent).Children.Remove(Browser); - } - MainGrid.Children.Add(Browser); - Grid.SetRow(Browser, 3); - Grid.SetColumnSpan(Browser, 3); - }); - } + this.BindCommand(ViewModel, vm => vm.BackCommand, v => v.BackButton) + .DisposeWith(disposables); - private void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e) - { - if (e.LeftButton == MouseButtonState.Pressed) - { - base.DragMove(); - } - } + this.BindCommand(ViewModel, vm => vm.CloseCommand, v => v.CloseButton) + .DisposeWith(disposables); - private void BrowserWindow_OnActivated(object sender, EventArgs e) - { - var vm = ((BrowserWindowViewModel) DataContext); - vm.Browser = this; + this.WhenAnyValue(v => v.ViewModel.HeaderText) + .BindToStrict(this, view => view.Header.Text) + .DisposeWith(disposables); - vm.WhenAnyValue(vm => vm.HeaderText) - .BindToStrict(this, view => view.Header.Text) - .DisposeWith(_disposable); - - vm.WhenAnyValue(vm => vm.Instructions) - .BindToStrict(this, view => view.Instructions.Text) - .DisposeWith(_disposable); - - vm.WhenAnyValue(vm => vm.Address) - .BindToStrict(this, view => view.AddressBar.Text) - .DisposeWith(_disposable); - - this.CopyButton.Command = ReactiveCommand.Create(() => - { - Clipboard.SetText(vm.Address.ToString()); - }); - - this.BackButton.Command = ReactiveCommand.Create(() => - { - Browser.GoBack(); + this.WhenAnyValue(v => v.ViewModel.Instructions) + .BindToStrict(this, view => view.Instructions.Text) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.Address) + .BindToStrict(this, view => view.AddressBar.Text) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ViewModel.Browser) + .WhereNotNull() + .ObserveOnGuiThread() + .Subscribe(browser => + { + RxApp.MainThreadScheduler.Schedule(() => + { + if (browser.Parent != null) + { + ((Panel)browser.Parent).Children.Remove(browser); + } + ViewModel.Browser.Visibility = Visibility.Visible; + ViewModel.Browser.Width = double.NaN; + ViewModel.Browser.Height = double.NaN; + WebViewGrid.Children.Add(browser); + }); + }) + .DisposeWith(disposables); }); - - vm.RunWrapper(CancellationToken.None) - .ContinueWith(_ => Dispatcher.Invoke(() => - { - Close(); - })); } } \ No newline at end of file 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..c1ec2ff51 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..6652851f0 100644 --- a/Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/DetailImageView.xaml.cs @@ -1,132 +1,125 @@ 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 Version? Version + { + get => (Version?)GetValue(VersionProperty); + set => SetValue(VersionProperty, value); + } + public static readonly DependencyProperty VersionProperty = DependencyProperty.Register(nameof(Version), typeof(Version), typeof(DetailImageView), new FrameworkPropertyMetadata(null, 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() + public DetailImageView() + { + InitializeComponent(); + + this.WhenActivated(dispose => { - InitializeComponent(); + // 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 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); - 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); + /* + var versionVisible = this.WhenAny(x => x.Version) + .Select(x => x?.ToString() ?? string.Empty) + .Select(x => string.IsNullOrWhiteSpace(x) ? Visibility.Hidden : Visibility.Visible) + .Replay(1) + .RefCount(); + versionVisible + .BindToStrict(this, x => x.VersionTextRun.Visibility) + .DisposeWith(dispose); + */ + this.WhenAny(x => x.Version) + .Select(x => x != null ? x.ToString() : string.Empty) + .BindToStrict(this, x => x.VersionTextRun.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); + this.WhenAny(x => x.Version) + .Subscribe(x => VersionPrefixRun.Text = x != null ? "version" : string.Empty) + .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..327aab10b 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..8b6471753 100644 --- a/Wabbajack.App.Wpf/Views/Common/LogView.xaml +++ b/Wabbajack.App.Wpf/Views/Common/LogView.xaml @@ -8,19 +8,44 @@ d:DesignHeight="450" d:DesignWidth="800" mc:Ignorable="d"> - - - + + + + + + + + + + - - - - - - - + BorderThickness="0" + ItemsSource="{Binding Source={StaticResource FilteredRows}}" + 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..03d053fc7 100644 --- a/Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Common/LogView.xaml.cs @@ -1,24 +1,21 @@ -using System.Windows; -using System.Windows.Controls; +using System.Windows.Controls; +using static Wabbajack.Models.LogStream; -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 LogView() { - 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))); + InitializeComponent(); + } - public LogView() - { - InitializeComponent(); - } + private void CollectionViewSource_Filter(object sender, System.Windows.Data.FilterEventArgs e) + { + var row = e.Item as ILogMessage; + e.Accepted = row.Level.Ordinal >= 2; } } 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..bd595542d --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml @@ -0,0 +1,23 @@ + 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..7edab0f87 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml.cs @@ -0,0 +1,210 @@ +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, + Transparent, + SemiTransparent +} +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))); + } + } + public Symbol Icon + { + get => (Symbol)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(Symbol), typeof(WJButton), new FrameworkPropertyMetadata(default(Symbol), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + + public double IconSize + { + get => (double)GetValue(IconSizeProperty); + set => SetValue(IconSizeProperty, value); + } + public static readonly DependencyProperty IconSizeProperty = DependencyProperty.Register(nameof(IconSize), typeof(double), typeof(WJButton), new FrameworkPropertyMetadata(24D, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + public FlowDirection Direction + { + get => (FlowDirection)GetValue(DirectionProperty); + set => SetValue(DirectionProperty, value); + } + public static readonly DependencyProperty DirectionProperty = DependencyProperty.Register(nameof(Direction), typeof(FlowDirection), typeof(WJButton), new FrameworkPropertyMetadata(default(FlowDirection), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + public ButtonStyle ButtonStyle + { + get => (ButtonStyle)GetValue(ButtonStyleProperty); + set => SetValue(ButtonStyleProperty, value); + } + public static readonly DependencyProperty ButtonStyleProperty = DependencyProperty.Register(nameof(ButtonStyle), typeof(ButtonStyle), typeof(WJButton), new FrameworkPropertyMetadata(default(ButtonStyle), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, WireNotifyPropertyChanged)); + + 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"], + ButtonStyle.Transparent => (Style)Application.Current.Resources["TransparentBackgroundButtonStyle"], + ButtonStyle.SemiTransparent => (Style)Application.Current.Resources["WJSemiTransparentButtonStyle"], + _ => (Style)Application.Current.Resources["WJButtonStyle"], + }) + .DisposeWith(dispose); + + this.WhenAnyValue(x => x.ProgressPercentage) + .Subscribe(percent => + { + if (ButtonStyle != ButtonStyle.Progress) return; + if (percent == Percent.One) + { + Style = (Style)Application.Current.Resources["WJColorButtonStyle"]; + } + 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); + } + private static void WireNotifyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (Equals(e.OldValue, e.NewValue)) return; + ((WJButton)d).RaisePropertyChanged(e.Property.Name); + } +} diff --git a/Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml b/Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml similarity index 93% rename from Wabbajack.App.Wpf/Views/Compilers/CompilationCompleteView.xaml rename to Wabbajack.App.Wpf/Views/Compiler/CompilationCompleteView.xaml index 0a1df3fdc..6114aeb4d 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"> @@ -44,11 +44,11 @@ Width="55" Height="55" Style="{StaticResource CircleButtonStyle}"> - + Kind="ArrowLeft" />--> - + Kind="FolderMove" />--> - + Kind="Check" />--> +/// 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..e2c531631 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerDetailsView.xaml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..403b32480 --- /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..e3b063dbc --- /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..cc06a1ec7 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompilerMainView.xaml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..91027c75e --- /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..d6c45d69f --- /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..af867c817 --- /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.CloseCommand, x => x.PrevButton) + .DisposeWith(dispose); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml b/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml deleted file mode 100644 index f76f9d150..000000000 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs deleted file mode 100644 index b4000b86d..000000000 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationCompleteView.xaml.cs +++ /dev/null @@ -1,61 +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 InstallationCompleteView.xaml - /// - public partial class InstallationCompleteView : ReactiveUserControl - { - public InstallationCompleteView() - { - 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); - }); - } - } -} diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml index a7a2e2b5a..76ace5f0d 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" /> @@ -88,6 +88,7 @@ x:Name="BeginButton" HorizontalAlignment="Center" VerticalAlignment="Center" /> + +/// 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.InstallCommand) + .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..c57ff66c7 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml @@ -11,12 +11,15 @@ 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}" + xmlns:ic="clr-namespace:FluentIcons.Wpf;assembly=FluentIcons.Wpf" + xmlns:math="http://hexinnovation.com/math" xmlns:controls="http://schemas.sdl.com/xaml" + 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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The modlist installation has failed because your installation or downloads directory has run out of space. + Please make sure enough space is available on the disk and try again. + + + + + + + + + + + + + + + + + + + + + + Readme + Log Viewer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + - + + + diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs index 871ba5c03..33d789c7a 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs @@ -1,108 +1,372 @@ using System.Reactive.Disposables; using System.Reactive.Linq; -using System.Windows.Controls; using ReactiveUI; using System.Windows; +using System; +using System.Linq; +using Wabbajack.Paths; +using Wabbajack.Messages; +using ReactiveMarbles.ObservableEvents; +using System.Windows.Controls; +using System.Reactive.Concurrency; +using System.Windows.Media; +using Symbol = FluentIcons.Common.Symbol; +using Wabbajack.Installer; + +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; + this.Bind(ViewModel, vm => vm.Installer.Location, view => view.InstallationLocationPicker.PickerVM) + .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); + this.Bind(ViewModel, vm => vm.Installer.DownloadLocation, view => view.DownloadLocationPicker.PickerVM) + .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); + this.BindCommand(ViewModel, vm => vm.OpenReadmeCommand, v => v.DocumentationButton) + .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.BackCommand) - .BindToStrict(this, view => view.BackButton.Command) - .DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.OpenWebsiteCommand, v => v.WebsiteButton) + .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.InstallState) - .Select(v => v == InstallState.Installing ? Visibility.Collapsed : Visibility.Visible) - .BindToStrict(this, view => view.BackButton.Visibility) - .DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.OpenDiscordButton, v => v.DiscordButton) + .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); + this.BindCommand(ViewModel, vm => vm.OpenManifestCommand, v => v.ManifestButton) + .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); + this.BindCommand(ViewModel, vm => vm.CancelCommand, v => v.CancelButton) + .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); + this.BindCommand(ViewModel, vm => vm.EditInstallDetailsCommand, v => v.EditInstallDetailsButton) + .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.StatusProgress) - .ObserveOnGuiThread() - .Select(p => p.Value) - .BindToStrict(this, view => view.TopProgressBar.ProgressPercent) - .DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.InstallCommand, v => v.RetryButton) + .DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.InstallCommand, v => v.InstallButton) + .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) + this.BindCommand(ViewModel, vm => vm.BackToGalleryCommand, v => v.BackToGalleryButton) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.HashingSpeed) + .BindToStrict(this, v => v.HashSpeedText.Text) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.ExtractingSpeed) + .BindToStrict(this, v => v.ExtractionSpeedText.Text) + .DisposeWith(disposables); + + this.WhenAnyValue(v => v.ViewModel.DownloadingSpeed) + .BindToStrict(this, v => v.DownloadSpeedText.Text) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.OpenReadmeCommand, v => v.OpenReadmeButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.OpenLogFolderCommand, v => v.OpenLogFolderButton) + .DisposeWith(disposables); + + + this.WhenAnyValue(x => x.ReadmeToggleButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.OpenReadmeButton.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.LogToggleButton.IsChecked) + .Select(x => x ?? false ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, x => x.OpenLogFolderButton.Visibility) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.InstallResult) + .ObserveOnGuiThread() + .Subscribe(result => + { + StoppedTitle.Text = result?.GetTitle() ?? string.Empty; + StoppedDescription.Text = result?.GetDescription() ?? string.Empty; + switch(result) + { + case InstallResult.DownloadFailed: + StoppedButton.Command = ViewModel.OpenMissingArchivesCommand; + StoppedButton.Icon = Symbol.DocumentGlobe; + StoppedButton.Text = "Show Missing Archives"; + break; + + default: + StoppedButton.Command = ViewModel.OpenInstallFolderCommand; + StoppedButton.Icon = Symbol.FolderOpen; + StoppedButton.Text = "Open File Explorer"; + break; + } + }) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.InstallState) + .ObserveOnGuiThread() + .Subscribe(x => + { + SetupGrid.Visibility = x == InstallState.Configuration ? Visibility.Visible : Visibility.Collapsed; + InstallationGrid.Visibility = x == InstallState.Installing || x == InstallState.Failure ? Visibility.Visible : Visibility.Collapsed; + CompletedInstallationGrid.Visibility = x == InstallState.Success ? Visibility.Visible : Visibility.Collapsed; + + CpuView.Visibility = x == InstallState.Installing ? Visibility.Visible : Visibility.Collapsed; + InstallationRightColumn.Width = x == InstallState.Installing ? new GridLength(3, GridUnitType.Star) : new GridLength(4, GridUnitType.Star); + WorkerIndicators.Visibility = x == InstallState.Installing ? Visibility.Visible : Visibility.Collapsed; + StoppedMessage.Visibility = x == InstallState.Failure ? Visibility.Visible : Visibility.Collapsed; + StoppedBorder.Background = x == InstallState.Failure ? (Brush)Application.Current.Resources["ErrorBrush"] : (Brush)Application.Current.Resources["SuccessBrush"]; + StoppedIcon.Symbol = x == InstallState.Failure ? Symbol.ErrorCircle : Symbol.CheckmarkCircle; + StoppedInstallMsg.Text = x == InstallState.Failure ? "Installation failed" : "Installation succeeded"; + + CancelButton.Visibility = x == InstallState.Installing ? Visibility.Visible : Visibility.Collapsed; + EditInstallDetailsButton.Visibility = x == InstallState.Failure ? Visibility.Visible : Visibility.Collapsed; + RetryButton.Visibility = x == InstallState.Failure ? Visibility.Visible : Visibility.Collapsed; + + + if (x == InstallState.Failure || x == InstallState.Success) + LogToggleButton.IsChecked = true; + + if (x == InstallState.Installing) + HideNavigation.Send(); + else + ShowNavigation.Send(); + }) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.SuggestedInstallFolder) + .ObserveOnGuiThread() + .Subscribe(x => + { + InstallationLocationPicker.Watermark = x; + if (string.IsNullOrEmpty(ViewModel?.Installer?.Location?.TargetPath.ToString())) + ViewModel.Installer.Location.TargetPath = (AbsolutePath)x; + }) .DisposeWith(disposables); - ViewModel.WhenAnyValue(vm => vm.SlideShowImage) - .BindToStrict(this, view => view.DetailImage.Image) + ViewModel.WhenAnyValue(vm => vm.SuggestedDownloadFolder) + .ObserveOnGuiThread() + .Subscribe(x => + { + DownloadLocationPicker.Watermark = x; + if (string.IsNullOrEmpty(ViewModel?.Installer?.DownloadLocation?.TargetPath.ToString())) + ViewModel.Installer.DownloadLocation.TargetPath = (AbsolutePath)x; + }) .DisposeWith(disposables); - }); - } + ViewModel.WhenAny(vm => vm.ModListImage) + .BindToStrict(this, v => v.DetailImage.Image) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModListImage) + .BindToStrict(this, v => v.InstallDetailImage.Image) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModListImage) + .BindToStrict(this, v => v.CompletedImage.Image) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModlistMetadata.Author) + .BindToStrict(this, v => v.DetailImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModlistMetadata.Author) + .BindToStrict(this, v => v.InstallDetailImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModlistMetadata.Author) + .BindToStrict(this, v => v.CompletedImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModlistMetadata.Title) + .BindToStrict(this, v => v.DetailImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModlistMetadata.Title) + .BindToStrict(this, v => v.InstallDetailImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModlistMetadata.Title) + .BindToStrict(this, v => v.CompletedImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ModlistMetadata.Version) + .BindToStrict(this, v => v.DetailImage.Version) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ModlistMetadata.Version) + .BindToStrict(this, v => v.InstallDetailImage.Version) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ModlistMetadata.Version) + .BindToStrict(this, v => v.CompletedImage.Version) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.LoadingLock.IsLoading) + .Select(loading => loading ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, v => v.ModlistLoadingRing.Visibility) + .DisposeWith(disposables); + + this.WhenAnyValue(x => x.ViewModel.ModList.Readme) + .Select(x => + { + var humanReadableReadme = UIUtils.GetHumanReadableReadmeLink(ViewModel.ModList.Readme); + if (Uri.TryCreate(humanReadableReadme, UriKind.Absolute, out var uri)) + { + return uri; + } + return default; + }) + .BindToStrict(this, x => x.ViewModel.ReadmeBrowser.Source) + .DisposeWith(disposables); + + ReadmeToggleButton.Events().Checked + .ObserveOnGuiThread() + .Subscribe(_ => + { + LogToggleButton.IsChecked = false; + LogView.Visibility = Visibility.Collapsed; + ReadmeBrowserGrid.Visibility = Visibility.Visible; + }) + .DisposeWith(disposables); + + LogToggleButton.Events().Checked + .ObserveOnGuiThread() + .Subscribe(_ => + { + ReadmeToggleButton.IsChecked = false; + LogView.Visibility = Visibility.Visible; + ReadmeBrowserGrid.Visibility = Visibility.Collapsed; + }) + .DisposeWith(disposables); + + + this.WhenAnyValue(x => x.ReadmeBrowserGrid.Visibility) + .Where(x => x == Visibility.Visible) + .Subscribe(x => + { + if (x == Visibility.Visible) + TakeWebViewOwnershipForReadme(); + }) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.OpenReadmeCommand, v => v.ReadmeButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.OpenInstallFolderCommand, v => v.OpenFolderButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.PlayCommand, v => v.PlayButton) + .DisposeWith(disposables); + + + // Initially, readme tab should be visible + ReadmeToggleButton.IsChecked = true; + + MessageBus.Current.Listen() + .Subscribe(msg => + { + if (msg.Screen == FloatingScreenType.None && ReadmeBrowserGrid.Visibility == Visibility.Visible) + TakeWebViewOwnershipForReadme(); + }) + .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); + + 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.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.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); + */ + }); + } + + private void TakeWebViewOwnershipForReadme() + { + RxApp.MainThreadScheduler.Schedule(() => + { + ViewModel.ReadmeBrowser.Margin = new Thickness(0, 0, 0, 16); + if (ViewModel.ReadmeBrowser.Parent != null) + { + ((Panel)ViewModel.ReadmeBrowser.Parent).Children.Remove(ViewModel.ReadmeBrowser); + } + ViewModel.ReadmeBrowser.Width = double.NaN; + ViewModel.ReadmeBrowser.Height = double.NaN; + ViewModel.ReadmeBrowser.Visibility = Visibility.Visible; + if(ViewModel?.ModList?.Readme != null) + ViewModel.ReadmeBrowser.Source = new Uri(UIUtils.GetHumanReadableReadmeLink(ViewModel.ModList.Readme)); + ReadmeBrowserGrid.Children.Add(ViewModel.ReadmeBrowser); + }); } } 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 b/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml index d394bcf57..727f9a56a 100644 --- a/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml +++ b/Wabbajack.App.Wpf/Views/Interventions/BethesdaNetLoginView.xaml @@ -61,7 +61,7 @@ Command="{Binding BackCommand}" Style="{StaticResource IconCircleButtonStyle}" ToolTip="Back to main menu"> - + 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..ce78fbdb9 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..43b213211 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml @@ -7,92 +7,183 @@ 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: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}" - mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - + WindowTitleBrush="{StaticResource BackgroundBrush}" + mc:Ignorable="d" + d:DataContext="{d:DesignInstance Type=local:MainWindowVM}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..758bef4dd --- /dev/null +++ b/Wabbajack.App.Wpf/Views/ModListDetailsView.xaml.cs @@ -0,0 +1,140 @@ +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 System.Windows.Controls; +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.CloseCommand, x => x.CloseButton) + .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 + { + var humanReadableReadme = UIUtils.GetHumanReadableReadmeLink(readme); + if(Uri.TryCreate(humanReadableReadme, UriKind.Absolute, out var uri)) { + return uri; + } + return default; + } + catch(Exception) + { + return new Uri(readme); + } + }) + .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..6a6c0980e 100644 --- a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml +++ b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml @@ -9,31 +9,171 @@ 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 +189,37 @@ - - - + + + HorizontalAlignment="Center" + VerticalAlignment="Top" + Symbol="DismissCircle" + IconVariant="Regular" + 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..1157f72cf 100644 --- a/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/ModListGalleryView.xaml.cs @@ -1,61 +1,114 @@ -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.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..caa112f03 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:BaseModListMetadataVM" 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..2fdd09d98 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..edd85804d --- /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..319e8ea93 --- /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, ScreenType.Installer] }, + { 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 eb135688b..5244f1a3a 100644 --- a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml @@ -40,12 +40,12 @@ diff --git a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs index 71e425296..ebe600044 100644 --- a/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/PerformanceSettingsView.xaml.cs @@ -3,26 +3,25 @@ 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.EditResourceSettings.Command = ReactiveCommand.Create(() => - { - UIUtils.OpenFile( - KnownFolders.WabbajackAppLocal.Combine("saved_settings", "resource_settings.json")); - Environment.Exit(0); - }); + this.WhenActivated(disposable => + { + this.EditResourceSettings.Command = ReactiveCommand.Create(() => + { + UIUtils.OpenFile( + KnownFolders.WabbajackAppLocal.Combine("saved_settings", "resource_settings.json")); + Environment.Exit(0); }); - } + }); } } diff --git a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml index 479a98035..013271db1 100644 --- a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml @@ -13,7 +13,7 @@ mc:Ignorable="d"> @@ -35,7 +35,7 @@ VerticalAlignment="Top" Style="{StaticResource IconCircleButtonStyle}" ToolTip="Back to main menu"> - + +/// 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.CloseCommand, 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 b/Wabbajack.App.Wpf/Views/WebBrowserView.xaml index 00dfaa522..c589960dc 100644 --- a/Wabbajack.App.Wpf/Views/WebBrowserView.xaml +++ b/Wabbajack.App.Wpf/Views/WebBrowserView.xaml @@ -62,7 +62,7 @@ Command="{Binding BackCommand}" Style="{StaticResource IconCircleButtonStyle}" ToolTip="Back to main menu"> - + 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..76765b23d 100644 --- a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj +++ b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj @@ -48,8 +48,15 @@ - + + + + + + + + @@ -59,8 +66,14 @@ - - + + + + Always + + + Always + TextTemplatingFileGenerator VerbRegistration.cs @@ -73,44 +86,55 @@ - - + + NU1701 - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + - - - - + + + + + + - - - - - + + + + + + + + + + + Never + diff --git a/Wabbajack.CLI.Builder/Wabbajack.CLI.Builder.csproj b/Wabbajack.CLI.Builder/Wabbajack.CLI.Builder.csproj index a280d319f..622b5bb78 100644 --- a/Wabbajack.CLI.Builder/Wabbajack.CLI.Builder.csproj +++ b/Wabbajack.CLI.Builder/Wabbajack.CLI.Builder.csproj @@ -7,11 +7,12 @@ - - - - - + + + + + + diff --git a/Wabbajack.CLI/Program.cs b/Wabbajack.CLI/Program.cs index cbcd99347..575fa4b32 100644 --- a/Wabbajack.CLI/Program.cs +++ b/Wabbajack.CLI/Program.cs @@ -51,11 +51,6 @@ private static async Task Main(string[] args) services.AddSingleton(); services.AddCLIVerbs(); - - - - - services.AddSingleton(); }).Build(); var service = host.Services.GetService(); diff --git a/Wabbajack.CLI/UserInterventionHandler.cs b/Wabbajack.CLI/UserInterventionHandler.cs deleted file mode 100644 index 28313f214..000000000 --- a/Wabbajack.CLI/UserInterventionHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using Wabbajack.DTOs.Interventions; -using Wabbajack.Networking.Steam.UserInterventions; - -namespace Wabbajack.CLI; - -public class UserInterventionHandler : IUserInterventionHandler -{ - public void Raise(IUserIntervention intervention) - { - if (intervention is GetAuthCode gac) - { - switch (gac.Type) - { - case GetAuthCode.AuthType.EmailCode: - Console.WriteLine("Please enter the Steam code that was just emailed to you"); - break; - case GetAuthCode.AuthType.TwoFactorAuth: - Console.WriteLine("Please enter your 2FA code for Steam"); - break; - default: - throw new ArgumentOutOfRangeException(); - } - gac.Finish(Console.ReadLine()!.Trim()); - } - } -} \ No newline at end of file diff --git a/Wabbajack.CLI/VerbRegistration.cs b/Wabbajack.CLI/VerbRegistration.cs index 93ed9e134..b41cb6f1b 100644 --- a/Wabbajack.CLI/VerbRegistration.cs +++ b/Wabbajack.CLI/VerbRegistration.cs @@ -45,12 +45,6 @@ public static void AddCLIVerbs(this IServiceCollection services) { services.AddSingleton(); CommandLineBuilder.RegisterCommand(ModlistReport.Definition, c => ((ModlistReport)c).Run); services.AddSingleton(); -CommandLineBuilder.RegisterCommand(SteamDownloadFile.Definition, c => ((SteamDownloadFile)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(SteamDumpAppInfo.Definition, c => ((SteamDumpAppInfo)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(SteamLogin.Definition, c => ((SteamLogin)c).Run); -services.AddSingleton(); CommandLineBuilder.RegisterCommand(UploadToNexus.Definition, c => ((UploadToNexus)c).Run); services.AddSingleton(); CommandLineBuilder.RegisterCommand(ValidateLists.Definition, c => ((ValidateLists)c).Run); diff --git a/Wabbajack.CLI/Verbs/Install.cs b/Wabbajack.CLI/Verbs/Install.cs index 1e29046f4..ba896b8e4 100644 --- a/Wabbajack.CLI/Verbs/Install.cs +++ b/Wabbajack.CLI/Verbs/Install.cs @@ -71,7 +71,7 @@ internal async Task Run(AbsolutePath wabbajack, AbsolutePath output, Absolu var result = await installer.Begin(token); - return result ? 0 : 2; + return result == InstallResult.Succeeded ? 0 : 2; } private async Task DownloadMachineUrl(string machineUrl, AbsolutePath wabbajack, CancellationToken token) diff --git a/Wabbajack.CLI/Verbs/InstallCompileInstallVerify.cs b/Wabbajack.CLI/Verbs/InstallCompileInstallVerify.cs index 3a36dc0df..9e58b14a8 100644 --- a/Wabbajack.CLI/Verbs/InstallCompileInstallVerify.cs +++ b/Wabbajack.CLI/Verbs/InstallCompileInstallVerify.cs @@ -79,7 +79,7 @@ public async Task Run(AbsolutePath outputs, AbsolutePath downloads, IEnumer GameFolder = _gameLocator.GameLocation(modlist.GameType) }); - var result = await installer.Begin(token); + var result = await installer.Begin(token) == InstallResult.Succeeded; if (!result) { _logger.LogInformation("Error installing {MachineUrl}", machineUrl); @@ -101,7 +101,7 @@ public async Task Run(AbsolutePath outputs, AbsolutePath downloads, IEnumer var compiler = MO2Compiler.Create(_serviceProvider, inferredSettings); result = await compiler.Begin(token); if (!result) - return result ? 0 : 3; + return 3; var installPath2 = outputs.Combine("verify_list"); @@ -122,7 +122,7 @@ public async Task Run(AbsolutePath outputs, AbsolutePath downloads, IEnumer GameFolder = _gameLocator.GameLocation(modlist2.GameType) }); - result = await installer2.Begin(token); + result = await installer2.Begin(token) == InstallResult.Succeeded; if (!result) { _logger.LogInformation("Error installing recompiled {MachineUrl}", machineUrl); diff --git a/Wabbajack.CLI/Verbs/SteamDownloadFile.cs b/Wabbajack.CLI/Verbs/SteamDownloadFile.cs deleted file mode 100644 index 16f66e393..000000000 --- a/Wabbajack.CLI/Verbs/SteamDownloadFile.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.CommandLine; -using System.CommandLine.Invocation; -using System.CommandLine.NamingConventionBinder; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using FluentFTP.Helpers; -using Microsoft.Extensions.Logging; -using SteamKit2; -using Wabbajack.CLI.Builder; -using Wabbajack.DTOs; -using Wabbajack.DTOs.JsonConverters; -using Wabbajack.Networking.Http.Interfaces; -using Wabbajack.Networking.Steam; -using Wabbajack.Paths; - -namespace Wabbajack.CLI.Verbs; - -public class SteamDownloadFile -{ - private readonly ILogger _logger; - private readonly Client _client; - private readonly ITokenProvider _token; - private readonly DepotDownloader _downloader; - private readonly DTOSerializer _dtos; - private readonly Wabbajack.Networking.WabbajackClientApi.Client _wjClient; - - public SteamDownloadFile(ILogger logger, Client steamClient, ITokenProvider token, - DepotDownloader downloader, DTOSerializer dtos, Wabbajack.Networking.WabbajackClientApi.Client wjClient) - { - _logger = logger; - _client = steamClient; - _token = token; - _downloader = downloader; - _dtos = dtos; - _wjClient = wjClient; - } - - public static VerbDefinition Definition = new("steam-download-file", - "Dumps information to the console about the given app", - new[] - { - new OptionDefinition(typeof(string), "g", "game", "Wabbajack game name"), - new OptionDefinition(typeof(string), "v", "version", "Version of the game to download for"), - new OptionDefinition(typeof(string), "f", "file", "File to download (relative path)"), - new OptionDefinition(typeof(string), "o", "output", "Output location") - }); - - internal async Task Run(string gameName, string version, string file, AbsolutePath output) - { - if (!GameRegistry.TryGetByFuzzyName(gameName, out var game)) - _logger.LogError("Can't find definition for {Game}", gameName); - - await _client.Login(); - - var definition = await _wjClient.GetGameArchives(game.Game, version); - var manifests = await _wjClient.GetSteamManifests(game.Game, version); - - _logger.LogInformation("Found {Count} manifests, looking for file", manifests.Length); - - SteamManifest? steamManifest = null; - DepotManifest? depotManifest = null; - DepotManifest.FileData? fileData = null; - - var appId = (uint) game.SteamIDs.First(); - - foreach (var manifest in manifests) - { - steamManifest = manifest; - depotManifest = await _client.GetAppManifest(appId, manifest.Depot, manifest.Manifest); - fileData = depotManifest.Files!.FirstOrDefault(f => f.FileName == file); - if (fileData != default) - { - break; - } - } - - if (fileData == default) - { - _logger.LogError("Cannot find {File} in any manifests", file); - return 1; - } - - _logger.LogInformation("File is {Size} and {ChunkCount} chunks", fileData.TotalSize.FileSizeToString(), fileData.Chunks.Count); - - await _client.Download(appId, depotManifest!.DepotID, steamManifest!.Manifest, fileData, output, CancellationToken.None); - - _logger.LogInformation("File downloaded"); - - return 0; - - - - } -} \ No newline at end of file diff --git a/Wabbajack.CLI/Verbs/SteamDumpAppInfo.cs b/Wabbajack.CLI/Verbs/SteamDumpAppInfo.cs deleted file mode 100644 index 4e61bd723..000000000 --- a/Wabbajack.CLI/Verbs/SteamDumpAppInfo.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.CommandLine.NamingConventionBinder; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using SteamKit2; -using Wabbajack.CLI.Builder; -using Wabbajack.DTOs; -using Wabbajack.DTOs.JsonConverters; -using Wabbajack.Networking.Http.Interfaces; -using Wabbajack.Networking.Steam; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace Wabbajack.CLI.Verbs; - -public class SteamDumpAppInfo -{ - private readonly ILogger _logger; - private readonly Client _client; - private readonly ITokenProvider _token; - private readonly DepotDownloader _downloader; - private readonly DTOSerializer _dtos; - - public SteamDumpAppInfo(ILogger logger, Client steamClient, ITokenProvider token, - DepotDownloader downloader, DTOSerializer dtos) - { - _logger = logger; - _client = steamClient; - _token = token; - _downloader = downloader; - _dtos = dtos; - } - - public static VerbDefinition Definition = new("steam-app-dump-info", - "Dumps information to the console about the given app", new[] - { - new OptionDefinition(typeof(string), "g", "game", "Wabbajack game name") - }); - - public Command MakeCommand() - { - var command = new Command("steam-app-dump-info"); - command.Description = "Dumps information to the console about the given app"; - - command.Add(new Option(new[] {"-g", "-game", "-gameName"}, "Wabbajack game name")); - command.Handler = CommandHandler.Create(Run); - return command; - } - - public async Task Run(string gameName) - { - if (!GameRegistry.TryGetByFuzzyName(gameName, out var game)) - { - _logger.LogError("Can't find game {GameName} in game registry", gameName); - return 1; - } - - await _client.Login(); - var appId = (uint) game.SteamIDs.First(); - - if (!await _downloader.AccountHasAccess(appId)) - { - _logger.LogError("Your account does not have access to this Steam App"); - return 1; - } - - var appData = await _downloader.GetAppInfo((uint)game.SteamIDs.First()); - - Console.WriteLine("App Depots: "); - - Console.WriteLine(_dtos.Serialize(appData, true)); - - return 0; - } - - -} \ No newline at end of file diff --git a/Wabbajack.CLI/Verbs/SteamLogin.cs b/Wabbajack.CLI/Verbs/SteamLogin.cs deleted file mode 100644 index 4fb45e363..000000000 --- a/Wabbajack.CLI/Verbs/SteamLogin.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.CommandLine; -using System.CommandLine.Invocation; -using System.CommandLine.NamingConventionBinder; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Wabbajack.CLI.Builder; -using Wabbajack.Networking.Http.Interfaces; -using Wabbajack.Networking.Steam; -using Wabbajack.Paths; - -namespace Wabbajack.CLI.Verbs; - -public class SteamLogin -{ - private readonly ILogger _logger; - private readonly Client _client; - private readonly ITokenProvider _token; - - public SteamLogin(ILogger logger, Client steamClient, ITokenProvider token) - { - _logger = logger; - _client = steamClient; - _token = token; - } - - public static VerbDefinition Definition = new("steam-login", - "Logs into Steam via interactive prompts", new[] - { - new OptionDefinition(typeof(string), "u", "user", "Username for login") - }); - - public async Task Run(string user) - { - var token = await _token.Get(); - - if (token == null || token.User != user || string.IsNullOrWhiteSpace(token.Password)) - { - Console.WriteLine("Please enter password"); - var password = Console.ReadLine() ?? ""; - - await _token.SetToken(new SteamLoginState - { - User = user, - Password = password.Trim() - }); - } - - _logger.LogInformation("Attempting login"); - await _client.Login(); - - await Task.Delay(10000); - - return 0; - } - -} \ No newline at end of file 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.CLI/Wabbajack.CLI.csproj b/Wabbajack.CLI/Wabbajack.CLI.csproj index 7760c1fa2..d5ec7523a 100644 --- a/Wabbajack.CLI/Wabbajack.CLI.csproj +++ b/Wabbajack.CLI/Wabbajack.CLI.csproj @@ -18,14 +18,16 @@ - - - - - - - - + + + + + + + + + + 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..027264845 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -33,8 +33,10 @@ - - + + + + diff --git a/Wabbajack.Compiler.Test/ModListHarness.cs b/Wabbajack.Compiler.Test/ModListHarness.cs index ec7490524..6552dbcc3 100644 --- a/Wabbajack.Compiler.Test/ModListHarness.cs +++ b/Wabbajack.Compiler.Test/ModListHarness.cs @@ -127,7 +127,7 @@ public async Task Install() var installer = scope.ServiceProvider.GetService()!; - return await installer.Begin(CancellationToken.None); + return await installer.Begin(CancellationToken.None) == InstallResult.Succeeded; } public async Task AddManualDownload(AbsolutePath path) diff --git a/Wabbajack.Compiler.Test/Startup.cs b/Wabbajack.Compiler.Test/Startup.cs index 7b001891b..f526d1c2d 100644 --- a/Wabbajack.Compiler.Test/Startup.cs +++ b/Wabbajack.Compiler.Test/Startup.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Wabbajack.DTOs.Interventions; -using Wabbajack.Networking.Steam.UserInterventions; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Services.OSIntegrated; using Xunit.DependencyInjection; @@ -22,33 +21,10 @@ public void ConfigureServices(IServiceCollection service) }); service.AddScoped(); - service.AddSingleton(); } public void Configure(ILoggerFactory loggerFactory, ITestOutputHelperAccessor accessor) { loggerFactory.AddProvider(new XunitTestOutputLoggerProvider(accessor, delegate { return true; })); } - - public class UserInterventionHandler : IUserInterventionHandler - { - public void Raise(IUserIntervention intervention) - { - if (intervention is GetAuthCode gac) - { - switch (gac.Type) - { - case GetAuthCode.AuthType.EmailCode: - Console.WriteLine("Please enter the Steam code that was just emailed to you"); - break; - case GetAuthCode.AuthType.TwoFactorAuth: - Console.WriteLine("Please enter your 2FA code for Steam"); - break; - default: - throw new ArgumentOutOfRangeException(); - } - gac.Finish(Console.ReadLine()!.Trim()); - } - } - } } \ No newline at end of file diff --git a/Wabbajack.Compiler.Test/Wabbajack.Compiler.Test.csproj b/Wabbajack.Compiler.Test/Wabbajack.Compiler.Test.csproj index 94a54844c..11760428a 100644 --- a/Wabbajack.Compiler.Test/Wabbajack.Compiler.Test.csproj +++ b/Wabbajack.Compiler.Test/Wabbajack.Compiler.Test.csproj @@ -9,18 +9,23 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Compiler/CompilerSettingsInferencer.cs b/Wabbajack.Compiler/CompilerSettingsInferencer.cs index b56dc000d..a3bafb634 100644 --- a/Wabbajack.Compiler/CompilerSettingsInferencer.cs +++ b/Wabbajack.Compiler/CompilerSettingsInferencer.cs @@ -54,7 +54,7 @@ public CompilerSettingsInferencer(ILogger logger) cs.ModListName = selectedProfile; cs.Profile = selectedProfile; - cs.OutputFile = cs.Source.Parent; + cs.OutputFile = cs.Source.Parent.Combine(cs.ModListName).Combine(Ext.Wabbajack); var settings = iniData["Settings"]; cs.Downloads = settings["download_directory"].FromMO2Ini().ToAbsolutePath(); @@ -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.Compiler/MO2Compiler.cs b/Wabbajack.Compiler/MO2Compiler.cs index e6b86452e..7e3efa725 100644 --- a/Wabbajack.Compiler/MO2Compiler.cs +++ b/Wabbajack.Compiler/MO2Compiler.cs @@ -304,6 +304,7 @@ public override IEnumerable MakeStack() new IgnoreFilename(this, ".refcache".ToRelativePath()), //Include custom categories / splash screens new IncludeRegex(this, @"categories\.dat$"), + new IncludeRegex(this, @"nexuscatmap\.dat$"), new IncludeRegex(this, @"splash\.png"), new IncludeAllConfigs(this), diff --git a/Wabbajack.Compiler/Wabbajack.Compiler.csproj b/Wabbajack.Compiler/Wabbajack.Compiler.csproj index 284335ba9..d1547c939 100644 --- a/Wabbajack.Compiler/Wabbajack.Compiler.csproj +++ b/Wabbajack.Compiler/Wabbajack.Compiler.csproj @@ -18,8 +18,12 @@ - + + + + + diff --git a/Wabbajack.Compression.BSA.Test/Wabbajack.Compression.BSA.Test.csproj b/Wabbajack.Compression.BSA.Test/Wabbajack.Compression.BSA.Test.csproj index 0285184d4..eac1d9983 100644 --- a/Wabbajack.Compression.BSA.Test/Wabbajack.Compression.BSA.Test.csproj +++ b/Wabbajack.Compression.BSA.Test/Wabbajack.Compression.BSA.Test.csproj @@ -7,20 +7,24 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Compression.BSA/Wabbajack.Compression.BSA.csproj b/Wabbajack.Compression.BSA/Wabbajack.Compression.BSA.csproj index 4565b2c92..2392ef696 100644 --- a/Wabbajack.Compression.BSA/Wabbajack.Compression.BSA.csproj +++ b/Wabbajack.Compression.BSA/Wabbajack.Compression.BSA.csproj @@ -18,7 +18,9 @@ - + + + diff --git a/Wabbajack.Compression.Zip.Test/Wabbajack.Compression.Zip.Test.csproj b/Wabbajack.Compression.Zip.Test/Wabbajack.Compression.Zip.Test.csproj index b658f0059..602f7f8a6 100644 --- a/Wabbajack.Compression.Zip.Test/Wabbajack.Compression.Zip.Test.csproj +++ b/Wabbajack.Compression.Zip.Test/Wabbajack.Compression.Zip.Test.csproj @@ -8,17 +8,18 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.DTOs.ConverterGenerators/Wabbajack.DTOs.ConverterGenerators.csproj b/Wabbajack.DTOs.ConverterGenerators/Wabbajack.DTOs.ConverterGenerators.csproj index f0a69d630..24217f6db 100644 --- a/Wabbajack.DTOs.ConverterGenerators/Wabbajack.DTOs.ConverterGenerators.csproj +++ b/Wabbajack.DTOs.ConverterGenerators/Wabbajack.DTOs.ConverterGenerators.csproj @@ -11,7 +11,12 @@ - + + + + + + diff --git a/Wabbajack.DTOs.Test/ModListTests.cs b/Wabbajack.DTOs.Test/ModListTests.cs index baf3d87e4..3a0249107 100644 --- a/Wabbajack.DTOs.Test/ModListTests.cs +++ b/Wabbajack.DTOs.Test/ModListTests.cs @@ -83,7 +83,7 @@ await statuses.PDoAll(new Resource("Resource Test", 4), async status => { _logger.LogInformation("Loading {machineURL}", status.MachineURL); - var detailed = await _wjClient.GetDetailedStatus(status.MachineURL); + var detailed = await _wjClient.GetDetailedStatus(status.MachineURL.Split('/')[0], status.MachineURL.Split('/')[1]); Assert.True(detailed.MachineURL == status.MachineURL); }); } diff --git a/Wabbajack.DTOs.Test/Wabbajack.DTOs.Test.csproj b/Wabbajack.DTOs.Test/Wabbajack.DTOs.Test.csproj index e4e878f97..9de63fa5e 100644 --- a/Wabbajack.DTOs.Test/Wabbajack.DTOs.Test.csproj +++ b/Wabbajack.DTOs.Test/Wabbajack.DTOs.Test.csproj @@ -7,22 +7,28 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + 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.DTOs/Wabbajack.DTOs.csproj b/Wabbajack.DTOs/Wabbajack.DTOs.csproj index 284b55ead..8e034f7e7 100644 --- a/Wabbajack.DTOs/Wabbajack.DTOs.csproj +++ b/Wabbajack.DTOs/Wabbajack.DTOs.csproj @@ -12,7 +12,8 @@ - + + diff --git a/Wabbajack.Downloaders.Bethesda/Wabbajack.Downloaders.Bethesda.csproj b/Wabbajack.Downloaders.Bethesda/Wabbajack.Downloaders.Bethesda.csproj index d012a5b6b..fb38b5387 100644 --- a/Wabbajack.Downloaders.Bethesda/Wabbajack.Downloaders.Bethesda.csproj +++ b/Wabbajack.Downloaders.Bethesda/Wabbajack.Downloaders.Bethesda.csproj @@ -13,7 +13,9 @@ - + + + diff --git a/Wabbajack.Downloaders.Dispatcher.Test/Wabbajack.Downloaders.Dispatcher.Test.csproj b/Wabbajack.Downloaders.Dispatcher.Test/Wabbajack.Downloaders.Dispatcher.Test.csproj index 99322309d..df3d6d56c 100644 --- a/Wabbajack.Downloaders.Dispatcher.Test/Wabbajack.Downloaders.Dispatcher.Test.csproj +++ b/Wabbajack.Downloaders.Dispatcher.Test/Wabbajack.Downloaders.Dispatcher.Test.csproj @@ -7,19 +7,23 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Downloaders.Dispatcher/DownloadDispatcher.cs b/Wabbajack.Downloaders.Dispatcher/DownloadDispatcher.cs index 5511a58b6..1d85da376 100644 --- a/Wabbajack.Downloaders.Dispatcher/DownloadDispatcher.cs +++ b/Wabbajack.Downloaders.Dispatcher/DownloadDispatcher.cs @@ -237,6 +237,7 @@ private async Task DownloadFromMirror(Archive archive, AbsolutePath destin { try { + _logger.LogInformation("Downloading {archiveName} from mirror, hash {archiveHash}", archive.Name, archive.Hash); var url = _wjClient.GetMirrorUrl(archive.Hash); if (url == null) return default; diff --git a/Wabbajack.Downloaders.Dispatcher/Wabbajack.Downloaders.Dispatcher.csproj b/Wabbajack.Downloaders.Dispatcher/Wabbajack.Downloaders.Dispatcher.csproj index 738168488..01c7960ae 100644 --- a/Wabbajack.Downloaders.Dispatcher/Wabbajack.Downloaders.Dispatcher.csproj +++ b/Wabbajack.Downloaders.Dispatcher/Wabbajack.Downloaders.Dispatcher.csproj @@ -25,7 +25,11 @@ - + + + + + diff --git a/Wabbajack.Downloaders.GameFile/Wabbajack.Downloaders.GameFile.csproj b/Wabbajack.Downloaders.GameFile/Wabbajack.Downloaders.GameFile.csproj index 2c4aed4f0..a4de6a07b 100644 --- a/Wabbajack.Downloaders.GameFile/Wabbajack.Downloaders.GameFile.csproj +++ b/Wabbajack.Downloaders.GameFile/Wabbajack.Downloaders.GameFile.csproj @@ -18,11 +18,14 @@ - - - - - + + + + + + + + diff --git a/Wabbajack.Downloaders.GoogleDrive/Wabbajack.Downloaders.GoogleDrive.csproj b/Wabbajack.Downloaders.GoogleDrive/Wabbajack.Downloaders.GoogleDrive.csproj index 552ba610c..275230da3 100644 --- a/Wabbajack.Downloaders.GoogleDrive/Wabbajack.Downloaders.GoogleDrive.csproj +++ b/Wabbajack.Downloaders.GoogleDrive/Wabbajack.Downloaders.GoogleDrive.csproj @@ -12,8 +12,10 @@ - - + + + + diff --git a/Wabbajack.Downloaders.Http/Wabbajack.Downloaders.Http.csproj b/Wabbajack.Downloaders.Http/Wabbajack.Downloaders.Http.csproj index 03862d2fb..dcb06d1a7 100644 --- a/Wabbajack.Downloaders.Http/Wabbajack.Downloaders.Http.csproj +++ b/Wabbajack.Downloaders.Http/Wabbajack.Downloaders.Http.csproj @@ -17,7 +17,9 @@ - + + + diff --git a/Wabbajack.Downloaders.IPS4OAuth2Downloader/Wabbajack.Downloaders.IPS4OAuth2Downloader.csproj b/Wabbajack.Downloaders.IPS4OAuth2Downloader/Wabbajack.Downloaders.IPS4OAuth2Downloader.csproj index 5898b6366..070ddd96e 100644 --- a/Wabbajack.Downloaders.IPS4OAuth2Downloader/Wabbajack.Downloaders.IPS4OAuth2Downloader.csproj +++ b/Wabbajack.Downloaders.IPS4OAuth2Downloader/Wabbajack.Downloaders.IPS4OAuth2Downloader.csproj @@ -17,8 +17,10 @@ - - + + + + diff --git a/Wabbajack.Downloaders.Interfaces/Wabbajack.Downloaders.Interfaces.csproj b/Wabbajack.Downloaders.Interfaces/Wabbajack.Downloaders.Interfaces.csproj index d68d5a9cd..8dc566102 100644 --- a/Wabbajack.Downloaders.Interfaces/Wabbajack.Downloaders.Interfaces.csproj +++ b/Wabbajack.Downloaders.Interfaces/Wabbajack.Downloaders.Interfaces.csproj @@ -7,6 +7,11 @@ $(VERSION) + + + + + diff --git a/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj b/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj index 921284497..527574e55 100644 --- a/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj +++ b/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj @@ -12,7 +12,9 @@ - + + + diff --git a/Wabbajack.Downloaders.MediaFire/Wabbajack.Downloaders.MediaFire.csproj b/Wabbajack.Downloaders.MediaFire/Wabbajack.Downloaders.MediaFire.csproj index 3928aa4f8..78add5a67 100644 --- a/Wabbajack.Downloaders.MediaFire/Wabbajack.Downloaders.MediaFire.csproj +++ b/Wabbajack.Downloaders.MediaFire/Wabbajack.Downloaders.MediaFire.csproj @@ -6,9 +6,10 @@ - - - + + + + diff --git a/Wabbajack.Downloaders.Mega/Wabbajack.Downloaders.Mega.csproj b/Wabbajack.Downloaders.Mega/Wabbajack.Downloaders.Mega.csproj index 351d8586f..f75932b26 100644 --- a/Wabbajack.Downloaders.Mega/Wabbajack.Downloaders.Mega.csproj +++ b/Wabbajack.Downloaders.Mega/Wabbajack.Downloaders.Mega.csproj @@ -12,8 +12,11 @@ - - + + + + + diff --git a/Wabbajack.Downloaders.ModDB/Wabbajack.Downloaders.ModDB.csproj b/Wabbajack.Downloaders.ModDB/Wabbajack.Downloaders.ModDB.csproj index a76274143..6a468b8ce 100644 --- a/Wabbajack.Downloaders.ModDB/Wabbajack.Downloaders.ModDB.csproj +++ b/Wabbajack.Downloaders.ModDB/Wabbajack.Downloaders.ModDB.csproj @@ -13,9 +13,12 @@ - - - + + + + + + diff --git a/Wabbajack.Downloaders.Nexus/Wabbajack.Downloaders.Nexus.csproj b/Wabbajack.Downloaders.Nexus/Wabbajack.Downloaders.Nexus.csproj index 15f7493b6..3288b17c7 100644 --- a/Wabbajack.Downloaders.Nexus/Wabbajack.Downloaders.Nexus.csproj +++ b/Wabbajack.Downloaders.Nexus/Wabbajack.Downloaders.Nexus.csproj @@ -7,6 +7,11 @@ $(VERSION) + + + + + diff --git a/Wabbajack.Downloaders.VerificationCache/Wabbajack.Downloaders.VerificationCache.csproj b/Wabbajack.Downloaders.VerificationCache/Wabbajack.Downloaders.VerificationCache.csproj index e155554bd..f6fd10541 100644 --- a/Wabbajack.Downloaders.VerificationCache/Wabbajack.Downloaders.VerificationCache.csproj +++ b/Wabbajack.Downloaders.VerificationCache/Wabbajack.Downloaders.VerificationCache.csproj @@ -12,8 +12,10 @@ - - + + + + diff --git a/Wabbajack.Downloaders.WabbajackCDN/Wabbajack.Downloaders.WabbajackCDN.csproj b/Wabbajack.Downloaders.WabbajackCDN/Wabbajack.Downloaders.WabbajackCDN.csproj index 61f8f5e0f..7f71c7e47 100644 --- a/Wabbajack.Downloaders.WabbajackCDN/Wabbajack.Downloaders.WabbajackCDN.csproj +++ b/Wabbajack.Downloaders.WabbajackCDN/Wabbajack.Downloaders.WabbajackCDN.csproj @@ -15,7 +15,9 @@ - + + + diff --git a/Wabbajack.Downloaders.WabbajackCDN/WabbajackCDNDownloader.cs b/Wabbajack.Downloaders.WabbajackCDN/WabbajackCDNDownloader.cs index 95d265c33..3441e038e 100644 --- a/Wabbajack.Downloaders.WabbajackCDN/WabbajackCDNDownloader.cs +++ b/Wabbajack.Downloaders.WabbajackCDN/WabbajackCDNDownloader.cs @@ -126,6 +126,7 @@ public override async Task Download(Archive archive, WabbajackCDN state, A private async Task GetDefinition(WabbajackCDN state, CancellationToken token) { + _logger.LogInformation("Getting file definition for CDN download {primaryKeyString}, {url}", state.PrimaryKeyString, state.Url); var msg = MakeMessage(new Uri(state.Url + "/definition.json.gz")); using var data = await _client.SendAsync(msg, token); if (!data.IsSuccessStatusCode) return null; diff --git a/Wabbajack.FileExtractor.Test/Wabbajack.FileExtractor.Test.csproj b/Wabbajack.FileExtractor.Test/Wabbajack.FileExtractor.Test.csproj index 42e75e785..74c6cbdfd 100644 --- a/Wabbajack.FileExtractor.Test/Wabbajack.FileExtractor.Test.csproj +++ b/Wabbajack.FileExtractor.Test/Wabbajack.FileExtractor.Test.csproj @@ -7,21 +7,24 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.FileExtractor/Wabbajack.FileExtractor.csproj b/Wabbajack.FileExtractor/Wabbajack.FileExtractor.csproj index a85e27df5..c1784a3ee 100644 --- a/Wabbajack.FileExtractor/Wabbajack.FileExtractor.csproj +++ b/Wabbajack.FileExtractor/Wabbajack.FileExtractor.csproj @@ -29,7 +29,9 @@ - + + + diff --git a/Wabbajack.Hashing.PHash.Test/Wabbajack.Hashing.PHash.Test.csproj b/Wabbajack.Hashing.PHash.Test/Wabbajack.Hashing.PHash.Test.csproj index c3181a1a1..3e157d36f 100644 --- a/Wabbajack.Hashing.PHash.Test/Wabbajack.Hashing.PHash.Test.csproj +++ b/Wabbajack.Hashing.PHash.Test/Wabbajack.Hashing.PHash.Test.csproj @@ -7,20 +7,23 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + + + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Hashing.PHash/Wabbajack.Hashing.PHash.csproj b/Wabbajack.Hashing.PHash/Wabbajack.Hashing.PHash.csproj index 8cd08d916..37fddf622 100644 --- a/Wabbajack.Hashing.PHash/Wabbajack.Hashing.PHash.csproj +++ b/Wabbajack.Hashing.PHash/Wabbajack.Hashing.PHash.csproj @@ -9,7 +9,10 @@ + + + diff --git a/Wabbajack.Hashing.xxHash64.Benchmark/Wabbajack.Hashing.xxHash64.Benchmark.csproj b/Wabbajack.Hashing.xxHash64.Benchmark/Wabbajack.Hashing.xxHash64.Benchmark.csproj index 9d734ec89..f8036da12 100644 --- a/Wabbajack.Hashing.xxHash64.Benchmark/Wabbajack.Hashing.xxHash64.Benchmark.csproj +++ b/Wabbajack.Hashing.xxHash64.Benchmark/Wabbajack.Hashing.xxHash64.Benchmark.csproj @@ -8,7 +8,8 @@ - + + diff --git a/Wabbajack.Hashing.xxHash64.Test/Wabbajack.Hashing.xxHash64.Test.csproj b/Wabbajack.Hashing.xxHash64.Test/Wabbajack.Hashing.xxHash64.Test.csproj index 3e15de15d..97b2821f0 100644 --- a/Wabbajack.Hashing.xxHash64.Test/Wabbajack.Hashing.xxHash64.Test.csproj +++ b/Wabbajack.Hashing.xxHash64.Test/Wabbajack.Hashing.xxHash64.Test.csproj @@ -7,18 +7,19 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all 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.Installer.Test/StandardInstallerTest.cs b/Wabbajack.Installer.Test/StandardInstallerTest.cs index 3385565df..aad8fe714 100644 --- a/Wabbajack.Installer.Test/StandardInstallerTest.cs +++ b/Wabbajack.Installer.Test/StandardInstallerTest.cs @@ -57,7 +57,7 @@ public async Task CanInstallAList() configuration.IgnoreMirrorList = true; var installer = _provider.GetService(); - Assert.True(await installer.Begin(CancellationToken.None)); + Assert.True(await installer.Begin(CancellationToken.None) == InstallResult.Succeeded); Assert.True("ModOrganizer.exe".ToRelativePath().RelativeTo(installFolder).FileExists()); } diff --git a/Wabbajack.Installer.Test/Wabbajack.Installer.Test.csproj b/Wabbajack.Installer.Test/Wabbajack.Installer.Test.csproj index 2256e9845..0166409f1 100644 --- a/Wabbajack.Installer.Test/Wabbajack.Installer.Test.csproj +++ b/Wabbajack.Installer.Test/Wabbajack.Installer.Test.csproj @@ -7,18 +7,22 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Installer/AInstaller.cs b/Wabbajack.Installer/AInstaller.cs index 6f4c79e04..153695fb7 100644 --- a/Wabbajack.Installer/AInstaller.cs +++ b/Wabbajack.Installer/AInstaller.cs @@ -131,7 +131,7 @@ public void UpdateProgress(long stepProgress) Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress), _currentStep)); } - public abstract Task Begin(CancellationToken token); + public abstract Task Begin(CancellationToken token); protected async Task ExtractModlist(CancellationToken token) { @@ -184,13 +184,13 @@ public static async Task LoadFromFile(DTOSerializer serializer, Absolut } } - public static async Task ModListImageStream(AbsolutePath path) + public static async Task ModListImageStream(AbsolutePath path) { await using var fs = path.Open(FileMode.Open, FileAccess.Read, FileShare.Read); using var ar = new ZipArchive(fs, ZipArchiveMode.Read); var entry = ar.GetEntry("modlist-image.png"); if (entry == null) - throw new InvalidDataException("No modlist image found"); + return null; return new MemoryStream(await entry.Open().ReadAllAsync()); } diff --git a/Wabbajack.Installer/InstallResult.cs b/Wabbajack.Installer/InstallResult.cs new file mode 100644 index 000000000..00a936429 --- /dev/null +++ b/Wabbajack.Installer/InstallResult.cs @@ -0,0 +1,13 @@ +namespace Wabbajack.Installer +{ + public enum InstallResult + { + Succeeded, + Cancelled, + Errored, + GameMissing, + GameInvalid, + DownloadFailed, + NotEnoughSpace, + } +} diff --git a/Wabbajack.Installer/StandardInstaller.cs b/Wabbajack.Installer/StandardInstaller.cs index 5df533685..dc255305e 100644 --- a/Wabbajack.Installer/StandardInstaller.cs +++ b/Wabbajack.Installer/StandardInstaller.cs @@ -66,9 +66,9 @@ public static StandardInstaller Create(IServiceProvider provider, InstallerConfi provider.GetRequiredService()); } - public override async Task Begin(CancellationToken token) + public override async Task Begin(CancellationToken token) { - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; _logger.LogInformation("Installing: {Name} - {Version}", _configuration.ModList.Name, _configuration.ModList.Version); await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name); NextStep(Consts.StepPreparing, "Configuring Installer", 0); @@ -82,23 +82,25 @@ public override async Task Begin(CancellationToken token) var otherGame = _configuration.Game.MetaData().CommonlyConfusedWith .Where(g => _gameLocator.IsInstalled(g)).Select(g => g.MetaData()).FirstOrDefault(); if (otherGame != null) + { _logger.LogError( "In order to do a proper install Wabbajack needs to know where your {lookingFor} folder resides. However this game doesn't seem to be installed, we did however find an installed " + "copy of {otherGame}, did you install the wrong game?", _configuration.Game.MetaData().HumanFriendlyGameName, otherGame.HumanFriendlyGameName); + } else _logger.LogError( "In order to do a proper install Wabbajack needs to know where your {lookingFor} folder resides. However this game doesn't seem to be installed.", _configuration.Game.MetaData().HumanFriendlyGameName); - return false; + return InstallResult.GameMissing; } if (!_configuration.GameFolder.DirectoryExists()) { _logger.LogError("Located game {game} at \"{gameFolder}\" but the folder does not exist!", _configuration.Game, _configuration.GameFolder); - return false; + return InstallResult.GameInvalid; } @@ -111,55 +113,50 @@ public override async Task Begin(CancellationToken token) _configuration.Downloads.CreateDirectory(); await OptimizeModlist(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await HashArchives(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await DownloadArchives(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await HashArchives(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; var missing = ModList.Archives.Where(a => !HashedArchives.ContainsKey(a.Hash)).ToList(); if (missing.Count > 0) { - if (missing.Any(m => m.State is not Nexus)) - { - ShowMissingManualReport(missing.Where(m => m.State is not Nexus).ToArray()); - return false; - } - foreach (var a in missing) _logger.LogCritical("Unable to download {name} ({primaryKeyString})", a.Name, a.State.PrimaryKeyString); _logger.LogCritical("Cannot continue, was unable to download one or more archives"); - return false; + + return InstallResult.DownloadFailed; } await ExtractModlist(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await PrimeVFS(); await BuildFolderStructure(); await InstallArchives(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await InstallIncludedFiles(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await WriteMetaFiles(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await BuildBSAs(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; // TODO: Port this await GenerateZEditMerges(token); - if (token.IsCancellationRequested) return false; + if (token.IsCancellationRequested) return InstallResult.Cancelled; await ForcePortable(); await RemapMO2File(); @@ -173,48 +170,9 @@ public override async Task Begin(CancellationToken token) NextStep(Consts.StepFinished, "Finished", 1); _logger.LogInformation("Finished Installation"); - return true; + return InstallResult.Succeeded; } - private void ShowMissingManualReport(Archive[] toArray) - { - _logger.LogError("Writing Manual helper report"); - var report = _configuration.Downloads.Combine("MissingManuals.html"); - { - using var writer = new StreamWriter(report.Open(FileMode.Create, FileAccess.Write, FileShare.None)); - writer.Write("Missing Manual Downloads"); - writer.Write("

Missing Manual Downloads

"); - writer.Write( - "

Wabbajack was unable to download the following archives automatically. Please download them manually and place them in the downloads folder you chose during the install setup.

"); - foreach (var archive in toArray) - { - switch (archive.State) - { - case Manual manual: - writer.Write($"

{archive.Name}

"); - writer.Write($"

{manual.Prompt}

"); - writer.Write($"

Download URL: {manual.Url}

"); - break; - case MediaFire mediaFire: - writer.Write($"

{archive.Name}

"); - writer.Write($"

Download URL: {mediaFire.Url}

"); - break; - default: - writer.Write($"

{archive.Name}

"); - writer.Write($"

Unknown download type

"); - writer.Write($"

Primary Key (may not be helpful): {archive.State.PrimaryKeyString}

"); - break; - } - } - - writer.Write(""); - } - - Process.Start(new ProcessStartInfo("cmd.exe", $"start /c \"{report}\"") - { - CreateNoWindow = true, - }); - } private Task RemapMO2File() { diff --git a/Wabbajack.Installer/Wabbajack.Installer.csproj b/Wabbajack.Installer/Wabbajack.Installer.csproj index 015cfcd66..31eb53f53 100644 --- a/Wabbajack.Installer/Wabbajack.Installer.csproj +++ b/Wabbajack.Installer/Wabbajack.Installer.csproj @@ -24,7 +24,11 @@ - + + + + + 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..e4cda14ce 100644 --- a/Wabbajack.Launcher/Wabbajack.Launcher.csproj +++ b/Wabbajack.Launcher/Wabbajack.Launcher.csproj @@ -19,20 +19,25 @@ net9.0-windows - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + + + + + + + + + + + + + + diff --git a/Wabbajack.Networking.BethesdaNet/Wabbajack.Networking.BethesdaNet.csproj b/Wabbajack.Networking.BethesdaNet/Wabbajack.Networking.BethesdaNet.csproj index 45fbc5bce..8dd3b7ef8 100644 --- a/Wabbajack.Networking.BethesdaNet/Wabbajack.Networking.BethesdaNet.csproj +++ b/Wabbajack.Networking.BethesdaNet/Wabbajack.Networking.BethesdaNet.csproj @@ -10,6 +10,11 @@ CS8600,CS8601,CS8618,CS8604 + + + + + ..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\6.0.1\Microsoft.Extensions.Logging.Abstractions.dll diff --git a/Wabbajack.Networking.Discord/Wabbajack.Networking.Discord.csproj b/Wabbajack.Networking.Discord/Wabbajack.Networking.Discord.csproj index 26b586aca..f48cc23fe 100644 --- a/Wabbajack.Networking.Discord/Wabbajack.Networking.Discord.csproj +++ b/Wabbajack.Networking.Discord/Wabbajack.Networking.Discord.csproj @@ -16,7 +16,8 @@ - + + diff --git a/Wabbajack.Networking.GitHub/Wabbajack.Networking.GitHub.csproj b/Wabbajack.Networking.GitHub/Wabbajack.Networking.GitHub.csproj index 1c4b108a1..a0d70b15f 100644 --- a/Wabbajack.Networking.GitHub/Wabbajack.Networking.GitHub.csproj +++ b/Wabbajack.Networking.GitHub/Wabbajack.Networking.GitHub.csproj @@ -17,8 +17,10 @@ - - + + + + diff --git a/Wabbajack.Networking.Http.Test/Wabbajack.Networking.Http.Test.csproj b/Wabbajack.Networking.Http.Test/Wabbajack.Networking.Http.Test.csproj index 8d71f1be0..bea6fda81 100644 --- a/Wabbajack.Networking.Http.Test/Wabbajack.Networking.Http.Test.csproj +++ b/Wabbajack.Networking.Http.Test/Wabbajack.Networking.Http.Test.csproj @@ -8,17 +8,20 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Networking.Http/ServiceExtensions.cs b/Wabbajack.Networking.Http/ServiceExtensions.cs index b6c871c0f..8a1b8af76 100644 --- a/Wabbajack.Networking.Http/ServiceExtensions.cs +++ b/Wabbajack.Networking.Http/ServiceExtensions.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; using System; using Wabbajack.Networking.Http.Interfaces; @@ -10,5 +12,6 @@ public static void AddResumableHttpDownloader(this IServiceCollection services) { services.AddHttpClient("ResumableClient").ConfigureHttpClient(c => c.Timeout = TimeSpan.FromMinutes(5)); services.AddSingleton(); + services.RemoveAll(); } } \ No newline at end of file diff --git a/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj b/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj index 4f158d080..c577a81a5 100644 --- a/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj +++ b/Wabbajack.Networking.Http/Wabbajack.Networking.Http.csproj @@ -8,9 +8,10 @@ - - - + + + + diff --git a/Wabbajack.Networking.NexusApi.Test/Wabbajack.Networking.NexusApi.Test.csproj b/Wabbajack.Networking.NexusApi.Test/Wabbajack.Networking.NexusApi.Test.csproj index 0ad8a0ad6..fc9697500 100644 --- a/Wabbajack.Networking.NexusApi.Test/Wabbajack.Networking.NexusApi.Test.csproj +++ b/Wabbajack.Networking.NexusApi.Test/Wabbajack.Networking.NexusApi.Test.csproj @@ -7,19 +7,24 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + + + + + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Networking.NexusApi/Wabbajack.Networking.NexusApi.csproj b/Wabbajack.Networking.NexusApi/Wabbajack.Networking.NexusApi.csproj index dbd139db4..571cefe0a 100644 --- a/Wabbajack.Networking.NexusApi/Wabbajack.Networking.NexusApi.csproj +++ b/Wabbajack.Networking.NexusApi/Wabbajack.Networking.NexusApi.csproj @@ -12,8 +12,9 @@ - - + + + diff --git a/Wabbajack.Networking.Steam.Test/Wabbajack.Networking.Steam.Test.csproj b/Wabbajack.Networking.Steam.Test/Wabbajack.Networking.Steam.Test.csproj index 874745d30..99b921d4c 100644 --- a/Wabbajack.Networking.Steam.Test/Wabbajack.Networking.Steam.Test.csproj +++ b/Wabbajack.Networking.Steam.Test/Wabbajack.Networking.Steam.Test.csproj @@ -8,20 +8,24 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Networking.Steam/Wabbajack.Networking.Steam.csproj b/Wabbajack.Networking.Steam/Wabbajack.Networking.Steam.csproj index 8b182a385..ba1950ee0 100644 --- a/Wabbajack.Networking.Steam/Wabbajack.Networking.Steam.csproj +++ b/Wabbajack.Networking.Steam/Wabbajack.Networking.Steam.csproj @@ -11,8 +11,10 @@ - - + + + + diff --git a/Wabbajack.Networking.WabbajackClientApi/Client.cs b/Wabbajack.Networking.WabbajackClientApi/Client.cs index b20b08b5c..8e18c6d96 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; @@ -171,10 +172,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))!; } @@ -231,14 +232,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(); } }) @@ -263,12 +264,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.Networking.WabbajackClientApi/Wabbajack.Networking.WabbajackClientApi.csproj b/Wabbajack.Networking.WabbajackClientApi/Wabbajack.Networking.WabbajackClientApi.csproj index 3a8ad26a0..efa72396e 100644 --- a/Wabbajack.Networking.WabbajackClientApi/Wabbajack.Networking.WabbajackClientApi.csproj +++ b/Wabbajack.Networking.WabbajackClientApi/Wabbajack.Networking.WabbajackClientApi.csproj @@ -12,9 +12,11 @@ - - - + + + + + diff --git a/Wabbajack.Paths.IO.Test/Wabbajack.Paths.IO.Test.csproj b/Wabbajack.Paths.IO.Test/Wabbajack.Paths.IO.Test.csproj index e976433e9..69e853dd8 100644 --- a/Wabbajack.Paths.IO.Test/Wabbajack.Paths.IO.Test.csproj +++ b/Wabbajack.Paths.IO.Test/Wabbajack.Paths.IO.Test.csproj @@ -7,18 +7,19 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.Paths.Test/Wabbajack.Paths.Test.csproj b/Wabbajack.Paths.Test/Wabbajack.Paths.Test.csproj index f8562675a..ca84c6fce 100644 --- a/Wabbajack.Paths.Test/Wabbajack.Paths.Test.csproj +++ b/Wabbajack.Paths.Test/Wabbajack.Paths.Test.csproj @@ -7,17 +7,18 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.RateLimiter.Test/Wabbajack.RateLimiter.Test.csproj b/Wabbajack.RateLimiter.Test/Wabbajack.RateLimiter.Test.csproj index f9069357c..855aa1e3a 100644 --- a/Wabbajack.RateLimiter.Test/Wabbajack.RateLimiter.Test.csproj +++ b/Wabbajack.RateLimiter.Test/Wabbajack.RateLimiter.Test.csproj @@ -8,17 +8,18 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all 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; diff --git a/Wabbajack.Server.Lib/Wabbajack.Server.Lib.csproj b/Wabbajack.Server.Lib/Wabbajack.Server.Lib.csproj index 4fe810833..f57a06219 100644 --- a/Wabbajack.Server.Lib/Wabbajack.Server.Lib.csproj +++ b/Wabbajack.Server.Lib/Wabbajack.Server.Lib.csproj @@ -16,8 +16,12 @@ - + + + + + diff --git a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs index 572588125..a72d50db9 100644 --- a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs +++ b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs @@ -26,7 +26,6 @@ using Wabbajack.Networking.Http; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.Networking.NexusApi; -using Wabbajack.Networking.Steam; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Paths; using Wabbajack.Paths.IO; @@ -159,8 +158,6 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service service.AddSingleton(); service.AddResumableHttpDownloader(); - service.AddSteam(); - service.AddSingleton(); service.AddSingleton(); service.AddBethesdaNet(); @@ -177,10 +174,6 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service .AddAllSingleton, EncryptedJsonTokenProvider, VectorPlexusTokenProvider>(); - service - .AddAllSingleton, EncryptedJsonTokenProvider, - SteamTokenProvider>(); - service.AddAllSingleton, WabbajackApiTokenProvider>(); service diff --git a/Wabbajack.Services.OSIntegrated/TokenProviders/SteamTokenProvider.cs b/Wabbajack.Services.OSIntegrated/TokenProviders/SteamTokenProvider.cs deleted file mode 100644 index 1431fee3a..000000000 --- a/Wabbajack.Services.OSIntegrated/TokenProviders/SteamTokenProvider.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.Logging; -using Wabbajack.DTOs.JsonConverters; -using Wabbajack.Networking.Steam; - -namespace Wabbajack.Services.OSIntegrated.TokenProviders; - -public class SteamTokenProvider : EncryptedJsonTokenProvider -{ - public SteamTokenProvider(ILogger logger, DTOSerializer dtos) : base(logger, dtos, - "steam-login") - { - } -} \ No newline at end of file diff --git a/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj b/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj index 55e7bfb54..9a2f1a885 100644 --- a/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj +++ b/Wabbajack.Services.OSIntegrated/Wabbajack.Services.OSIntegrated.csproj @@ -12,12 +12,15 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + @@ -26,7 +29,6 @@ - diff --git a/Wabbajack.VFS.Interfaces/Wabbajack.VFS.Interfaces.csproj b/Wabbajack.VFS.Interfaces/Wabbajack.VFS.Interfaces.csproj index 26877da82..a8bf9ba74 100644 --- a/Wabbajack.VFS.Interfaces/Wabbajack.VFS.Interfaces.csproj +++ b/Wabbajack.VFS.Interfaces/Wabbajack.VFS.Interfaces.csproj @@ -6,6 +6,11 @@ enable + + + + + diff --git a/Wabbajack.VFS.Test/Wabbajack.VFS.Test.csproj b/Wabbajack.VFS.Test/Wabbajack.VFS.Test.csproj index 48a56f1e5..8b86d4daf 100644 --- a/Wabbajack.VFS.Test/Wabbajack.VFS.Test.csproj +++ b/Wabbajack.VFS.Test/Wabbajack.VFS.Test.csproj @@ -7,25 +7,29 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + + + + + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Wabbajack.VFS/Wabbajack.VFS.csproj b/Wabbajack.VFS/Wabbajack.VFS.csproj index cf70222c4..31db6e8bd 100644 --- a/Wabbajack.VFS/Wabbajack.VFS.csproj +++ b/Wabbajack.VFS/Wabbajack.VFS.csproj @@ -12,8 +12,11 @@ - - + + + + + diff --git a/Wabbajack.sln b/Wabbajack.sln index bedde649f..2c2610a80 100644 --- a/Wabbajack.sln +++ b/Wabbajack.sln @@ -108,8 +108,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solutionItems", ".solutionItems", "{109037C8-CF2F-4179-B064-A66147BC18C5}" ProjectSection(SolutionItems) = preProject .gitignore = .gitignore - nuget.config = nuget.config CHANGELOG.md = CHANGELOG.md + nuget.config = nuget.config README.md = README.md EndProjectSection EndProject @@ -117,10 +117,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wabbajack.Downloaders.GameF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wabbajack.Launcher", "Wabbajack.Launcher\Wabbajack.Launcher.csproj", "{23D49FCC-A6CB-4873-879B-F90DA1871AA3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wabbajack.Networking.Steam", "Wabbajack.Networking.Steam\Wabbajack.Networking.Steam.csproj", "{AB9A5C22-10CC-4EE0-A808-FB1DC9E24247}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Wabbajack.Networking.Steam.Test", "Wabbajack.Networking.Steam.Test\Wabbajack.Networking.Steam.Test.csproj", "{D6351587-CAF6-4CB6-A2BD-5368E69F297C}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{18E36813-CB53-4172-8FF3-EFE3B9B30A5F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Networking.Http.Test", "Wabbajack.Networking.Http.Test\Wabbajack.Networking.Http.Test.csproj", "{34FC755D-24F0-456A-B5C1-5BA7F12DC233}" @@ -345,14 +341,6 @@ Global {23D49FCC-A6CB-4873-879B-F90DA1871AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU {23D49FCC-A6CB-4873-879B-F90DA1871AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU {23D49FCC-A6CB-4873-879B-F90DA1871AA3}.Release|Any CPU.Build.0 = Release|Any CPU - {AB9A5C22-10CC-4EE0-A808-FB1DC9E24247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB9A5C22-10CC-4EE0-A808-FB1DC9E24247}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB9A5C22-10CC-4EE0-A808-FB1DC9E24247}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB9A5C22-10CC-4EE0-A808-FB1DC9E24247}.Release|Any CPU.Build.0 = Release|Any CPU - {D6351587-CAF6-4CB6-A2BD-5368E69F297C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D6351587-CAF6-4CB6-A2BD-5368E69F297C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D6351587-CAF6-4CB6-A2BD-5368E69F297C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D6351587-CAF6-4CB6-A2BD-5368E69F297C}.Release|Any CPU.Build.0 = Release|Any CPU {34FC755D-24F0-456A-B5C1-5BA7F12DC233}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {34FC755D-24F0-456A-B5C1-5BA7F12DC233}.Debug|Any CPU.Build.0 = Debug|Any CPU {34FC755D-24F0-456A-B5C1-5BA7F12DC233}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -440,8 +428,6 @@ Global {29AC8A68-D5EC-43F5-B2CC-72A75545E418} = {98B731EE-4FC0-4482-A069-BCBA25497871} {DEB4B073-4EAA-49FD-9D43-F0F8CB930E7A} = {F01F8595-5FD7-4506-8469-F4A5522DACC1} {4F252332-CA77-41DE-95A8-9DF38A81D675} = {98B731EE-4FC0-4482-A069-BCBA25497871} - {AB9A5C22-10CC-4EE0-A808-FB1DC9E24247} = {F01F8595-5FD7-4506-8469-F4A5522DACC1} - {D6351587-CAF6-4CB6-A2BD-5368E69F297C} = {F01F8595-5FD7-4506-8469-F4A5522DACC1} {34FC755D-24F0-456A-B5C1-5BA7F12DC233} = {F01F8595-5FD7-4506-8469-F4A5522DACC1} {10165025-D30B-44B7-A764-50E15603AE56} = {F677890D-5109-43BC-97C7-C4CD47C8EE0C} {64AD7E26-5643-4969-A61C-E0A90FA25FCB} = {F677890D-5109-43BC-97C7-C4CD47C8EE0C}