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/Branding/WabbajackGraphics/Emotes/wabbajack-small-alt.svg b/Branding/WabbajackGraphics/Emotes/wabbajack-small-alt.svg new file mode 100644 index 000000000..eb8714ac8 --- /dev/null +++ b/Branding/WabbajackGraphics/Emotes/wabbajack-small-alt.svg @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Branding/WabbajackGraphics/Emotes/wabbajack-small.svg b/Branding/WabbajackGraphics/Emotes/wabbajack-small.svg index f93d95c41..9721f516f 100644 --- a/Branding/WabbajackGraphics/Emotes/wabbajack-small.svg +++ b/Branding/WabbajackGraphics/Emotes/wabbajack-small.svg @@ -1,2580 +1,446 @@ - + - - - - - - - - -]> - - - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - - + + - - + + - - + + - - - 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 - - + diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs index b796692de..f14a0ad78 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -1,8 +1,16 @@ using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.Diagnostics; +using System.IO; using System.Reactive.Concurrency; using System.Reactive.Disposables; +using System.Reflection; using System.Runtime.InteropServices; +using System.Security.AccessControl; using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Threading; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +19,7 @@ using Microsoft.Web.WebView2.Wpf; using NLog.Extensions.Logging; using NLog.Targets; +using Octokit; using Orc.FileAssociation; using ReactiveUI; using Wabbajack.CLI.Builder; @@ -26,176 +35,283 @@ 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 void OnStartup(object sender, StartupEventArgs e) { - private IHost _host; + 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) + { + 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(); + + 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; - private void OnStartup(object sender, StartupEventArgs e) + RxApp.MainThreadScheduler.Schedule(0, (_, _) => { - if (IsAdmin()) + if (args.Length == 1) { - var messageBox = MessageBox.Show("Don't run Wabbajack as Admin!", "Error", MessageBoxButton.OK, MessageBoxImage.Error, MessageBoxResult.OK, MessageBoxOptions.DefaultDesktopOnly); - if (messageBox == MessageBoxResult.OK) + var arg = args[0].ToAbsolutePath(); + if (arg.FileExists() && arg.Extension == Ext.Wabbajack) { - Environment.Exit(1); + OpenUI(); + return Disposable.Empty; } - else + } else if (args.Length > 0) + { + var builder = _host.Services.GetRequiredService(); + builder.Run(e.Args).ContinueWith(async x => { - Environment.Exit(1); - } + Environment.Exit(await x); + }); + return Disposable.Empty; + } + else + { + OpenUI(); + return Disposable.Empty; } - RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher); - _host = Host.CreateDefaultBuilder(Array.Empty()) - .ConfigureLogging(AddLogging) - .ConfigureServices((host, services) => - { - ConfigureServices(services); - }) - .Build(); - - var args = e.Args; + return Disposable.Empty; + }); + } - RxApp.MainThreadScheduler.Schedule(0, (_, _) => + private void OpenUI() + { + MainWindow mainWindow = null; + try + { + mainWindow = _host.Services.GetRequiredService(); + } + catch (Exception ex) + { + if (ex is SQLiteException sqlException) { - if (args.Length == 1) + // Attempt to clear read-only flag off Wabbajack directory + if (sqlException.ResultCode == SQLiteErrorCode.CantOpen) { - var arg = args[0].ToAbsolutePath(); - if (arg.FileExists() && arg.Extension == Ext.Wabbajack) + // First MessageBox in App.OnStartup does not trigger: https://github.com/dotnet/wpf/issues/10067 + MessageBox.Show(""); + var result = MessageBox.Show($"Wabbajack cannot read or write to settings files inside %localappdata%/Wabbajack! Let Wabbajack adjust permissions?", "Failed to start Wabbajack", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.Yes) { - var mainWindow = _host.Services.GetRequiredService(); - mainWindow!.Show(); - return Disposable.Empty; + GrantFullControlOverDir(KnownFolders.WabbajackAppLocal); + Restart(); } - } 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; } + } - return Disposable.Empty; - }); + if (mainWindow == null) + { + MessageBox.Show($"Wabbajack failed to start! Full exception: {ex}", "Failed to start Wabbajack", MessageBoxButton.OK, MessageBoxImage.Error); + throw; + } } + mainWindow!.Show(); + } - private static bool IsAdmin() + private static void Restart() + { + try { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; - - 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() { - 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); + FileName = "wabbajack-cli.exe", + Arguments = "restart", + CreateNoWindow = true + }); + } + catch (Exception ex) + { + } + } - } - catch (Exception) + private bool GrantFullControlOverDir(AbsolutePath path) + { + try + { + if (path.DirectoryExists()) { - return false; + var dir = new DirectoryInfo(path.ToString()); + var dirSecurity = dir.GetAccessControl(); + AuthorizationRuleCollection rules = dirSecurity.GetAccessRules(true, true, typeof(NTAccount)); + foreach (FileSystemAccessRule rule in rules) + { + if (rule.AccessControlType != AccessControlType.Deny) continue; + + dirSecurity.RemoveAccessRule(rule); + dirSecurity.AddAccessRule(new FileSystemAccessRule(rule.IdentityReference, FileSystemRights.FullControl, InheritanceFlags.ObjectInherit | InheritanceFlags.ContainerInherit, PropagationFlags.None, AccessControlType.Allow)); + dir.SetAccessControl(dirSecurity); + } + return true; } + return false; } - - private void AddLogging(ILoggingBuilder loggingBuilder) + catch (Exception) { - var config = new NLog.Config.LoggingConfiguration(); + return false; + } + } - var logFolder = KnownFolders.LauncherAwarePath.Combine("logs"); - if (!logFolder.DirectoryExists()) - logFolder.CreateDirectory(); + protected override void OnExit(ExitEventArgs e) + { + base.OnExit(e); + } - 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} ############" - }; + private static bool IsAdmin() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return false; - var consoleTarget = new ConsoleTarget("console"); + try + { + var identity = WindowsIdentity.GetCurrent(); + var owner = identity.Owner; + if (owner is not null) return owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); - var uiTarget = new LogStream - { - Name = "ui", - Layout = "${message:withexception=false}", - }; + var principle = new WindowsPrincipal(identity); + return principle.IsInRole(WindowsBuiltInRole.Administrator); - loggingBuilder.Services.AddSingleton(uiTarget); + } + catch (Exception) + { + return false; + } + } - config.AddRuleForAllLevels(fileTarget); - config.AddRuleForAllLevels(consoleTarget); - config.AddRuleForAllLevels(uiTarget); + private void AddLogging(ILoggingBuilder loggingBuilder) + { + var config = new NLog.Config.LoggingConfiguration(); - loggingBuilder.ClearProviders(); - loggingBuilder.SetMinimumLevel(LogLevel.Information); - loggingBuilder.AddNLog(config); - } + var logFolder = KnownFolders.LauncherAwarePath.Combine("logs"); + if (!logFolder.DirectoryExists()) + logFolder.CreateDirectory(); - private static IServiceCollection ConfigureServices(IServiceCollection services) + var fileTarget = new FileTarget("file") { - 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; - } + 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 consoleTarget = new ConsoleTarget("console"); + + var uiTarget = new LogStream + { + Name = "ui", + Layout = "${message:withexception=false}", + }; + + loggingBuilder.Services.AddSingleton(uiTarget); + + config.AddRuleForAllLevels(fileTarget); + config.AddRuleForAllLevels(consoleTarget); + config.AddRuleForAllLevels(uiTarget); + + 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(); + services.AddSingleton(); + services.AddSingleton(s => new GitHubClient(new ProductHeaderValue("wabbajack"))); + + 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(); + 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(); + 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/Icons/mega.png b/Wabbajack.App.Wpf/LoginManagers/Icons/mega.png new file mode 100644 index 000000000..0bade3957 Binary files /dev/null and b/Wabbajack.App.Wpf/LoginManagers/Icons/mega.png differ diff --git a/Wabbajack.App.Wpf/LoginManagers/Icons/nexus.png b/Wabbajack.App.Wpf/LoginManagers/Icons/nexus.png deleted file mode 100644 index 1d2616cc1..000000000 Binary files a/Wabbajack.App.Wpf/LoginManagers/Icons/nexus.png and /dev/null differ 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/MegaLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/MegaLoginManager.cs new file mode 100644 index 000000000..c9c59f0de --- /dev/null +++ b/Wabbajack.App.Wpf/LoginManagers/MegaLoginManager.cs @@ -0,0 +1,102 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Downloaders; +using Wabbajack.Downloaders.ModDB; +using Wabbajack.DTOs.Logins; +using Wabbajack.Messages; +using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.UserIntervention; +using static CG.Web.MegaApiClient.MegaApiClient; + +namespace Wabbajack.LoginManagers; + +public class MegaLoginManager : ViewModel, ILoginFor +{ + private readonly ILogger _logger; + private readonly ITokenProvider _token; + private readonly IServiceProvider _serviceProvider; + + public string SiteName { get; } = "MEGA"; + public ICommand TriggerLogin { get; set; } + public ICommand ClearLogin { get; set; } + public ICommand ToggleLogin { get; set; } + + public ImageSource Icon { get; set; } + public Type LoginFor() + { + return typeof(MegaDownloader); + } + + [Reactive] + public bool LoggedIn { get; set; } + + public MegaLoginManager(ILogger logger, ITokenProvider token, IServiceProvider serviceProvider) + { + _logger = logger; + _token = token; + _serviceProvider = serviceProvider; + + ClearLogin = ReactiveCommand.CreateFromTask(async () => + { + _logger.LogInformation("Deleting Login information for {SiteName}", SiteName); + await ClearLoginToken(); + }, this.WhenAnyValue(v => v.LoggedIn)); + + Icon = BitmapFrame.Create( + typeof(LoversLabLoginManager).Assembly.GetManifestResourceStream("Wabbajack.App.Wpf.LoginManagers.Icons.mega.png")!); + + TriggerLogin = ReactiveCommand.CreateFromTask(async () => + { + _logger.LogInformation("Logging into {SiteName}", SiteName); + StartLogin(); + }, this.WhenAnyValue(v => v.LoggedIn).Select(v => !v)); + + ToggleLogin = ReactiveCommand.Create(() => + { + if (LoggedIn) ClearLogin.Execute(null); + else TriggerLogin.Execute(null); + }); + + MessageBus.Current.Listen() + .Subscribe(async (loggedIntoMega) => await UpdateToken(loggedIntoMega.Login)) + .DisposeWith(CompositeDisposable); + + LoggedIn = _token.HaveToken(); + } + + private async Task ClearLoginToken() + { + await _token.Delete(); + LoggedIn = _token.HaveToken(); + } + + private void StartLogin() + { + ShowFloatingWindow.Send(FloatingScreenType.MegaLogin); + } + + private async Task UpdateToken(AuthInfos login) + { + MegaToken token = null; + try + { + await _token.SetToken(new MegaToken() { Login = login }); + } + catch(Exception ex) + { + _logger.LogError("Failed to refresh Mega token state: {ex}", ex.ToString()); + } + + 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/LoggedIntoMega.cs b/Wabbajack.App.Wpf/Messages/LoggedIntoMega.cs new file mode 100644 index 000000000..cc4de1ebd --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/LoggedIntoMega.cs @@ -0,0 +1,17 @@ +using ReactiveUI; +using static CG.Web.MegaApiClient.MegaApiClient; + +namespace Wabbajack.Messages; + +public class LoggedIntoMega +{ + public AuthInfos Login { get; set; } + public LoggedIntoMega(AuthInfos login) + { + Login = login; + } + public static void Send(AuthInfos login) + { + MessageBus.Current.SendMessage(new LoggedIntoMega(login)); + } +} \ 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/ReloadCompiledModLists.cs b/Wabbajack.App.Wpf/Messages/ReloadCompiledModLists.cs new file mode 100644 index 000000000..585cd8851 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ReloadCompiledModLists.cs @@ -0,0 +1,16 @@ +using ReactiveUI; +using Wabbajack.DTOs; + +namespace Wabbajack.Messages; + +public class ReloadCompiledModLists +{ + public ReloadCompiledModLists() + { + } + + public static void Send() + { + MessageBus.Current.SendMessage(new ReloadCompiledModLists()); + } +} \ No newline at end of file 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..080a16c76 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/ShowFloatingWindow.cs @@ -0,0 +1,27 @@ +using ReactiveUI; + +namespace Wabbajack.Messages; + +public enum FloatingScreenType +{ + None, + ModListDetails, + FileUpload, + MegaLogin +} + +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/Icons/wabbajack.ico b/Wabbajack.App.Wpf/Resources/Icons/wabbajack.ico index 8fa8e7b03..66c1c1c40 100644 Binary files a/Wabbajack.App.Wpf/Resources/Icons/wabbajack.ico and b/Wabbajack.App.Wpf/Resources/Icons/wabbajack.ico 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..b3132de57 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 +1853,7 @@ - + + + + - - + + @@ -1327,39 +1890,158 @@ + - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -1889,14 +2580,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/Interfaces/INeedsLoginCredentials.cs b/Wabbajack.App.Wpf/View Models/Interfaces/INeedsLoginCredentials.cs deleted file mode 100644 index c4aa7307e..000000000 --- a/Wabbajack.App.Wpf/View Models/Interfaces/INeedsLoginCredentials.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Wabbajack; - -public interface INeedsLoginCredentials -{ - -} \ No newline at end of file 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..bf68d106f --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompiledModListTileVM.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Microsoft.Extensions.Logging; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.Compiler; +using Wabbajack.Messages; +using Wabbajack.Models; +using Wabbajack.Paths; +using Wabbajack.Services.OSIntegrated; + +namespace Wabbajack; + +public class CompiledModListTileVM +{ + private ILogger _logger; + private SettingsManager _settingsManager; + public LoadingLock LoadingImageLock { get; } = new(); + public ICommand CompileModListCommand { get; } + public ICommand DeleteModListCommand { get; } + [Reactive] public CompilerSettings CompilerSettings { get; set; } + [Reactive] public bool Deleted { get; set; } + + public CompiledModListTileVM(ILogger logger, SettingsManager settingsManager, CompilerSettings compilerSettings) + { + _logger = logger; + _settingsManager = settingsManager; + CompilerSettings = compilerSettings; + CompileModListCommand = ReactiveCommand.Create(CompileModList); + DeleteModListCommand = ReactiveCommand.Create(DeleteModList); + } + + private async Task DeleteModList() + { + var savedCompilerSettingsPaths = await _settingsManager.Load>(Consts.AllSavedCompilerSettingsPaths); + if (savedCompilerSettingsPaths.RemoveAll(path => path.Parent == CompilerSettings.Source) > 0) + { + await _settingsManager.Save(Consts.AllSavedCompilerSettingsPaths, savedCompilerSettingsPaths); + ReloadCompiledModLists.Send(); + return true; + } + return false; + } + + 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..d576a87e4 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerFileManagerVM.cs @@ -0,0 +1,181 @@ +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) + { + try + { + var directories = dir.EnumerateDirectories(); + var items = dir.EnumerateFiles(); + return directories.OrderBy(x => x.Name).Concat(items.OrderBy(y => y.Name)); + } + catch(Exception ex) + { + _logger.LogError("While loading compiler settings path for directory {dir}: {ex}", dir.FullName, ex.ToString()); + throw; + } + } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs new file mode 100644 index 000000000..b2fd9660d --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Compiler/CompilerHomeVM.cs @@ -0,0 +1,128 @@ +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 DynamicData.Binding; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Wabbajack.CLI.Verbs; +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; + + MessageBus.Current.Listen() + .Subscribe(m => LoadAllCompilerSettings().FireAndForget()) + .DisposeWith(CompositeDisposable); + + 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, _settingsManager, 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..eb9d82aed --- /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, ICanGetHelpVM, 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 GetHelpCommand { 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(); + + GetHelpCommand = 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.LogError(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/ViewModels/ContributorVM.cs b/Wabbajack.App.Wpf/ViewModels/ContributorVM.cs new file mode 100644 index 000000000..64d814d5d --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/ContributorVM.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reactive.Disposables; +using System.Reflection; +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.Downloaders; +using Wabbajack.DTOs.Logins; +using Wabbajack.LoginManagers; +using Wabbajack.Messages; +using Wabbajack.Networking.GitHub; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Services.OSIntegrated.TokenProviders; +using Wabbajack.Util; +using System.Reactive.Linq; + +namespace Wabbajack; + +public class ContributorVM : ViewModel +{ + private readonly ILogger _logger; + private readonly HttpClient _httpClient; + private readonly ImageCacheManager _icm; + private readonly Client _client; + + [Reactive] public Octokit.RepositoryContributor Contributor { get; set; } + protected ObservableAsPropertyHelper _Avatar { get; set; } + public BitmapImage Avatar => _Avatar.Value; + [Reactive] public ICommand OpenProfileCommand { get; private set; } + + public ContributorVM(ILogger logger, HttpClient httpClient, Octokit.RepositoryContributor contributor, ImageCacheManager icm) + { + _logger = logger; + _httpClient = httpClient; + _icm = icm; + Contributor = contributor; + + OpenProfileCommand = ReactiveCommand.Create(OpenProfile); + + var avatarObservable = Observable.Return(Contributor.AvatarUrl) + .ObserveOn(RxApp.TaskpoolScheduler) + .DownloadBitmapImage(ex => _logger.LogWarning(ex, "Could not load contributor image for user {Name}", Contributor.Login), LoadingLock, _httpClient, _icm) + .Replay(1) + .RefCount(TimeSpan.FromMilliseconds(5000)); + + _Avatar = avatarObservable + .ToGuiProperty(this, nameof(Avatar)) + .DisposeWith(CompositeDisposable); + + this.WhenActivated(async disposables => + { + Disposable.Empty.DisposeWith(disposables); + }); + } + + private void OpenProfile() + { + UIUtils.OpenWebsite(Contributor.HtmlUrl); + } +} 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..b7344de09 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Gallery/ModListGalleryVM.cs @@ -0,0 +1,405 @@ +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 Humanizer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.WindowsAPICodePack.Dialogs; +using ReactiveMarbles.ObservableEvents; +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.Paths.IO; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Services.OSIntegrated.Services; + +namespace Wabbajack; +public class ModListGalleryVM : BackNavigatingVM, ICanLoadLocalFileVM +{ + 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; } + + public Dictionary CommonlyWrongFormattedTags { get; set; } = new(); + [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 FilePickerVM LocalFilePicker { get; set; } + public ICommand LoadLocalFileCommand { 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; + + LocalFilePicker = new FilePickerVM(this); + LocalFilePicker.ExistCheckOption = FilePickerVM.CheckOptions.On; + LocalFilePicker.PathType = FilePickerVM.PathTypeOptions.File; + LocalFilePicker.Filters.AddRange(new[] + { + new CommonFileDialogFilter("Wabbajack Modlist", "*" + Ext.Wabbajack), + }); + + ResetFiltersCommand = ReactiveCommand.Create(() => { + OnlyInstalled = false; + IncludeNSFW = false; + IncludeUnofficial = false; + Search = string.Empty; + SelectedGameTypeEntry = GameTypeEntries?.FirstOrDefault(); + HasTags = new ObservableCollection(); + HasMods = new ObservableCollection(); + }); + + LoadLocalFileCommand = ReactiveCommand.Create(() => + { + LocalFilePicker.ConstructTypicalPickerCommand().Execute(null); + if (LocalFilePicker.TargetPath.FileExists()) + { + LoadModlistForInstalling.Send(LocalFilePicker.TargetPath, null); + NavigateToGlobal.Send(ScreenType.Installer); + } + }); + + 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; + LoadGameTypeEntries(); + 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(); + var tagMappings = await _wjClient.LoadTagMappings(); + + 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); + foreach (var modlist in modLists) + { + var modlistTags = new List(); + foreach(var tag in modlist.Tags) + { + string? allowedTag = null; + tagMappings.TryGetValue(tag, out allowedTag); + + if (allowedTags.TryGetValue(tag, out allowedTag)) + modlistTags.Add(allowedTag); + } + if (modlist.NSFW) modlistTags.Insert(0, "NSFW"); + if (modlist.Official) modlistTags.Insert(0, "Featured"); + + modlist.Tags = modlistTags; + } + + var httpClient = _serviceProvider.GetRequiredService(); + var cacheManager = _serviceProvider.GetRequiredService(); + _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 void LoadGameTypeEntries() + { + GameTypeEntries = new(ModLists.Select(m => m.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..7ecf3d3c4 --- /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, ICanGetHelpVM +{ + 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)); + GetHelpCommand = 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 GetHelpCommand { 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..fb83d2f42 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Installers/InstallationVM.cs @@ -0,0 +1,827 @@ +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.IsNullOrEmpty(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, vm => vm.ModlistMetadata, (isNotLoading, metadata) => isNotLoading && !string.IsNullOrEmpty(metadata?.NamespacedName))); + + 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 {metadata?.Title ?? ModList.Name}"; + 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 next to file"); + } + } + else + { + _logger.LogInformation("Modlist metadata not loaded, possibly using local install without metadata 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; + + case Mega mega: + writer.Write($"

MEGA: {archive.Name}

"); + writer.Write($"

Please click here to download this file, then manually place it inside the Wabbajack downloads directory.

"); + 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/ICanGetHelpVM.cs b/Wabbajack.App.Wpf/ViewModels/Interfaces/ICanGetHelpVM.cs new file mode 100644 index 000000000..a18d03c2b --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Interfaces/ICanGetHelpVM.cs @@ -0,0 +1,8 @@ +using System.Windows.Input; + +namespace Wabbajack; + +public interface ICanGetHelpVM +{ + public ICommand GetHelpCommand { get; } +} diff --git a/Wabbajack.App.Wpf/ViewModels/Interfaces/ICanLoadLocalFileVM.cs b/Wabbajack.App.Wpf/ViewModels/Interfaces/ICanLoadLocalFileVM.cs new file mode 100644 index 000000000..874bc6397 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Interfaces/ICanLoadLocalFileVM.cs @@ -0,0 +1,8 @@ +using System.Windows.Input; + +namespace Wabbajack; + +public interface ICanLoadLocalFileVM +{ + public ICommand LoadLocalFileCommand { get; } +} 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/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/Interventions/MegaLoginVM.cs b/Wabbajack.App.Wpf/ViewModels/Interventions/MegaLoginVM.cs new file mode 100644 index 000000000..05fc91238 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Interventions/MegaLoginVM.cs @@ -0,0 +1,54 @@ +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 CG.Web.MegaApiClient; +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; +using static CG.Web.MegaApiClient.MegaApiClient; + +namespace Wabbajack; + +public class MegaLoginVM : ViewModel +{ + + private readonly ILogger _logger; + private readonly MegaTokenProvider _tokenProvider; + private readonly MegaApiClient _apiClient; + + public ICommand CloseCommand { get; } + + [Reactive] public double UploadProgress { get; set; } + [Reactive] public string FileUrl { get; set; } + public FilePickerVM Picker { get;} + + [Reactive] public string Email { get; set; } + [Reactive] public string Password { get; set; } + [Reactive] public AuthInfos Login { get; private set; } + + public MegaLoginVM(ILogger logger, MegaTokenProvider tokenProvider, Client wjClient, SettingsVM vm, MegaApiClient apiClient) + { + _logger = logger; + _tokenProvider = tokenProvider; + _apiClient = apiClient; + CloseCommand = ReactiveCommand.Create(async () => + { + ShowFloatingWindow.Send(FloatingScreenType.None); + var login = await _apiClient.GenerateAuthInfosAsync(Email, Password); + // Clearing unencrypted data out of memory + Email = ""; + Password = ""; + LoggedIntoMega.Send(login); + }); + } + +} diff --git a/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs b/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs new file mode 100644 index 000000000..d7ec97344 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/MainWindowVM.cs @@ -0,0 +1,425 @@ +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 MegaLoginVM MegaLoginVM; + 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 GetHelpCommand { get; } + public ICommand LoadLocalFileCommand { 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, MegaLoginVM megaLoginVM, 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; + MegaLoginVM = megaLoginVM; + 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)) + { + LoadModlist(path); + } + 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}"); + }); + GetHelpCommand = ReactiveCommand.Create(GetHelp); + LoadLocalFileCommand = ReactiveCommand.Create(LoadLocalFile); + MinimizeCommand = ReactiveCommand.Create(Minimize); + MaximizeCommand = ReactiveCommand.Create(ToggleMaximized); + CloseCommand = ReactiveCommand.Create(Close); + } + + public void LoadModlist(AbsolutePath path) + { + LoadModlistForInstalling.Send(path, null); + NavigateToGlobal.Send(ScreenType.Installer); + } + + private void GetHelp() + { + if (ActivePane is ICanGetHelpVM) ((ICanGetHelpVM)ActivePane).GetHelpCommand.Execute(null); + } + + private void LoadLocalFile() + { + if (ActivePane is ICanLoadLocalFileVM) ((ICanLoadLocalFileVM)ActivePane).LoadLocalFileCommand.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, + FloatingScreenType.MegaLogin => MegaLoginVM, + _ => 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/AboutVM.cs b/Wabbajack.App.Wpf/ViewModels/Settings/AboutVM.cs new file mode 100644 index 000000000..34e2345bc --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Settings/AboutVM.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reactive.Disposables; +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.Networking.GitHub; +using Wabbajack.RateLimiter; +using Wabbajack.Services.OSIntegrated; +using Wabbajack.Services.OSIntegrated.TokenProviders; +using Wabbajack.Util; + +namespace Wabbajack; + +public class AboutVM : ViewModel +{ + private readonly ILogger _logger; + private readonly Client _client; + private readonly IServiceProvider _provider; + + [Reactive] public ObservableCollection Contributors { get; private set; } + public AboutVM(ILogger logger, Client client, IServiceProvider provider) + { + _logger = logger; + _client = client; + _provider = provider; + + this.WhenActivated(async disposables => + { + Task.Run(LoadContributors).FireAndForget(); + + Disposable.Empty.DisposeWith(disposables); + }); + } + + private async Task LoadContributors() + { + try + { + var contributors = await _client.GetWabbajackContributors(); + if (contributors != null) + { + var contributorVMs = contributors + .Where(c => !c.Type.Equals("Bot", StringComparison.OrdinalIgnoreCase)) + .Select(c => + { + return new ContributorVM(_provider.GetRequiredService>(), _provider.GetRequiredService(), c, _provider.GetRequiredService()); + }) + .Take(5); // Sorry! Not enough space for everyone :( + Contributors = new ObservableCollection(contributorVMs); + } + } + catch (Exception ex) + { + _logger.LogError("Failed to get Wabbajack GitHub contributors: {ex}", ex.ToString()); + } + } +} 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..983011fe2 --- /dev/null +++ b/Wabbajack.App.Wpf/ViewModels/Settings/SettingsVM.cs @@ -0,0 +1,109 @@ +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 LoginVM { get; } + public PerformanceSettingsVM PerformanceVM { get; } + public AboutVM AboutVM { 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}")); + }); + }); + + LoginVM = new LoginManagerVM(provider.GetRequiredService>(), this, + provider.GetRequiredService>()); + LaunchCLICommand = ReactiveCommand.CreateFromTask(LaunchCLI); + ResetCommand = ReactiveCommand.Create(Reset); + OpenFileUploadCommand = ReactiveCommand.Create(OpenFileUpload); + PerformanceVM = new PerformanceSettingsVM( + provider.GetRequiredService>(), + provider.GetRequiredService(), + provider.GetRequiredService()); + AboutVM = 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..8cbf87b10 --- /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["TransparentButtonStyle"], + 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..fe4c8e68a --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..93c02310b --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Compiler/CompiledModListTileView.xaml.cs @@ -0,0 +1,41 @@ +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); + + this.BindCommand(ViewModel, vm => vm.DeleteModListCommand, v => v.DeleteButton) + .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..ed4325b4c --- /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..9c8913b5a 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs @@ -1,108 +1,314 @@ 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.ModList.Author) + .BindToStrict(this, v => v.DetailImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModList.Author) + .BindToStrict(this, v => v.InstallDetailImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModList.Author) + .BindToStrict(this, v => v.CompletedImage.Author) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModList.Name) + .BindToStrict(this, v => v.DetailImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModList.Name) + .BindToStrict(this, v => v.InstallDetailImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAny(vm => vm.ModList.Name) + .BindToStrict(this, v => v.CompletedImage.Title) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ModList.Version) + .BindToStrict(this, v => v.DetailImage.Version) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ModList.Version) + .BindToStrict(this, v => v.InstallDetailImage.Version) + .DisposeWith(disposables); + + ViewModel.WhenAnyValue(vm => vm.ModList.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); + + /* + // 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(!string.IsNullOrEmpty(ViewModel?.ModList?.Readme)) + 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/Interventions/MegaLoginView.xaml b/Wabbajack.App.Wpf/Views/Interventions/MegaLoginView.xaml new file mode 100644 index 000000000..09e1511d2 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Interventions/MegaLoginView.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Interventions/MegaLoginView.xaml.cs b/Wabbajack.App.Wpf/Views/Interventions/MegaLoginView.xaml.cs new file mode 100644 index 000000000..d6cde23ec --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Interventions/MegaLoginView.xaml.cs @@ -0,0 +1,32 @@ +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 MegaLoginView : ReactiveUserControl +{ + public MegaLoginView() + { + InitializeComponent(); + + this.WhenActivated(disposables => + { + this.BindCommand(ViewModel, vm => vm.CloseCommand, v => v.CloseButton) + .DisposeWith(disposables); + }); + } + + private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) + { + var password = PasswordBox.Password; + ViewModel.Password = password; + } +} + 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..29dafb5c7 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml @@ -7,92 +7,201 @@ 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}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + - - + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Archives + + Readme + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..c653d7906 --- /dev/null +++ b/Wabbajack.App.Wpf/Views/ModListDetailsView.xaml.cs @@ -0,0 +1,145 @@ +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; +using System.Linq; + +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.WhenAnyValue(x => x.ViewModel.MetadataVM.ModListTagList) + .BindToStrict(this, v => v.TagsControl.ItemsSource) + .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/AboutView.xaml b/Wabbajack.App.Wpf/Views/Settings/AboutView.xaml new file mode 100644 index 000000000..ba571ef6a --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Settings/AboutView.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + Wabbajack + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wabbajack.App.Wpf/Views/Settings/AboutView.xaml.cs b/Wabbajack.App.Wpf/Views/Settings/AboutView.xaml.cs new file mode 100644 index 000000000..2e951654d --- /dev/null +++ b/Wabbajack.App.Wpf/Views/Settings/AboutView.xaml.cs @@ -0,0 +1,24 @@ +using System.Windows; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using ReactiveUI; + +namespace Wabbajack; + +/// +/// Interaction logic for AboutView.xaml +/// +public partial class AboutView : ReactiveUserControl +{ + public AboutView() + { + InitializeComponent(); + + this.WhenActivated(disposable => + { + ViewModel.WhenAnyValue(vm => vm.Contributors) + .BindToStrict(this, v => v.ContributorsControl.ItemsSource) + .DisposeWith(disposable); + }); + } +} 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..890a96d01 100644 --- a/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml +++ b/Wabbajack.App.Wpf/Views/Settings/LoginItemView.xaml @@ -10,31 +10,17 @@ 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..d98c8df7c 100644 --- a/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Settings/SettingsView.xaml.cs @@ -1,43 +1,34 @@ -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.LoginVM, x => x.LoginView.ViewModel) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, x => x.PerformanceVM, x => x.PerformanceView.ViewModel) + .DisposeWith(disposable); + this.OneWayBindStrict(this.ViewModel, x => x.AboutVM, x => x.AboutView.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..c22c61755 100644 --- a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj +++ b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj @@ -48,8 +48,16 @@ - + + + + + + + + + @@ -59,8 +67,14 @@ - - + + + + Always + + + Always + TextTemplatingFileGenerator VerbRegistration.cs @@ -73,44 +87,55 @@ - - + + NU1701 - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + - - - - + + + + + + - - - - - + + + + + + + + + + + Never + @@ -120,6 +145,7 @@ + @@ -128,9 +154,4 @@ - - - - - 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..3cf1fb6a6 100644 --- a/Wabbajack.CLI/Program.cs +++ b/Wabbajack.CLI/Program.cs @@ -19,6 +19,7 @@ using Wabbajack.VFS; using Client = Wabbajack.Networking.GitHub.Client; using Wabbajack.CLI.Builder; +using CG.Web.MegaApiClient; namespace Wabbajack.CLI; @@ -42,6 +43,7 @@ private static async Task Main(string[] args) services.AddSingleton(); services.AddSingleton(s => new GitHubClient(new ProductHeaderValue("wabbajack"))); services.AddSingleton(); + services.AddSingleton(); services.AddOSIntegrated(); services.AddServerLib(); @@ -51,11 +53,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..acb4ea83e 100644 --- a/Wabbajack.CLI/VerbRegistration.cs +++ b/Wabbajack.CLI/VerbRegistration.cs @@ -4,60 +4,60 @@ 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(Restart.Definition, c => ((Restart)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/MegaLogin.cs b/Wabbajack.CLI/Verbs/MegaLogin.cs index bb1164f8c..2ad42b767 100644 --- a/Wabbajack.CLI/Verbs/MegaLogin.cs +++ b/Wabbajack.CLI/Verbs/MegaLogin.cs @@ -1,8 +1,10 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using CG.Web.MegaApiClient; using Microsoft.Extensions.Logging; using Wabbajack.CLI.Builder; +using Wabbajack.Downloaders; using Wabbajack.Downloaders.ModDB; using Wabbajack.Networking.Http.Interfaces; @@ -11,11 +13,13 @@ namespace Wabbajack.CLI.Verbs; public class MegaLogin { private readonly ILogger _logger; + private readonly MegaApiClient _apiClient; - public MegaLogin(ILogger logger, ITokenProvider tokenProvider) + public MegaLogin(ILogger logger, ITokenProvider tokenProvider, MegaApiClient apiClient) { _logger = logger; _tokenProvider = tokenProvider; + _apiClient = apiClient; } public static VerbDefinition Definition = new VerbDefinition("mega-login", @@ -32,8 +36,7 @@ public async Task Run(string email, string password) _logger.LogInformation("Logging into Mega"); await _tokenProvider.SetToken(new MegaToken { - Email = email, - Password = password + Login = _apiClient.GenerateAuthInfos(email, password) }); return 0; } 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/Restart.cs b/Wabbajack.CLI/Verbs/Restart.cs new file mode 100644 index 000000000..8f8a55165 --- /dev/null +++ b/Wabbajack.CLI/Verbs/Restart.cs @@ -0,0 +1,53 @@ +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 Restart +{ + private const string WabbajackExecutableName = "Wabbajack.exe"; + private readonly ILogger _logger; + + public Restart(ILogger logger) + { + _logger = logger; + } + + public static VerbDefinition Definition = new VerbDefinition("restart", + "Forces the main application to restart when opened", 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); + } + + 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/CompilationSteps/DeconstructBSAs.cs b/Wabbajack.Compiler/CompilationSteps/DeconstructBSAs.cs index 6c8da7980..ccbeface0 100644 --- a/Wabbajack.Compiler/CompilationSteps/DeconstructBSAs.cs +++ b/Wabbajack.Compiler/CompilationSteps/DeconstructBSAs.cs @@ -45,26 +45,46 @@ public DeconstructBSAs(ACompiler compiler) : base(compiler) // Cache these so their internal caches aren't recreated on every use _directMatch = new DirectMatch(_mo2Compiler); - _matchSimilar = new MatchSimilarTextures(_mo2Compiler); + if(compiler.Settings.UseTextureRecompression) + _matchSimilar = new MatchSimilarTextures(_mo2Compiler); _includePatches = new IncludePatches(_mo2Compiler); _dropAll = new DropAll(_mo2Compiler); _includeAll = new IncludeAll(_mo2Compiler); - _microstack = bsa => new List + if (compiler.Settings.UseTextureRecompression) { - _directMatch, - _matchSimilar, - _includePatches.WithBSA(bsa), - _dropAll - }; + _microstack = bsa => new List + { + _directMatch, + _matchSimilar, + _includePatches.WithBSA(bsa), + _dropAll + }; - _microstackWithInclude = bsa => new List + _microstackWithInclude = bsa => new List + { + _directMatch, + _matchSimilar, + _includePatches.WithBSA(bsa), + _includeAll + }; + } + else { - _directMatch, - _matchSimilar, - _includePatches.WithBSA(bsa), - _includeAll - }; + _microstack = bsa => new List + { + _directMatch, + _includePatches.WithBSA(bsa), + _dropAll + }; + + _microstackWithInclude = bsa => new List + { + _directMatch, + _includePatches.WithBSA(bsa), + _includeAll + }; + } } public override async ValueTask Run(RawSourceFile source) 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..d6d72f692 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), @@ -322,7 +323,14 @@ public override IEnumerable MakeStack() }; if (!_settings.UseTextureRecompression) + { + _logger.LogInformation("Texture recompression disabled! Removing MatchSimiliarTextures from the compilation stack."); steps = steps.Where(s => s is not MatchSimilarTextures).ToList(); + } + else + { + _logger.LogInformation("Texture recompression enabled!"); + } return steps.Where(s => !s.Disabled); } 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..e61e37716 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/811cf46d61c9ae564bf7fa4b5abc639b.png" } }, { @@ -582,7 +612,8 @@ 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(), + IconSource = "https://cdn2.steamgriddb.com/icon/d9b47f916e531ac9ef2b0887ca72d698.png" } }, { @@ -600,7 +631,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 +649,8 @@ public static class GameRegistry { @"Starfield.exe".ToRelativePath() }, - MainExecutable = @"Starfield.exe".ToRelativePath() + MainExecutable = @"Starfield.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/1a495bc86abe171f690e27192ea6c367.png" } }, { @@ -633,7 +667,8 @@ public static class GameRegistry @"7DaysToDie.exe".ToRelativePath(), @"7dLauncher.exe".ToRelativePath(), }, - MainExecutable = @"7dLauncher.exe".ToRelativePath() + MainExecutable = @"7DaysToDie.exe".ToRelativePath(), + IconSource = "https://cdn2.steamgriddb.com/icon/2b1f462a660e29c47acdcc25cb14d321.png", } }, { diff --git a/Wabbajack.DTOs/JsonConverters/DTOSerializer.cs b/Wabbajack.DTOs/JsonConverters/DTOSerializer.cs index 7ebc882f0..22b5b6ca7 100644 --- a/Wabbajack.DTOs/JsonConverters/DTOSerializer.cs +++ b/Wabbajack.DTOs/JsonConverters/DTOSerializer.cs @@ -15,6 +15,8 @@ public DTOSerializer(IEnumerable converters) { Options = new JsonSerializerOptions(); Options.NumberHandling = JsonNumberHandling.AllowReadingFromString; + Options.ReadCommentHandling = JsonCommentHandling.Skip; + Options.AllowTrailingCommas = true; foreach (var c in converters) Options.Converters.Add(c); } 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..deedc5171 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/MegaDownloader.cs b/Wabbajack.Downloaders.Mega/MegaDownloader.cs index d0d34d40a..fbe657381 100644 --- a/Wabbajack.Downloaders.Mega/MegaDownloader.cs +++ b/Wabbajack.Downloaders.Mega/MegaDownloader.cs @@ -17,7 +17,7 @@ using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; -namespace Wabbajack.Downloaders.ModDB; +namespace Wabbajack.Downloaders; public class MegaDownloader : ADownloader, IUrlDownloader, IProxyable { @@ -79,13 +79,16 @@ private async Task LoginIfNotLoggedIn() if (_tokenProvider.HaveToken()) { var authInfo = await _tokenProvider.Get(); - _logger.LogInformation("Logging into Mega with {Email}", authInfo!.Email); - await _apiClient.LoginAsync(authInfo!.Email, authInfo.Password); + _logger.LogInformation("Logging into Mega"); + await _apiClient.LoginAsync(authInfo!.Login); } else { + _logger.LogWarning("This modlist requires MEGA downloads but the user is not signed in. MEGA downloads will fail!"); + /* _logger.LogInformation("Logging into Mega without credentials"); await _apiClient.LoginAsync(); + */ } } } diff --git a/Wabbajack.Downloaders.Mega/MegaToken.cs b/Wabbajack.Downloaders.Mega/MegaToken.cs index 1f14fe422..d9ad96b94 100644 --- a/Wabbajack.Downloaders.Mega/MegaToken.cs +++ b/Wabbajack.Downloaders.Mega/MegaToken.cs @@ -1,12 +1,10 @@ using System.Text.Json.Serialization; +using static CG.Web.MegaApiClient.MegaApiClient; -namespace Wabbajack.Downloaders.ModDB; +namespace Wabbajack.Downloaders; public class MegaToken { - [JsonPropertyName("email")] - public string Email { get; set; } - - [JsonPropertyName("password")] - public string Password { get; set; } + [JsonPropertyName("login")] + public required AuthInfos Login { get; set; } } \ No newline at end of file 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..e08d1205b 100644 --- a/Wabbajack.Downloaders.WabbajackCDN/WabbajackCDNDownloader.cs +++ b/Wabbajack.Downloaders.WabbajackCDN/WabbajackCDNDownloader.cs @@ -92,11 +92,6 @@ public override async Task Download(Archive archive, WabbajackCDN state, A if (!response.IsSuccessStatusCode) throw new InvalidDataException($"Bad response for part request for part {part.Index}"); - var length = response.Content.Headers.ContentLength; - if (length != part.Size) - throw new InvalidDataException( - $"Bad part size, expected {part.Size} got {length} for part {part.Index}"); - await using var data = await response.Content.ReadAsStreamAsync(token); var ms = new MemoryStream(); @@ -126,6 +121,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; @@ -175,11 +171,6 @@ public async Task GetPart(WabbajackCDN state, PartDefinition part, Cance if (!response.IsSuccessStatusCode) throw new InvalidDataException($"Bad response for part request for part {part.Index}"); - var length = response.Content.Headers.ContentLength; - if (length != part.Size) - throw new InvalidDataException( - $"Bad part size, expected {part.Size} got {length} for part {part.Index}"); - return await response.Content.ReadAsByteArrayAsync(token); } 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/Client.cs b/Wabbajack.Networking.GitHub/Client.cs index 47dff8281..34cf3a204 100644 --- a/Wabbajack.Networking.GitHub/Client.cs +++ b/Wabbajack.Networking.GitHub/Client.cs @@ -89,4 +89,9 @@ public async Task PutData(string owner, string repo, string path, string message { await _client.Repository.Content.UpdateFile(owner, repo, path, new UpdateFileRequest(message, content, oldSha)); } + + public async Task> GetWabbajackContributors() + { + return await _client.Repository.GetAllContributors("wabbajack-tools", "wabbajack"); + } } \ No newline at end of file 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..f41ea55dc 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; @@ -134,6 +135,7 @@ public async Task> LoadUpgradedArchives() public async Task GetGameArchives(Game game, string version) { var url = $"https://raw.githubusercontent.com/wabbajack-tools/indexed-game-files/master/{game}/{version}.json"; + _logger.LogInformation("Fetching game archives for {game} from {url}", game.ToString(), url); return await _client.GetFromJsonAsync(url, _dtos.Options) ?? Array.Empty(); } @@ -171,10 +173,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 +233,19 @@ 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 {repo} from {url} - Exception: {ex}", url.Key, url.Value, ex.ToString()); + return Enumerable.Empty(); + } + catch(Exception ex) + { + _logger.LogError(ex, "Failed loading lists from repository {repo}: {url} - Exception: {ex}", url.Key, url.Value, ex.ToString()); return Enumerable.Empty(); } }) @@ -263,12 +270,29 @@ 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/master/allowed_tags.json"), + _dtos.Options); + return data!.ToHashSet(StringComparer.OrdinalIgnoreCase); + } + public async Task> LoadTagMappings() + { + var data = await _client.GetFromJsonAsync>(_limiter, + new HttpRequestMessage(HttpMethod.Get, + "https://raw.githubusercontent.com/wabbajack-tools/mod-lists/master/tag_mappings.json"), + _dtos.Options); + return data!; + } + 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/MegaTokenProvider.cs b/Wabbajack.Services.OSIntegrated/TokenProviders/MegaTokenProvider.cs index 2eee5f3ea..441702108 100644 --- a/Wabbajack.Services.OSIntegrated/TokenProviders/MegaTokenProvider.cs +++ b/Wabbajack.Services.OSIntegrated/TokenProviders/MegaTokenProvider.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.Logging; -using Wabbajack.Downloaders.ModDB; +using Wabbajack.Downloaders; using Wabbajack.DTOs.JsonConverters; namespace Wabbajack.Services.OSIntegrated.TokenProviders; 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} diff --git a/buildall.bat b/buildall.bat index bfa8c7db7..258e89b93 100644 --- a/buildall.bat +++ b/buildall.bat @@ -10,7 +10,7 @@ dotnet publish Wabbajack.App.Wpf\Wabbajack.App.Wpf.csproj --framework "net9.0-wi dotnet publish Wabbajack.Launcher\Wabbajack.Launcher.csproj --framework "net9.0-windows" --runtime win-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\launcher /p:PublishSingleFile=true /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true --self-contained /p:DebugType=embedded dotnet publish Wabbajack.CLI\Wabbajack.CLI.csproj --framework "net9.0" --runtime win-x64 --configuration Release /p:Platform=x64 -o c:\tmp\publish-wj\app\cli /p:IncludeNativeLibrariesForSelfExtract=true --self-contained /p:DebugType=embedded -cd C:\tmp\CodeSignTool-v1.2.0-windows\ +cd C:\tmp\CodeSignTool-v1.3.2-windows\ call CodeSignTool.bat sign -input_file_path c:\tmp\publish-wj\app\Wabbajack.exe -username=%CODE_SIGN_USER% -password=%CODE_SIGN_PASS% call CodeSignTool.bat sign -input_file_path c:\tmp\publish-wj\launcher\Wabbajack.exe -username=%CODE_SIGN_USER% -password=%CODE_SIGN_PASS% call CodeSignTool.bat sign -input_file_path c:\tmp\publish-wj\app\cli\wabbajack-cli.exe -username=%CODE_SIGN_USER% -password=%CODE_SIGN_PASS%