diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..f95026326 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# CS0618: Type or member is obsolete +dotnet_diagnostic.CS0618.severity = none 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..d69cfc78f 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.IO; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Runtime.InteropServices; @@ -26,176 +28,201 @@ 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); + protected override void OnExit(ExitEventArgs e) + { + base.OnExit(e); + } - var principle = new WindowsPrincipal(identity); - return principle.IsInRole(WindowsBuiltInRole.Administrator); + private static bool IsAdmin() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; - } - catch (Exception) - { - return false; - } - } + try + { + var identity = WindowsIdentity.GetCurrent(); + var owner = identity.Owner; + if (owner is not null) return owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); + + var principle = new WindowsPrincipal(identity); + return principle.IsInRole(WindowsBuiltInRole.Administrator); - 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"); - loggingBuilder.Services.AddSingleton(uiTarget); + var uiTarget = new LogStream + { + Name = "ui", + Layout = "${message:withexception=false}", + }; - config.AddRuleForAllLevels(fileTarget); - config.AddRuleForAllLevels(consoleTarget); - config.AddRuleForAllLevels(uiTarget); + loggingBuilder.Services.AddSingleton(uiTarget); - loggingBuilder.ClearProviders(); - loggingBuilder.SetMinimumLevel(LogLevel.Information); - loggingBuilder.AddNLog(config); - } + config.AddRuleForAllLevels(fileTarget); + config.AddRuleForAllLevels(consoleTarget); + config.AddRuleForAllLevels(uiTarget); - 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; - } + 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()); + + // 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(); + 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..108bf0fdc 100644 --- a/Wabbajack.App.Wpf/Consts.cs +++ b/Wabbajack.App.Wpf/Consts.cs @@ -1,6 +1,7 @@ using System; using Wabbajack.Paths; using Wabbajack.Paths.IO; +using Wabbajack.RateLimiter; namespace Wabbajack; @@ -9,6 +10,12 @@ 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 Uri TlsInfoUri => new("https://www.howsmyssl.com/a/check"); 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 +25,5 @@ public static class Consts public static byte SettingsVersion = 0; public static RelativePath NativeSettingsJson = "native_settings.json".ToRelativePath(); + public const string AllSavedCompilerSettingsPaths = "compiler_settings_paths"; } \ 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..05b2b77c4 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; @@ -13,12 +10,13 @@ public interface INeedsLogin string SiteName { get; } ICommand TriggerLogin { get; set; } ICommand ClearLogin { get; set; } + ICommand ToggleLogin { get; set; } ImageSource Icon { get; set; } Type LoginFor(); + public bool LoggedIn { get; set; } } public interface ILoginFor : INeedsLogin where T : IDownloader { - } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs index 8e982af75..4cdd8bbe9 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; @@ -30,6 +25,7 @@ public class LoversLabLoginManager : ViewModel, ILoginFor public string SiteName { get; } = "Lovers Lab"; public ICommand TriggerLogin { get; set; } public ICommand ClearLogin { get; set; } + public ICommand ToggleLogin { get; set; } public ImageSource Icon { get; set; } public Type LoginFor() @@ -38,7 +34,7 @@ public Type LoginFor() } [Reactive] - public bool HaveLogin { get; set; } + public bool LoggedIn { get; set; } public LoversLabLoginManager(ILogger logger, ITokenProvider token, IServiceProvider serviceProvider) { @@ -52,7 +48,7 @@ public LoversLabLoginManager(ILogger logger, ITokenProvid _logger.LogInformation("Deleting Login information for {SiteName}", SiteName); await _token.Delete(); RefreshTokenState(); - }, this.WhenAnyValue(v => v.HaveLogin)); + }, this.WhenAnyValue(v => v.LoggedIn)); Icon = BitmapFrame.Create( typeof(LoversLabLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.lovers_lab.png")!); @@ -61,20 +57,24 @@ public LoversLabLoginManager(ILogger logger, ITokenProvid { _logger.LogInformation("Logging into {SiteName}", SiteName); StartLogin(); - }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); + }, this.WhenAnyValue(v => v.LoggedIn).Select(v => !v)); + + ToggleLogin = ReactiveCommand.Create(() => + { + if (LoggedIn) ClearLogin.Execute(null); + else TriggerLogin.Execute(null); + }); } 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() { - HaveLogin = _token.HaveToken(); + LoggedIn = _token.HaveToken(); } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs index 27ff83543..d1209a652 100644 --- a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs @@ -1,6 +1,7 @@ using System; using System.Reactive.Linq; using System.Threading.Tasks; +using System.Windows; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -10,6 +11,7 @@ using ReactiveUI.Fody.Helpers; using Wabbajack.Downloaders; using Wabbajack.DTOs.Logins; +using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.UserIntervention; @@ -24,6 +26,7 @@ public class NexusLoginManager : ViewModel, ILoginFor public string SiteName { get; } = "Nexus Mods"; public ICommand TriggerLogin { get; set; } public ICommand ClearLogin { get; set; } + public ICommand ToggleLogin { get; set; } public ImageSource Icon { get; set; } public Type LoginFor() @@ -32,30 +35,34 @@ public Type LoginFor() } [Reactive] - public bool HaveLogin { get; set; } + public bool LoggedIn { get; set; } public NexusLoginManager(ILogger logger, ITokenProvider token, IServiceProvider serviceProvider) { _logger = logger; _token = token; _serviceProvider = serviceProvider; - Task.Run(async () => await RefreshTokenState()); + Task.Run(RefreshTokenState); ClearLogin = ReactiveCommand.CreateFromTask(async () => { _logger.LogInformation("Deleting Login information for {SiteName}", SiteName); await ClearLoginToken(); - }, this.WhenAnyValue(v => v.HaveLogin)); + }, this.WhenAnyValue(v => v.LoggedIn)); - Icon = BitmapFrame.Create( - typeof(NexusLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.nexus.png")!); + Icon = (DrawingImage)Application.Current.Resources["NexusLogo"]; TriggerLogin = ReactiveCommand.CreateFromTask(async () => { _logger.LogInformation("Logging into {SiteName}", SiteName); - //MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService())); StartLogin(); - }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); + }, this.WhenAnyValue(v => v.LoggedIn).Select(v => !v)); + + ToggleLogin = ReactiveCommand.Create(() => + { + if (LoggedIn) ClearLogin.Execute(null); + else TriggerLogin.Execute(null); + }); } private async Task ClearLoginToken() @@ -66,17 +73,23 @@ 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 (_, _) => await RefreshTokenState(); + ShowBrowserWindow.Send(handler); } private async Task RefreshTokenState() { - var token = await _token.Get(); + NexusOAuthState token = null; + try + { + token = await _token.Get(); + } + catch(Exception ex) + { + _logger.LogError("Failed to refresh Nexus token state: {ex}", ex.ToString()); + } - HaveLogin = _token.HaveToken() && !(token?.OAuth?.IsExpired ?? true); + LoggedIn = _token.HaveToken() && !(token?.OAuth?.IsExpired ?? true); } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs index 62a13e260..7f43150db 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; @@ -29,6 +25,7 @@ public class VectorPlexusLoginManager : ViewModel, ILoginFor logger, ITokenProvider token, IServiceProvider serviceProvider) { @@ -51,7 +48,7 @@ public VectorPlexusLoginManager(ILogger logger, IToken _logger.LogInformation("Deleting Login information for {SiteName}", SiteName); await _token.Delete(); RefreshTokenState(); - }, this.WhenAnyValue(v => v.HaveLogin)); + }, this.WhenAnyValue(v => v.LoggedIn)); Icon = BitmapFrame.Create( typeof(VectorPlexusLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.vector_plexus.png")!); @@ -60,22 +57,26 @@ public VectorPlexusLoginManager(ILogger logger, IToken { _logger.LogInformation("Logging into {SiteName}", SiteName); StartLogin(); - }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); + }, this.WhenAnyValue(v => v.LoggedIn).Select(v => !v)); + + ToggleLogin = ReactiveCommand.Create(() => + { + if (LoggedIn) ClearLogin.Execute(null); + else TriggerLogin.Execute(null); + }); } 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()); } private void RefreshTokenState() { - HaveLogin = _token.HaveToken(); + LoggedIn = _token.HaveToken(); } } \ No newline at end of file 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..df35a39fa --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs @@ -0,0 +1,26 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; + +public enum FloatingScreenType +{ + None, + ModListDetails, + FileUpload +} + +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..d5825acd7 100644 --- a/Wabbajack.App.Wpf/Settings.cs +++ b/Wabbajack.App.Wpf/Settings.cs @@ -1,28 +1,107 @@ -using Wabbajack.Downloaders; +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Wabbajack.Downloaders; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Paths; using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated; 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 PerformanceSettingVM : ViewModel +{ + private readonly ResourceSettingsManager _manager; + [Reactive] public string HumanName { get; set; } + [Reactive] public long MaxTasks { get; set; } + [Reactive] public long MaxThroughput { get; set; } + public PerformanceSettingVM(ResourceSettingsManager manager) { + _manager = manager; + + this.WhenActivated(disposables => + { + this.WhenAnyValue(x => x.MaxTasks, x => x.MaxThroughput) + .Throttle(TimeSpan.FromSeconds(0.5)) + .Subscribe(async mt => + { + var setting = new ResourceSettingsManager.ResourceSetting() + { + MaxTasks = mt.Item1, + MaxThroughput = mt.Item2 + }; + await manager.SetSetting(HumanName, setting); + }) + .DisposeWith(disposables); + }); } +} + +public class PerformanceSettingsVM : ViewModel +{ - public class PerformanceSettings : ViewModel + private readonly ResourceSettingsManager _settingsManager; + + public SourceList _settings = new(); + public ReadOnlyObservableCollection Settings; + [Reactive] public int MaxThreads { get; set; } + + public PerformanceSettingsVM(IResource downloadResources, SystemParametersConstructor systemParams, ResourceSettingsManager manager) { - private readonly Configuration.MainSettings _settings; + var p = systemParams.Create(); + + _settingsManager = manager; + MaxThreads = Environment.ProcessorCount; - public PerformanceSettings(Configuration.MainSettings settings, IResource downloadResources, SystemParametersConstructor systemParams) + this.WhenActivated(async disposables => { - var p = systemParams.Create(); + var settings = (await _settingsManager.GetSettings()).Select((kv) => + { + return new PerformanceSettingVM(manager) + { + HumanName = kv.Key, + MaxTasks = kv.Value.MaxTasks, + MaxThroughput = kv.Value.MaxThroughput + }; + }); + + _settings.Edit(s => + { + s.Clear(); + s.AddRange(settings); + }); + + _settings.Connect() + .Bind(out Settings) + .Subscribe() + .DisposeWith(disposables); - _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..9f90935a3 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 - @@ -256,9 +532,20 @@ + + + + --> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + x:Name="Border" + Background="{TemplateBinding Background}" + BorderBrush="{TemplateBinding BorderBrush}" + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="8"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + - @@ -1299,7 +1712,7 @@ - + + + + - - + + @@ -1333,33 +1755,113 @@ + + + - - + + - + + + + + + + + + + + + + - - + + @@ -1889,14 +2400,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..10d1fc33b 100644 --- a/Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs +++ b/Wabbajack.App.Wpf/Util/SystemParametersConstructor.cs @@ -1,16 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; +using System.Management; 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; @@ -113,16 +110,42 @@ public SystemParameters Create() } var memory = GetMemoryStatus(); + var gpuName = GetGPUName(); return new SystemParameters { ScreenWidth = width, ScreenHeight = height, VideoMemorySize = (long)dxgiMemory, SystemMemorySize = (long)memory.ullTotalPhys, - SystemPageSize = (long)memory.ullTotalPageFile - (long)memory.ullTotalPhys + SystemPageSize = (long)memory.ullTotalPageFile - (long)memory.ullTotalPhys, + GpuName = gpuName }; } - + + private string GetGPUName() + { + string gpuName = ""; + try + { + ManagementObjectSearcher videoControllers = new ManagementObjectSearcher("SELECT * FROM Win32_VideoController"); + + uint gpuRefreshRate = 0; + + foreach (ManagementObject obj in videoControllers.Get()) + { + var currentRefreshRate = (uint)obj["CurrentRefreshRate"]; + if (currentRefreshRate > gpuRefreshRate) + gpuName = obj["Description"].ToString(); + } + } + catch(Exception ex) + { + _logger.LogError("Failed to get GPU information: {ex}", ex.ToString()); + } + + return gpuName; + } + [return: MarshalAs(UnmanagedType.Bool)] [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] static extern bool GlobalMemoryStatusEx([In, Out] MEMORYSTATUSEX lpBuffer); 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..ba597776e 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,47 @@ 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()) + { + delegateMethod.DynamicInvoke(this, null); + Closed -= delegateMethod as EventHandler; + } + } + //Activator.Deactivate(); + } public async Task RunWrapper(CancellationToken token) { @@ -40,7 +69,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 +99,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 +117,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 +130,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 +145,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 +156,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 +194,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/FileUploadVM.cs b/Wabbajack.App.Wpf/ViewModels/FileUploadVM.cs new file mode 100644 index 000000000..705b6ab59 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/FileUploadVM.cs @@ -0,0 +1,107 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using System.Web; +using System.Windows; +using System.Windows.Input; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.DTOs.Logins; +using Wabbajack.Messages; +using Wabbajack.Networking.WabbajackClientApi; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated.TokenProviders; + +namespace Wabbajack; + +public class FileUploadVM : ViewModel +{ + + private readonly ILogger _logger; + private readonly WabbajackApiTokenProvider _tokenProvider; + private readonly Client _wjClient; + + public ICommand BrowseFileCommand { get; } + public ICommand BrowseAndUploadFileCommand { get; } + public ICommand CopyUrlCommand { get; } + public ICommand UploadCommand { get; } + public ICommand CloseCommand { get; } + public ICommand BrowseUploadsCommand { get; private set; } + public ICommand UploadMoreFilesCommand { get; private set; } + + [Reactive] public double UploadProgress { get; set; } + [Reactive] public string FileUrl { get; set; } + public FilePickerVM Picker { get;} + + private Subject _isUploading = new(); + private IObservable IsUploading { get; } + public WabbajackApiState ApiToken { get; private set; } + + public FileUploadVM(ILogger logger, WabbajackApiTokenProvider tokenProvider, Client wjClient, SettingsVM vm) + { + _logger = logger; + _tokenProvider = tokenProvider; + _wjClient = wjClient; + IsUploading = _isUploading; + Picker = new FilePickerVM(this); + + Task.Run(async () => + { + ApiToken = await _tokenProvider.Get(); + BrowseUploadsCommand = ReactiveCommand.Create(async () => + { + var authorApiKey = ApiToken?.AuthorKey; + UIUtils.OpenWebsite(new Uri($"{Consts.WabbajackBuildServerUri}author_controls/login/{authorApiKey}")); + }); + }); + + BrowseFileCommand = Picker.ConstructTypicalPickerCommand(IsUploading.StartWith(false).Select(u => !u)); + BrowseAndUploadFileCommand = ReactiveCommand.Create(() => { + BrowseFileCommand.Execute(null); + UploadCommand.Execute(null); + }); + + CopyUrlCommand = ReactiveCommand.Create(() => Clipboard.SetText(FileUrl)); + + UploadCommand = ReactiveCommand.Create(async () => + { + _isUploading.OnNext(true); + try + { + var (progress, task) = await _wjClient.UploadAuthorFile(Picker.TargetPath); + + var disposable = progress.Subscribe(m => + { + FileUrl = m.Message; + if(m.PercentDone != Percent.Zero) UploadProgress = (double)m.PercentDone; + }); + + var final = await task; + disposable.Dispose(); + FileUrl = final.ToString(); + } + catch (Exception ex) + { + _logger.LogError("Failed to upload file to CDN: {ex}", ex.ToString()); + FileUrl = ex.ToString(); + } + finally + { + FileUrl = FileUrl.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)); + + UploadMoreFilesCommand = ReactiveCommand.Create(() => + { + UploadProgress = 0; + }); + + CloseCommand = ReactiveCommand.Create(() => ShowFloatingWindow.Send(FloatingScreenType.None)); + } + +} 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..c3a45165f --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/ModListGalleryVM.cs @@ -0,0 +1,366 @@ +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() + .Filter(searchTextPredicates) + .Filter(onlyInstalledGamesFilter) + .Filter(includeUnofficialFilter) + .Filter(includeNSFWFilter) + .Filter(gameFilter) + .Filter(minModlistSizeFilter) + .Filter(maxModlistSizeFilter) + .Filter(includedTagsFilter) + .Filter(includedModsFilter) + .SortAndBind(out _filteredModLists, searchSorter) + .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..51949dfab --- /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; set; } + public ICommand BrowseCommand { get; set; } + public ReactiveCommand UpdateCommand { get; set; } + + [Reactive] + public ModlistMetadata[] Modlists { get; private set; } + + public ICommand InfoCommand { get; set; } +} 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..3a9c98c98 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs @@ -0,0 +1,410 @@ +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; +using Wabbajack.Util; +using System.IO; +using System.Net.Http; + +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 FileUploadVM FileUploadVM; + public readonly UserInterventionHandlers UserInterventionHandlers; + private readonly Client _wjClient; + private readonly ILogger _logger; + private readonly ResourceMonitor _resourceMonitor; + private readonly SystemParametersConstructor _systemParams; + + 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, FileUploadVM fileUploadVM, SystemParametersConstructor systemParams, HttpClient httpClient) + { + _logger = logger; + _wjClient = wjClient; + _resourceMonitor = resourceMonitor; + _serviceProvider = serviceProvider; + _systemParams = systemParams; + ConverterRegistration.Register(); + InstallerVM = installerVM; + CompilerHomeVM = compilerHomeVM; + CompilerDetailsVM = compilerDetailsVM; + CompilerFileManagerVM = compilerFileManagerVM; + CompilerMainVM = compilerMainVM; + SettingsPaneVM = settingsVM; + GalleryVM = modListGalleryVM; + HomeVM = homeVM; + WebBrowserVM = webBrowserVM; + NavigationVM = navigationVM; + InfoVM = infoVM; + ModListDetailsVM = modlistDetailsVM; + FileUploadVM = fileUploadVM; + 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!"); + + var fvi = FileVersionInfo.GetVersionInfo(string.IsNullOrWhiteSpace(assemblyLocation) ? processLocation : assemblyLocation); + Consts.CurrentMinimumWabbajackVersion = Version.Parse(fvi.FileVersion); + + _logger.LogInformation("Wabbajack information:"); + _logger.LogInformation(" Version: {FileVersion}", fvi.FileVersion); + _logger.LogInformation(" Build: {Sha}", ThisAssembly.Git.Sha); + _logger.LogInformation(" Entry point: {EntryPoint}", KnownFolders.EntryPoint); + _logger.LogInformation(" Assembly Location: {AssemblyLocation}", assemblyLocation); + _logger.LogInformation(" Process Location: {ProcessLocation}", processLocation); + + WindowTitle = Consts.AppName; + + _logger.LogInformation("General information:"); + _logger.LogInformation(" Windows version: {Version}", Environment.OSVersion.VersionString); + + var p = _systemParams.Create(); + + _logger.LogInformation("System information: "); + _logger.LogInformation(" GPU: {GpuName} ({VRAM})", p.GpuName, p.VideoMemorySize.ToFileSizeString()); + _logger.LogInformation(" RAM: {MemorySize}", p.SystemMemorySize.ToFileSizeString()); + _logger.LogInformation(" Primary display resolution: {ScreenWidth}x{ScreenHeight}", p.ScreenWidth, p.ScreenHeight); + _logger.LogInformation(" Pagefile: {PageSize}", p.SystemPageSize.ToFileSizeString()); + _logger.LogInformation(" VideoMemorySizeMb (ENB): {EnbLEVRAMSize}", p.EnbLEVRAMSize.ToString()); + + try + { + _logger.LogInformation("System partitions: "); + var drives = DriveHelper.Drives; + var partitions = DriveHelper.Partitions; + foreach (var drive in drives) + { + if (!drive.IsReady || drive.DriveType != DriveType.Fixed) continue; + var driveType = partitions[drive.RootDirectory.Name[0]].MediaType.ToString(); + var rootDir = drive.RootDirectory.ToString(); + var freeSpace = drive.AvailableFreeSpace.ToFileSizeString(); + _logger.LogInformation(" {RootDir} ({DriveType}): {FreeSpace} free", rootDir, driveType, freeSpace); + } + } + catch(Exception ex) + { + _logger.LogWarning("Failed to retrieve drive information: {ex}", ex.ToString()); + } + + try + { + Task.Run(async () => + { + var response = await httpClient.GetAsync(Consts.TlsInfoUri); + var content = await response.Content.ReadAsStringAsync(); + _logger.LogInformation("TLS Information: {content}", content); + }); + } + catch(Exception ex) + { + _logger.LogError("An error occurred while retrieving TLS information: {ex}", ex.ToString()); + } + + if (p.SystemPageSize == 0) + _logger.LogWarning("Pagefile is disabled! This will cause issues such as crashing with Wabbajack and other applications!"); + + + Task.Run(() => _wjClient.SendMetric("started_wabbajack", fvi.FileVersion)).FireAndForget(); + Task.Run(() => _wjClient.SendMetric("started_sha", ThisAssembly.Git.Sha)).FireAndForget(); + + 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()); + if(ActiveFloatingPane != null) ((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, + FloatingScreenType.FileUpload => FileUploadVM, + _ => 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/LoginManagerVM.cs b/Wabbajack.App.Wpf/ViewModels/Settings/LoginManagerVM.cs new file mode 100644 index 000000000..e785ab7ac --- /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 ReactiveUI.Fody.Helpers; +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..4dd28584a --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Settings/SettingsVM.cs @@ -0,0 +1,107 @@ +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 ReactiveUI.Fody.Helpers; +using Wabbajack.Common; +using Wabbajack.Downloaders; +using Wabbajack.DTOs.Logins; +using Wabbajack.LoginManagers; +using Wabbajack.Messages; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Services.OSIntegrated.TokenProviders; +using Wabbajack.Util; + +namespace Wabbajack; + +public class SettingsVM : ViewModel +{ + private readonly ILogger _logger; + private readonly Configuration.MainSettings _settings; + private readonly SettingsManager _settingsManager; + + public LoginManagerVM Login { get; } + public PerformanceSettingsVM Performance { get; } + + public ICommand LaunchCLICommand { get; } + public ICommand ResetCommand { get; } + public ICommand OpenFileUploadCommand { get; } + public ICommand BrowseUploadsCommand { get; private set; } + [Reactive] public WabbajackApiState ApiToken { get; private set; } + + public SettingsVM(ILogger logger, IServiceProvider provider) + { + _logger = logger; + _settings = provider.GetRequiredService(); + _settingsManager = provider.GetRequiredService(); + Task.Run(async () => + { + ApiToken = await provider.GetRequiredService().Get(); + BrowseUploadsCommand = ReactiveCommand.Create(async () => + { + var authorApiKey = ApiToken?.AuthorKey; + UIUtils.OpenWebsite(new Uri($"{Consts.WabbajackBuildServerUri}author_controls/login/{authorApiKey}")); + }); + }); + + Login = new LoginManagerVM(provider.GetRequiredService>(), this, + provider.GetRequiredService>()); + LaunchCLICommand = ReactiveCommand.CreateFromTask(LaunchCLI); + ResetCommand = ReactiveCommand.Create(Reset); + OpenFileUploadCommand = ReactiveCommand.Create(OpenFileUpload); + Performance = new PerformanceSettingsVM( + provider.GetRequiredService>(), + provider.GetRequiredService(), + provider.GetRequiredService()); + } + + private void OpenFileUpload() => ShowFloatingWindow.Send(FloatingScreenType.FileUpload); + + private void Reset() + { + try + { + var currentPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location); + var cliDir = Path.Combine(currentPath, "cli"); + string workingDir = Directory.Exists(cliDir) ? cliDir : currentPath; + Process.Start(new ProcessStartInfo() + { + FileName = "wabbajack-cli.exe", + Arguments = "reset", + CreateNoWindow = true + }); + } + catch (Exception ex) + { + _logger.LogError("Failed to reset Wabbajack: {ex}", ex); + } + } + + private async Task LaunchCLI() + { + try + { + var currentPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location); + var cliDir = Path.Combine(currentPath, "cli"); + string workingDir = Directory.Exists(cliDir) ? cliDir : currentPath; + var process = new ProcessStartInfo + { + FileName = "cmd.exe", + WorkingDirectory = workingDir, + Arguments = $"/k \"wabbajack-cli.exe -h\"", + }; + Process.Start(process); + } + catch (Exception ex) + { + _logger.LogError("Error while launching Wabbajack CLI: {ex}", ex); + } + } +} 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..e3e8296da --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Common/WJButton.xaml.cs @@ -0,0 +1,228 @@ +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 IconVariant? IconVariant + { + get => (IconVariant?)GetValue(IconVariantProperty); + set => SetValue(IconVariantProperty, value); + } + public static readonly DependencyProperty IconVariantProperty = DependencyProperty.Register(nameof(IconVariant), typeof(IconVariant?), typeof(WJButton), new FrameworkPropertyMetadata(null, 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.IconVariant) + .ObserveOnGuiThread() + .Subscribe((variant) => + { + if(variant != null) + { + ButtonSymbolIcon.IconVariant = (IconVariant)variant; + } + }) + .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/FileUploadView.xaml b/Wabbajack.App.Wpf/Views/FileUploadView.xaml new file mode 100644 index 000000000..d113ac8ba --- /dev/null +++ b/Wabbajack.App.Wpf/Views/FileUploadView.xaml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Choose a file + + + + + Please only upload files in accordance with the CDN usage policy on the wiki. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Upload complete! + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/FileUploadView.xaml.cs b/Wabbajack.App.Wpf/Views/FileUploadView.xaml.cs new file mode 100644 index 000000000..2beda0547 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/FileUploadView.xaml.cs @@ -0,0 +1,133 @@ +using System; +using ReactiveUI; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ReactiveMarbles.ObservableEvents; +using System.Windows.Input; +using System.Windows; +using System.IO; +using Wabbajack.Paths; + +namespace Wabbajack; + +public partial class FileUploadView : ReactiveUserControl +{ + public FileUploadView() + { + InitializeComponent(); + + this.WhenActivated(disposables => + { + this.BindCommand(ViewModel, vm => vm.CloseCommand, v => v.CloseButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.BrowseUploadsCommand, v => v.BrowseUploadsButton) + .DisposeWith(disposables); + + this.BindCommand(ViewModel, vm => vm.UploadMoreFilesCommand, v => v.UploadMoreFilesButton) + .DisposeWith(disposables); + + UploadBackground.Events().DragEnter + .ObserveOnGuiThread() + .Subscribe(_ => OnDragEnter()) + .DisposeWith(disposables); + + StartUploadIcon.Events().DragEnter + .ObserveOnGuiThread() + .Subscribe(_ => OnDragEnter()) + .DisposeWith(disposables); + + DragToUploadText.Events().DragEnter + .ObserveOnGuiThread() + .Subscribe(_ => OnDragEnter()) + .DisposeWith(disposables); + + UploadBackground.Events().DragLeave + .ObserveOnGuiThread() + .Subscribe(_ => OnDragLeave()) + .DisposeWith(disposables); + + StartUploadIcon.Events().DragLeave + .ObserveOnGuiThread() + .Subscribe(_ => OnDragLeave()) + .DisposeWith(disposables); + + DragToUploadText.Events().DragLeave + .ObserveOnGuiThread() + .Subscribe(_ => OnDragLeave()) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.UploadProgress) + .ObserveOnGuiThread() + .Select(up => $"{Math.Round(up * 100)}%") + .Subscribe(up => ProgressText.Text = up) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.UploadProgress) + .ObserveOnGuiThread() + .Subscribe(progress => + { + UploadBackground.AllowDrop = progress <= 0; + + if (progress <= 0) + { + StartUploadGrid.Visibility = Visibility.Visible; + UploadingGrid.Visibility = Visibility.Collapsed; + UploadCompletedGrid.Visibility = Visibility.Collapsed; + } + else if (progress > 0 && progress < 1) + { + StartUploadGrid.Visibility = Visibility.Collapsed; + UploadingGrid.Visibility = Visibility.Visible; + UploadCompletedGrid.Visibility = Visibility.Collapsed; + } + else if (progress >= 1) + { + StartUploadGrid.Visibility = Visibility.Collapsed; + UploadingGrid.Visibility = Visibility.Collapsed; + UploadCompletedGrid.Visibility = Visibility.Visible; + } + }) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.FileUrl) + .BindToStrict(this, v => v.FileUrlText.Text) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.CopyUrlCommand) + .BindToStrict(this, v => v.FileUrlHyperlink.Command) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.BrowseAndUploadFileCommand) + .BindToStrict(this, v => v.ChooseFileHyperlink.Command) + .DisposeWith(disposables); + }); + } + + private void OnDragEnter() + { + StartUploadIcon.IconVariant = FluentIcons.Common.IconVariant.Filled; + UploadBackground.Fill = (System.Windows.Media.Brush)Application.Current.Resources["BackgroundBrush"]; + } + + private void OnDragLeave() + { + StartUploadIcon.IconVariant = FluentIcons.Common.IconVariant.Regular; + UploadBackground.Fill = (System.Windows.Media.Brush)Application.Current.Resources["ComplementaryPrimary08Brush"]; + } + + private void OnDrop(object sender, System.Windows.DragEventArgs e) + { + OnDragLeave(); + + if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return; + + ViewModel.UploadProgress = 0; + + var filePath = (AbsolutePath)((string[])e.Data.GetData(DataFormats.FileDrop))[0]; + ViewModel.Picker.TargetPath = filePath; + ViewModel.UploadCommand.Execute(null); + + } +} + diff --git a/Wabbajack.App.Wpf/Views/HomeView.xaml b/Wabbajack.App.Wpf/Views/HomeView.xaml new file mode 100644 index 000000000..b0abdfa1c --- /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..35580ca0d 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs @@ -1,108 +1,371 @@ 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..45164aa6e 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml @@ -7,92 +7,186 @@ 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..8d78de149 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"> - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -43,118 +183,43 @@ - + - - - + + + 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..0c9d809ca 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,76 @@ - + - - + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + - - - - - - - - + - + - + - + - - - + + + + + + + + + + + + + + + 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 deleted file mode 100644 index fe2efd4b0..000000000 --- a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs deleted file mode 100644 index e9a0f67a8..000000000 --- a/Wabbajack.App.Wpf/Views/Settings/AuthorFilesView.xaml.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Windows.Controls; -using ReactiveUI; -using Wabbajack.View_Models.Settings; - -namespace Wabbajack -{ - public partial class AuthorFilesView : ReactiveUserControl - { - public AuthorFilesView() - { - InitializeComponent(); - } - } -} - diff --git a/Wabbajack.App.Wpf/Views/Settings/FileUploadSettingsView.xaml b/Wabbajack.App.Wpf/Views/Settings/FileUploadSettingsView.xaml new file mode 100644 index 000000000..3bf8a2dbb --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Settings/FileUploadSettingsView.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Settings/FileUploadSettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/FileUploadSettingsView.xaml.cs new file mode 100644 index 000000000..15e799950 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Settings/FileUploadSettingsView.xaml.cs @@ -0,0 +1,33 @@ +using System.Windows; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ReactiveUI; + +namespace Wabbajack; + +/// +/// Interaction logic for FileUploadSettingsView.xaml +/// +public partial class FileUploadSettingsView : ReactiveUserControl +{ + public FileUploadSettingsView() + { + InitializeComponent(); + + this.WhenActivated(disposable => + { + this.WhenAnyValue(x => x.ViewModel.OpenFileUploadCommand) + .BindToStrict(this, x => x.OpenFileUploadButton.Command) + .DisposeWith(disposable); + + this.WhenAnyValue(x => x.ViewModel.BrowseUploadsCommand) + .BindToStrict(this, x => x.BrowseUploadedFilesButton.Command) + .DisposeWith(disposable); + + ViewModel.WhenAnyValue(vm => vm.ApiToken.AuthorKey) + .Select(token => !string.IsNullOrEmpty(token) ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, v => v.OpenFileUploadButton.Visibility) + .DisposeWith(disposable); + }); + } +} diff --git a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml index 77ecef267..ef1b71de3 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml @@ -10,31 +10,18 @@ d:DesignWidth="800" x:TypeArguments="local:LoginTargetVM" mc:Ignorable="d"> - + - - - - + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs index 23b3f3311..98dd18c70 100644 --- a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs @@ -1,43 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Windows; 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; +using System.Reactive.Linq; + +namespace Wabbajack; -namespace Wabbajack +/// +/// Interaction logic for SettingsView.xaml +/// +public partial class SettingsView : ReactiveUserControl { - /// - /// Interaction logic for SettingsView.xaml - /// - public partial class SettingsView : ReactiveUserControl + public SettingsView() { - public SettingsView() + InitializeComponent(); + this.WhenActivated(disposable => { - InitializeComponent(); - this.WhenActivated(disposable => - { - this.OneWayBindStrict(this.ViewModel, x => x.BackCommand, x => x.BackButton.Command) - .DisposeWith(disposable); - this.OneWayBindStrict(this.ViewModel, x => x.Login, x => x.LoginView.ViewModel) - .DisposeWith(disposable); - this.OneWayBindStrict(this.ViewModel, x => x.Performance, x => x.PerformanceView.ViewModel) - .DisposeWith(disposable); - this.OneWayBindStrict(this.ViewModel, x => x.AuthorFile, x => x.AuthorFilesView.ViewModel) - .DisposeWith(disposable); - this.MiscGalleryView.ViewModel = this.ViewModel; - }); - } + this.OneWayBindStrict(this.ViewModel, x => x.Login, x => x.LoginView.ViewModel) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, x => x.Performance, x => x.PerformanceView.ViewModel) + .DisposeWith(disposable); + + ViewModel.WhenAnyValue(vm => vm.ApiToken) + .Select(token => !string.IsNullOrEmpty(token?.AuthorKey) ? Visibility.Visible : Visibility.Collapsed) + .BindToStrict(this, v => v.FileUploadSettingsView.Visibility) + .DisposeWith(disposable); + + this.FileUploadSettingsView.ViewModel = this.ViewModel; + 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..857ddf07b 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.App.Wpf/Wabbajack.App.Wpf_tdx5cb2h_wpftmp.csproj b/Wabbajack.App.Wpf/Wabbajack.App.Wpf_tdx5cb2h_wpftmp.csproj new file mode 100644 index 000000000..2ca85b2a7 --- /dev/null +++ b/Wabbajack.App.Wpf/Wabbajack.App.Wpf_tdx5cb2h_wpftmp.csproj @@ -0,0 +1,567 @@ + + + Wabbajack + obj\x64\Debug\ + obj\ + C:\Users\tik\source\repos\wabbajack_ui\Wabbajack.App.Wpf\obj\ + <_TargetAssemblyProjectName>Wabbajack.App.Wpf + Wabbajack.App.Wpf + + + + WinExe + net9.0-windows + true + x64 + win-x64 + $(VERSION) + $(VERSION) + $(VERSION) + Copyright © 2019-2024 + An automated ModList installer + true + + true + + + CS8600,CS8601,CS8618,CS8604,CS8632,CS1998 + + + Resources\Icons\wabbajack.ico + + + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + TextTemplatingFileGenerator + VerbRegistration.cs + + + True + True + VerbRegistration.tt + + + + + + NU1701 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..dd4a437a7 100644 --- a/Wabbajack.CLI/VerbRegistration.cs +++ b/Wabbajack.CLI/VerbRegistration.cs @@ -4,60 +4,58 @@ namespace Wabbajack.CLI; using Wabbajack.CLI.Verbs; using Wabbajack.CLI.Builder; -public static class CommandLineBuilderExtensions{ +public static class CommandLineBuilderExtensions +{ -public static void AddCLIVerbs(this IServiceCollection services) { -CommandLineBuilder.RegisterCommand(Compile.Definition, c => ((Compile)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(Decrypt.Definition, c => ((Decrypt)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(DownloadAll.Definition, c => ((DownloadAll)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(DownloadUrl.Definition, c => ((DownloadUrl)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(DumpZipInfo.Definition, c => ((DumpZipInfo)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(Encrypt.Definition, c => ((Encrypt)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(Extract.Definition, c => ((Extract)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(ForceHeal.Definition, c => ((ForceHeal)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(HashFile.Definition, c => ((HashFile)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(HashUrlString.Definition, c => ((HashUrlString)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(IndexNexusMod.Definition, c => ((IndexNexusMod)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(Install.Definition, c => ((Install)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(InstallCompileInstallVerify.Definition, c => ((InstallCompileInstallVerify)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(ListCreationClubContent.Definition, c => ((ListCreationClubContent)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(ListGames.Definition, c => ((ListGames)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(ListModlists.Definition, c => ((ListModlists)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(MegaLogin.Definition, c => ((MegaLogin)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(MirrorFile.Definition, c => ((MirrorFile)c).Run); -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); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(VerifyModlistInstall.Definition, c => ((VerifyModlistInstall)c).Run); -services.AddSingleton(); -CommandLineBuilder.RegisterCommand(VFSIndex.Definition, c => ((VFSIndex)c).Run); -services.AddSingleton(); -} + public static void AddCLIVerbs(this IServiceCollection services) + { + CommandLineBuilder.RegisterCommand(Compile.Definition, c => ((Compile)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(Decrypt.Definition, c => ((Decrypt)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(DownloadAll.Definition, c => ((DownloadAll)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(DownloadUrl.Definition, c => ((DownloadUrl)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(DumpZipInfo.Definition, c => ((DumpZipInfo)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(Encrypt.Definition, c => ((Encrypt)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(Extract.Definition, c => ((Extract)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(ForceHeal.Definition, c => ((ForceHeal)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(HashFile.Definition, c => ((HashFile)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(HashUrlString.Definition, c => ((HashUrlString)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(IndexNexusMod.Definition, c => ((IndexNexusMod)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(Install.Definition, c => ((Install)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(InstallCompileInstallVerify.Definition, c => ((InstallCompileInstallVerify)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(ListCreationClubContent.Definition, c => ((ListCreationClubContent)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(ListGames.Definition, c => ((ListGames)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(ListModlists.Definition, c => ((ListModlists)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(MegaLogin.Definition, c => ((MegaLogin)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(MirrorFile.Definition, c => ((MirrorFile)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(ModlistReport.Definition, c => ((ModlistReport)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(Reset.Definition, c => ((Reset)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(UploadToNexus.Definition, c => ((UploadToNexus)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(ValidateLists.Definition, c => ((ValidateLists)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(VerifyModlistInstall.Definition, c => ((VerifyModlistInstall)c).Run); + services.AddSingleton(); + CommandLineBuilder.RegisterCommand(VFSIndex.Definition, c => ((VFSIndex)c).Run); + services.AddSingleton(); + } } \ No newline at end of file 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/Reset.cs b/Wabbajack.CLI/Verbs/Reset.cs new file mode 100644 index 000000000..8451af2df --- /dev/null +++ b/Wabbajack.CLI/Verbs/Reset.cs @@ -0,0 +1,54 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.CommandLine.NamingConventionBinder; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Wabbajack.CLI.Builder; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; + +namespace Wabbajack.CLI.Verbs; + +public class Reset +{ + private const string WabbajackExecutableName = "Wabbajack.exe"; + private readonly ILogger _logger; + + public Reset(ILogger logger) + { + _logger = logger; + } + + public static VerbDefinition Definition = new VerbDefinition("reset", + "Resets Wabbajack settings, restarts the application if open", new OptionDefinition[] + { + }); + + public async Task Run() + { + Console.WriteLine("Checking if Wabbajack is running..."); + var wabbajackProcess = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(WabbajackExecutableName)).FirstOrDefault(); + string? fileName = wabbajackProcess?.MainModule?.FileName; + if(wabbajackProcess != null) + { + Console.WriteLine("Detected Wabbajack! Killing the process..."); + wabbajackProcess.Kill(); + Thread.Sleep(500); + } + Console.WriteLine("Deleting %localappdata%\\Wabbajack..."); + KnownFolders.WabbajackAppLocal.DeleteDirectory(); + if(fileName != null) + { + Console.WriteLine("Restarting Wabbajack..."); + Process.Start(fileName); + } + Console.WriteLine("Done!"); + return 0; + } +} \ No newline at end of file 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..2b22dbb31 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()); } @@ -223,7 +223,17 @@ public async Task InstallArchives(CancellationToken token) NextStep(Consts.StepInstalling, "Installing files", ModList.Directives.Sum(d => d.Size), x => x.ToFileSizeString()); var grouped = ModList.Directives .OfType() - .Select(a => new {VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a}) + .Select(a => { + try + { + return new { VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a }; + } + catch(Exception) + { + _logger.LogError("Failed to look up file {file} by hash {hash}", a.To.FileName.ToString(), a.Hash.ToString()); + throw; + } + }) .GroupBy(a => a.VF) .ToDictionary(a => a.Key); 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/SystemParameters.cs b/Wabbajack.Installer/SystemParameters.cs index 3f91f494e..7c310afdc 100644 --- a/Wabbajack.Installer/SystemParameters.cs +++ b/Wabbajack.Installer/SystemParameters.cs @@ -13,6 +13,7 @@ public class SystemParameters public long SystemPageSize { get; set; } + public string GpuName { get; set; } public long EnbLEVRAMSize => Math.Min(ToMB(SystemMemorySize) + ToMB(VideoMemorySize), 10240); private static long ToMB(long input) 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/ResourceSettingsManager.cs b/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs index 55c669f13..ec3590c4f 100644 --- a/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs +++ b/Wabbajack.Services.OSIntegrated/ResourceSettingsManager.cs @@ -8,7 +8,7 @@ namespace Wabbajack.Services.OSIntegrated; public class ResourceSettingsManager { private readonly SettingsManager _manager; - private Dictionary? _settings; + private Dictionary? _settings = null; public ResourceSettingsManager(SettingsManager manager) { @@ -17,9 +17,8 @@ public ResourceSettingsManager(SettingsManager manager) private SemaphoreSlim _lock = new(1); - public async Task GetSettings(string name) + public async Task GetSetting(string name) { - await _lock.WaitAsync(); try { @@ -43,7 +42,26 @@ public async Task GetSettings(string name) { _lock.Release(); } + } + public async Task SetSetting(string name, ResourceSetting setting) + { + await _lock.WaitAsync(); + try + { + _settings ??= await _manager.Load>("resource_settings"); + _settings[name] = setting; + await SaveSettings(_settings); + } + finally + { + _lock.Release(); + } + } + public async Task> GetSettings() + { + _settings ??= await _manager.Load>("resource_settings"); + return _settings; } public class ResourceSetting diff --git a/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs b/Wabbajack.Services.OSIntegrated/ServiceExtensions.cs index 572588125..5a943408b 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; @@ -105,7 +104,7 @@ public static IServiceCollection AddOSIntegrated(this IServiceCollection service { return async () => { - var s = await provider.GetService()!.GetSettings(name); + var s = await provider.GetService()!.GetSetting(name); return ((int) s.MaxTasks, s.MaxThroughput); }; } @@ -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..e1569e092 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,12 +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}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wabbajack.Compression.Zip", "Wabbajack.Compression.Zip\Wabbajack.Compression.Zip.csproj", "{10165025-D30B-44B7-A764-50E15603AE56}" @@ -345,14 +339,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 +426,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}