diff --git a/Branding/PNGs/Wabba_Mouth.png b/Branding/PNGs/Wabba_Mouth.png new file mode 100644 index 00000000..f2d2615b Binary files /dev/null and b/Branding/PNGs/Wabba_Mouth.png differ diff --git a/Branding/Project files/Wabba_Mouth.psd b/Branding/Project files/Wabba_Mouth.psd new file mode 100644 index 00000000..c068e5b7 Binary files /dev/null and b/Branding/Project files/Wabba_Mouth.psd differ diff --git a/Compression.BSA.Test/Compression.BSA.Test.csproj b/Compression.BSA.Test/Compression.BSA.Test.csproj index 9dd108ef..83abb030 100644 --- a/Compression.BSA.Test/Compression.BSA.Test.csproj +++ b/Compression.BSA.Test/Compression.BSA.Test.csproj @@ -75,12 +75,6 @@ true - - ..\packages\SharpZipLib.1.2.0\lib\net45\ICSharpCode.SharpZipLib.dll - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - @@ -96,7 +90,6 @@ - @@ -104,5 +97,13 @@ Compression.BSA + + + 12.0.2 + + + 1.2.0 + + \ No newline at end of file diff --git a/Compression.BSA.Test/packages.config b/Compression.BSA.Test/packages.config deleted file mode 100644 index 5db2ddea..00000000 --- a/Compression.BSA.Test/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Compression.BSA/Compression.BSA.csproj b/Compression.BSA/Compression.BSA.csproj index bf481c6a..a6b44cc6 100644 --- a/Compression.BSA/Compression.BSA.csproj +++ b/Compression.BSA/Compression.BSA.csproj @@ -76,37 +76,10 @@ MinimumRecommendedRules.ruleset - - ..\packages\AlphaFS.2.2.6\lib\net452\AlphaFS.dll - - - ..\packages\SharpZipLib.1.2.0\lib\net45\ICSharpCode.SharpZipLib.dll - - - ..\packages\K4os.Compression.LZ4.1.1.11\lib\net46\K4os.Compression.LZ4.dll - - - ..\packages\K4os.Compression.LZ4.Streams.1.1.11\lib\net46\K4os.Compression.LZ4.Streams.dll - - - ..\packages\K4os.Hash.xxHash.1.0.6\lib\net46\K4os.Hash.xxHash.dll - - - ..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll - - - ..\packages\System.Memory.4.5.3\lib\netstandard2.0\System.Memory.dll - - - ..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - @@ -127,7 +100,15 @@ - + + 2.2.6 + + + 1.1.11 + + + 1.2.0 + \ No newline at end of file diff --git a/Compression.BSA/packages.config b/Compression.BSA/packages.config deleted file mode 100644 index 4d29cdb4..00000000 --- a/Compression.BSA/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/VirtualFileSystem.Test/App.config b/VirtualFileSystem.Test/App.config index bd059d69..656cce25 100644 --- a/VirtualFileSystem.Test/App.config +++ b/VirtualFileSystem.Test/App.config @@ -8,7 +8,7 @@ - + diff --git a/VirtualFileSystem/VirtualFileSystem.csproj b/VirtualFileSystem/VirtualFileSystem.csproj index 2e6b2c31..b365484b 100644 --- a/VirtualFileSystem/VirtualFileSystem.csproj +++ b/VirtualFileSystem/VirtualFileSystem.csproj @@ -70,30 +70,9 @@ MinimumRecommendedRules.ruleset - - ..\packages\AlphaFS.2.2.6\lib\net452\AlphaFS.dll - - - ..\packages\Ceras.4.1.7\lib\net47\Ceras.dll - - - ..\packages\SharpZipLib.1.2.0\lib\net45\ICSharpCode.SharpZipLib.dll - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll - - - ..\packages\System.Collections.Immutable.1.5.0\lib\netstandard2.0\System.Collections.Immutable.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - @@ -118,7 +97,23 @@ - + + + + 2.2.6 + + + 4.1.7 + + + 12.0.2 + + + 1.2.0 + + + 1.5.0 + \ No newline at end of file diff --git a/VirtualFileSystem/app.config b/VirtualFileSystem/app.config index f6e7dca8..64d479d3 100644 --- a/VirtualFileSystem/app.config +++ b/VirtualFileSystem/app.config @@ -4,7 +4,7 @@ - + diff --git a/VirtualFileSystem/packages.config b/VirtualFileSystem/packages.config deleted file mode 100644 index 1191c694..00000000 --- a/VirtualFileSystem/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/Wabbajack.Common/Wabbajack.Common.csproj b/Wabbajack.Common/Wabbajack.Common.csproj index 081af695..7281a77e 100644 --- a/Wabbajack.Common/Wabbajack.Common.csproj +++ b/Wabbajack.Common/Wabbajack.Common.csproj @@ -70,56 +70,11 @@ MinimumRecommendedRules.ruleset - - ..\packages\AlphaFS.2.2.6\lib\net452\AlphaFS.dll - - - ..\packages\Ceras.4.1.7\lib\net47\Ceras.dll - - - ..\packages\erri120.OMODFramework.1.0.0\lib\net472\erri120.OMODFramework.dll - - - ..\packages\SharpZipLib.1.2.0\lib\net45\ICSharpCode.SharpZipLib.dll - - - ..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll - - - ..\packages\murmurhash.1.0.3\lib\net45\MurmurHash.dll - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\Newtonsoft.Json.Bson.1.0.2\lib\net45\Newtonsoft.Json.Bson.dll - - - ..\packages\protobuf-net.2.4.0\lib\net40\protobuf-net.dll - - - ..\packages\SevenZip.19.0.0\lib\net20\SevenZip.dll - - - ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll - - - ..\packages\System.Data.HashFunction.Core.2.0.0\lib\net45\System.Data.HashFunction.Core.dll - - - ..\packages\System.Data.HashFunction.Interfaces.2.0.0\lib\net45\System.Data.HashFunction.Interfaces.dll - - - ..\packages\System.Data.HashFunction.xxHash.2.0.0\lib\net45\System.Data.HashFunction.xxHash.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - @@ -128,9 +83,6 @@ - - ..\packages\YamlDotNet.8.0.0\lib\net45\YamlDotNet.dll - @@ -149,7 +101,6 @@ - @@ -161,5 +112,37 @@ + + + 2.2.6 + + + 4.1.7 + + + 1.0.0 + + + 2.5.2 + + + 1.0.3 + + + 12.0.2 + + + 1.0.2 + + + 2.4.0 + + + 2.0.0 + + + 8.0.0 + + \ No newline at end of file diff --git a/Wabbajack.Common/packages.config b/Wabbajack.Common/packages.config deleted file mode 100644 index 8ab18089..00000000 --- a/Wabbajack.Common/packages.config +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Wabbajack.Lib/Extensions/TaskExt.cs b/Wabbajack.Lib/Extensions/TaskExt.cs new file mode 100644 index 00000000..a4053c23 --- /dev/null +++ b/Wabbajack.Lib/Extensions/TaskExt.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Wabbajack +{ + public static class TaskExt + { + public static async void FireAndForget(this Task task, Action onException = null) + { + try + { + await task.ConfigureAwait(false); + } + catch (Exception ex) + when (onException != null) + { + onException(ex); + } + } + } +} diff --git a/Wabbajack.Lib/UI/UIUtils.cs b/Wabbajack.Lib/UI/UIUtils.cs index ee7ad62e..13471acb 100644 --- a/Wabbajack.Lib/UI/UIUtils.cs +++ b/Wabbajack.Lib/UI/UIUtils.cs @@ -62,15 +62,36 @@ namespace Wabbajack.Lib return null; } - public static BitmapImage BitmapImageFromResource(string name) + public static BitmapImage BitmapImageFromResource(string name) => BitmapImageFromStream(Utils.GetResourceStream(name)); + + public static BitmapImage BitmapImageFromStream(Stream stream) { var img = new BitmapImage(); img.BeginInit(); - img.StreamSource = Utils.GetResourceStream(name); + img.StreamSource = stream; img.EndInit(); return img; } + public static bool TryGetBitmapImageFromFile(string path, out BitmapImage bitmapImage) + { + try + { + if (!File.Exists(path)) + { + bitmapImage = default; + return false; + } + bitmapImage = new BitmapImage(new Uri(path, UriKind.RelativeOrAbsolute)); + return true; + } + catch (Exception) + { + bitmapImage = default; + return false; + } + } + public static string OpenFileDialog(string filter) { OpenFileDialog ofd = new OpenFileDialog(); diff --git a/Wabbajack.Lib/Wabbajack.Lib.csproj b/Wabbajack.Lib/Wabbajack.Lib.csproj index 9c3461e2..6b0795d4 100644 --- a/Wabbajack.Lib/Wabbajack.Lib.csproj +++ b/Wabbajack.Lib/Wabbajack.Lib.csproj @@ -31,74 +31,15 @@ 4 - - ..\packages\AlphaFS.2.2.6\lib\net452\AlphaFS.dll - - - ..\packages\Ceras.4.1.7\lib\net47\Ceras.dll - - - ..\packages\CommonMark.NET.0.15.1\lib\net45\CommonMark.dll - - - ..\packages\DynamicData.6.13.18\lib\net461\DynamicData.dll - - - ..\packages\MegaApiClient.1.7.1\lib\net46\MegaApiClient.dll - - - ..\packages\Microsoft.Toolkit.Wpf.UI.Controls.WebView.5.1.1\lib\net462\Microsoft.Toolkit.Wpf.UI.Controls.WebView.dll - - - ..\packages\Microsoft-WindowsAPICodePack-Core.1.1.3.3\lib\net452\Microsoft.WindowsAPICodePack.dll - - - ..\packages\Microsoft-WindowsAPICodePack-Shell.1.1.3.3\lib\net452\Microsoft.WindowsAPICodePack.Shell.dll - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\ReactiveUI.10.5.7\lib\net461\ReactiveUI.dll - - - ..\packages\SharpCompress.0.23.0\lib\net45\SharpCompress.dll - - - ..\packages\Splat.9.1.1\lib\net461\Splat.dll - - - ..\packages\Splat.Drawing.9.1.1\lib\net461\Splat.Drawing.dll - - - ..\packages\System.Buffers.4.5.0\lib\netstandard2.0\System.Buffers.dll - - - ..\packages\System.Drawing.Primitives.4.3.0\lib\net45\System.Drawing.Primitives.dll - True - True - - - ..\packages\System.Reactive.4.2.0\lib\net46\System.Reactive.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Threading.Tasks.Extensions.4.5.3\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - - ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - @@ -109,13 +50,7 @@ - - ..\packages\WebSocketSharpFork.1.0.4.0\lib\net35\websocket-sharp.dll - - - ..\packages\YamlDotNet.8.0.0\lib\net45\YamlDotNet.dll - @@ -160,6 +95,7 @@ + @@ -182,7 +118,6 @@ - @@ -204,7 +139,44 @@ - + + + 2.2.6 + + + 4.1.7 + + + 0.15.1 + + + 1.7.1 + + + 1.1.3.3 + + + 5.1.1 + + + 12.0.2 + + + 10.5.7 + + + 0.23.0 + + + 4.2.0 + + + 1.0.4 + + + 8.0.0 + + MSBuild:Compile diff --git a/Wabbajack.Lib/packages.config b/Wabbajack.Lib/packages.config deleted file mode 100644 index 8b9d706b..00000000 --- a/Wabbajack.Lib/packages.config +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Wabbajack.Test/Wabbajack.Test.csproj b/Wabbajack.Test/Wabbajack.Test.csproj index 4a117a55..6079c766 100644 --- a/Wabbajack.Test/Wabbajack.Test.csproj +++ b/Wabbajack.Test/Wabbajack.Test.csproj @@ -1,6 +1,5 @@  - Debug @@ -78,55 +77,14 @@ MinimumRecommendedRules.ruleset - - ..\packages\AlphaFS.2.2.6\lib\net452\AlphaFS.dll - - - ..\packages\DynamicData.6.13.18\lib\net461\DynamicData.dll - - - ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - - - ..\packages\MSTest.TestFramework.1.3.2\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll - - - ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll - - - ..\packages\ReactiveUI.10.5.7\lib\net461\ReactiveUI.dll - - - ..\packages\Splat.9.1.1\lib\net461\Splat.dll - - - ..\packages\Splat.Drawing.9.1.1\lib\net461\Splat.Drawing.dll - - - ..\packages\System.Drawing.Primitives.4.3.0\lib\net45\System.Drawing.Primitives.dll - True - True - - - ..\packages\System.Reactive.4.2.0\lib\net46\System.Reactive.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.2\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - ..\packages\System.Threading.Tasks.Extensions.4.5.3\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - - ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll - @@ -149,7 +107,6 @@ - @@ -169,14 +126,26 @@ Wabbajack + + + 2.2.6 + + + 1.3.2 + + + 1.3.2 + + + 12.0.2 + + + 10.5.7 + + + 4.2.0 + + - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - \ No newline at end of file diff --git a/Wabbajack.Test/app.config b/Wabbajack.Test/app.config index 3e3e46ab..6af99795 100644 --- a/Wabbajack.Test/app.config +++ b/Wabbajack.Test/app.config @@ -12,7 +12,7 @@ - + diff --git a/Wabbajack.Test/packages.config b/Wabbajack.Test/packages.config deleted file mode 100644 index ce27ac3f..00000000 --- a/Wabbajack.Test/packages.config +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Wabbajack/App.xaml b/Wabbajack/App.xaml index fa8f6a15..310e335e 100644 --- a/Wabbajack/App.xaml +++ b/Wabbajack/App.xaml @@ -6,6 +6,9 @@ + + + diff --git a/Wabbajack/App.xaml.cs b/Wabbajack/App.xaml.cs index 62d33cdc..7717a08e 100644 --- a/Wabbajack/App.xaml.cs +++ b/Wabbajack/App.xaml.cs @@ -13,22 +13,14 @@ namespace Wabbajack { public App() { - /* - Utils.Log($"Wabbajack Build - {ThisAssembly.Git.Sha}"); - SetupHandlers(); - - var args = Environment.GetCommandLineArgs(); - if (args.Length > 1) + // Wire any unhandled crashing exceptions to log before exiting + AppDomain.CurrentDomain.UnhandledException += (sender, e) => { - Utils.SetLoggerFn(f => { }); - WorkQueue.Init((a, b, c) => { }, (a, b) => { }); - var updater = new CheckForUpdates(args[1]); - if (updater.FindOutdatedMods()) - { - Environment.Exit(0); - } - Environment.Exit(1); - }*/ + // Don't do any special logging side effects + Utils.SetLoggerFn((s) => { }); + Utils.Log("Uncaught error:"); + Utils.Log(((Exception)e.ExceptionObject).ExceptionToString()); + }; var appPath = Assembly.GetExecutingAssembly().Location; if (!ExtensionManager.IsAssociated() || ExtensionManager.NeedsUpdating(appPath)) @@ -37,22 +29,11 @@ namespace Wabbajack } string[] args = Environment.GetCommandLineArgs(); - StartupUri = new Uri("UI/ModeSelectionWindow.xaml", UriKind.Relative); + StartupUri = new Uri("Views/ModeSelectionWindow.xaml", UriKind.Relative); if (args.Length != 3) return; if (!args[1].Contains("-i")) return; // modlists gets loaded using a shell command - StartupUri = new Uri("UI/MainWindow.xaml", UriKind.Relative); - } - - private void SetupHandlers() - { - AppDomain.CurrentDomain.UnhandledException += AppHandler; - } - - private void AppHandler(object sender, UnhandledExceptionEventArgs e) - { - Utils.Log("Uncaught error:"); - Utils.Log(((Exception)e.ExceptionObject).ExceptionToString()); + StartupUri = new Uri("Views/MainWindow.xaml", UriKind.Relative); } } } \ No newline at end of file diff --git a/Wabbajack/AppState.cs b/Wabbajack/AppState.cs deleted file mode 100644 index ff590923..00000000 --- a/Wabbajack/AppState.cs +++ /dev/null @@ -1,427 +0,0 @@ -using Syroot.Windows.IO; -using System; -using ReactiveUI; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net.Http; -using System.Reactive.Subjects; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reflection; -using System.Threading; -using System.Windows; -using System.Windows.Input; -using System.Windows.Media.Imaging; -using System.Windows.Threading; -using Wabbajack.Common; -using Wabbajack.Lib.Downloaders; -using Wabbajack.Lib.NexusApi; -using Wabbajack.UI; -using DynamicData; -using DynamicData.Binding; -using System.Reactive; -using System.Text; -using Wabbajack.Lib; - -namespace Wabbajack -{ - public class AppState : ViewModel, IDataErrorInfo - { - public SlideShow Slideshow { get; } - - public readonly string WabbajackVersion = FileVersionInfo.GetVersionInfo(Assembly.GetEntryAssembly().Location).FileVersion; - - private string _mo2Folder; - - public readonly BitmapImage _noneImage = UIUtils.BitmapImageFromResource("Wabbajack.UI.none.jpg"); - - private readonly Subject _statusSubject = new Subject(); - public ObservableCollectionExtended Status { get; } = new ObservableCollectionExtended(); - - private ModList _ModList; - public ModList ModList { get => _ModList; private set => this.RaiseAndSetIfChanged(ref _ModList, value); } - - private string _ModListPath; - public string ModListPath { get => _ModListPath; private set => this.RaiseAndSetIfChanged(ref _ModListPath, value); } - - private RunMode _Mode; - public RunMode Mode { get => _Mode; private set => this.RaiseAndSetIfChanged(ref _Mode, value); } - - private string _ModListName; - public string ModListName { get => _ModListName; set => this.RaiseAndSetIfChanged(ref _ModListName, value); } - - private bool _UIReady; - public bool UIReady { get => _UIReady; set => this.RaiseAndSetIfChanged(ref _UIReady, value); } - - private string _HTMLReport; - public string HTMLReport { get => _HTMLReport; set => this.RaiseAndSetIfChanged(ref _HTMLReport, value); } - - private bool _Installing; - public bool Installing { get => _Installing; set => this.RaiseAndSetIfChanged(ref _Installing, value); } - - // Command properties - public IReactiveCommand ChangePathCommand { get; } - public IReactiveCommand ChangeDownloadPathCommand { get; } - public IReactiveCommand BeginCommand { get; } - public IReactiveCommand ShowReportCommand { get; } - public IReactiveCommand OpenReadmeCommand { get; } - public IReactiveCommand OpenModListPropertiesCommand { get; } - - public AppState(RunMode mode) - { - if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower()) - { - MessageBox.Show( - "Wabbajack is running inside your Downloads folder. This folder is often highly monitored by antivirus software and these can often " + - "conflict with the operations Wabbajack needs to perform. Please move this executable outside of your Downloads folder and then restart the app.", - "Cannot run inside Downloads", - MessageBoxButton.OK, - MessageBoxImage.Error); - Environment.Exit(1); - } - - Mode = mode; - - // Define commands - this.ChangePathCommand = ReactiveCommand.Create(ExecuteChangePath); - this.ChangeDownloadPathCommand = ReactiveCommand.Create(ExecuteChangeDownloadPath); - this.ShowReportCommand = ReactiveCommand.Create(ShowReport); - this.OpenModListPropertiesCommand = ReactiveCommand.Create( - execute: OpenModListProperties, - canExecute: this.WhenAny(x => x.UIReady) - .ObserveOnGuiThread()); - this.OpenReadmeCommand = ReactiveCommand.Create( - execute: this.OpenReadmeWindow, - canExecute: this.WhenAny(x => x.ModList) - .Select(modList => !string.IsNullOrEmpty(modList?.Readme)) - .ObserveOnGuiThread()); - this.BeginCommand = ReactiveCommand.Create( - execute: this.ExecuteBegin, - canExecute: this.WhenAny(x => x.UIReady) - .ObserveOnGuiThread()); - - this.Slideshow = new SlideShow(this); - - // Initialize work queue - WorkQueue.Init( - report_function: (id, msg, progress) => this._statusSubject.OnNext(new CPUStatus() { ID = id, Msg = msg, Progress = progress }), - report_queue_size: (max, current) => this.SetQueueSize(max, current)); - // Compile progress updates and populate ObservableCollection - this._statusSubject - .ObserveOn(RxApp.TaskpoolScheduler) - .ToObservableChangeSet(x => x.ID) - .Batch(TimeSpan.FromMilliseconds(250)) - .EnsureUniqueChanges() - .ObserveOn(RxApp.MainThreadScheduler) - .Sort(SortExpressionComparer.Ascending(s => s.ID), SortOptimisations.ComparesImmutableValuesOnly) - .Bind(this.Status) - .Subscribe() - .DisposeWith(this.CompositeDisposable); - } - - public ObservableCollection Log { get; } = new ObservableCollection(); - - private string _Location; - public string Location { get => _Location; set => this.RaiseAndSetIfChanged(ref _Location, value); } - - private string _LocationLabel; - public string LocationLabel { get => _LocationLabel; set => this.RaiseAndSetIfChanged(ref _LocationLabel, value); } - - private string _DownloadLocation; - public string DownloadLocation { get => _DownloadLocation; set => this.RaiseAndSetIfChanged(ref _DownloadLocation, value); } - - private int _queueProgress; - public int QueueProgress { get => _queueProgress; set => this.RaiseAndSetIfChanged(ref _queueProgress, value); } - - public string LogFile { get; } - - private void ExecuteChangePath() - { - switch (this.Mode) - { - case RunMode.Compile: - Location = UIUtils.ShowFolderSelectionDialog("Select Your MO2 profile directory"); - break; - case RunMode.Install: - var folder = UIUtils.ShowFolderSelectionDialog("Select Installation directory"); - if (folder == null) return; - Location = folder; - if (DownloadLocation == null) - { - DownloadLocation = Path.Combine(Location, "downloads"); - } - break; - default: - throw new NotImplementedException(); - } - } - - private void ExecuteChangeDownloadPath() - { - var folder = UIUtils.ShowFolderSelectionDialog("Select a location for MO2 downloads"); - if (folder != null) DownloadLocation = folder; - } - - private void ShowReport() - { - var file = Path.GetTempFileName() + ".html"; - File.WriteAllText(file, HTMLReport); - Process.Start(file); - } - - private ModlistPropertiesWindow modlistPropertiesWindow; - public string newImagePath; - public string readmePath; - public bool ChangedProperties; - private void OpenModListProperties() - { - if (UIReady) - { - if (modlistPropertiesWindow == null) - { - modlistPropertiesWindow = new ModlistPropertiesWindow(this); - newImagePath = null; - ChangedProperties = false; - - } - if(!modlistPropertiesWindow.IsClosed) - modlistPropertiesWindow.Show(); - else - { - modlistPropertiesWindow = null; - OpenModListProperties(); - } - } - } - - private void OpenReadmeWindow() - { - if (string.IsNullOrEmpty(this.ModList.Readme)) return; - using (var fs = new FileStream(this.ModListPath, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (var ar = new ZipArchive(fs, ZipArchiveMode.Read)) - using (var ms = new MemoryStream()) - { - var entry = ar.GetEntry(this.ModList.Readme); - using (var e = entry.Open()) - { - e.CopyTo(ms); - } - ms.Seek(0, SeekOrigin.Begin); - using (var reader = new StreamReader(ms)) - { - var viewer = new TextViewer(reader.ReadToEnd(), this.ModListName); - viewer.Show(); - } - } - } - - public string Error => "Error"; - - public string this[string columnName] => Validate(columnName); - - private string Validate(string columnName) - { - string validationMessage = null; - switch (columnName) - { - case "Location": - if (Location == null) - { - validationMessage = null; - } - else switch (Mode) - { - case RunMode.Compile when Location != null && Directory.Exists(Location) && File.Exists(Path.Combine(Location, "modlist.txt")): - Location = Path.Combine(Location, "modlist.txt"); - validationMessage = null; - ConfigureForBuild(); - break; - case RunMode.Install when Location != null && Directory.Exists(Location) && !Directory.EnumerateFileSystemEntries(Location).Any(): - validationMessage = null; - break; - case RunMode.Install when Location != null && Directory.Exists(Location) && Directory.EnumerateFileSystemEntries(Location).Any(): - validationMessage = "You have selected a non-empty directory. Installing the modlist here might result in a broken install!"; - break; - default: - validationMessage = "Invalid Mod Organizer profile directory"; - break; - } - break; - } - return validationMessage; - } - - public void LogMsg(string msg) - { - Application.Current.Dispatcher.Invoke(() => Log.Add(msg)); - } - - public void SetQueueSize(int max, int current) - { - if (max == 0) - max = 1; - var total = current * 100 / max; - QueueProgress = total; - } - - private void ConfigureForBuild() - { - var profile_folder = Path.GetDirectoryName(Location); - var mo2folder = Path.GetDirectoryName(Path.GetDirectoryName(profile_folder)); - if (!File.Exists(Path.Combine(mo2folder, "ModOrganizer.exe"))) - LogMsg($"Error! No ModOrganizer2.exe found in {mo2folder}"); - - var profile_name = Path.GetFileName(profile_folder); - this.ModListName = profile_name; - this.Mode = RunMode.Compile; - - if (Utils.IsMO2Running(mo2folder)) - { - MessageBox.Show("You need to close MO2 before running Wabbajack!", - "Error", MessageBoxButton.OK); - Environment.Exit(1); - } - - var tmp_compiler = new Compiler(mo2folder); - DownloadLocation = tmp_compiler.MO2DownloadsFolder; - - _mo2Folder = mo2folder; - } - - internal void ConfigureForInstall(string source, ModList modlist) - { - this.ModList = modlist; - this.ModListPath = source; - this.Mode = RunMode.Install; - ModListName = this.ModList.Name; - HTMLReport = this.ModList.ReportHTML; - Location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - - var currentWJVersion = new Version(WabbajackVersion); - var modlistWJVersion = new Version(modlist.WabbajackVersion); - - if (currentWJVersion > modlistWJVersion) - { - MessageBox.Show( - "The selected Modlist was build with an earlier version of Wabbajack. " + - $"Current Version: {WabbajackVersion}, " + - $"Version used to build the Modlist: {modlist.WabbajackVersion}", - "Information", - MessageBoxButton.OK); - } - else if(currentWJVersion < modlistWJVersion) - { - MessageBox.Show( - "The selected Modlist was build with a newer version of Wabbajack. " + - $"Current Version: {WabbajackVersion}, " + - $"Version used to build the Modlist: {modlist.WabbajackVersion}", - "Information", - MessageBoxButton.OK); - } - - this.Slideshow.SlideShowElements = modlist.Archives - .Select(m => m.State) - .OfType() - .Select(m => - new Slide(NexusApiUtils.FixupSummary(m.ModName),m.ModID, - NexusApiUtils.FixupSummary(m.Summary), NexusApiUtils.FixupSummary(m.Author), - m.Adult,m.NexusURL,m.SlideShowPic)).ToList(); - - - this.Slideshow.PreloadSlideShow(); - } - - private void ExecuteBegin() - { - UIReady = false; - if (this.Mode == RunMode.Install) - { - this.Installing = true; - var installer = new Installer(this.ModListPath, this.ModList, Location) - { - DownloadFolder = DownloadLocation - }; - var th = new Thread(() => - { - UIReady = false; - try - { - installer.Install(); - } - catch (Exception ex) - { - while (ex.InnerException != null) ex = ex.InnerException; - LogMsg(ex.StackTrace); - LogMsg(ex.ToString()); - LogMsg($"{ex.Message} - Can't continue"); - } - finally - { - UIReady = true; - this.Installing = false; - } - }) - { - Priority = ThreadPriority.BelowNormal - }; - th.Start(); - } - else if (_mo2Folder != null) - { - var compiler = new Compiler(_mo2Folder) - { - MO2Profile = ModListName, - ModListName = ChangedProperties ? this.Slideshow.ModName : null, - ModListAuthor = ChangedProperties ? this.Slideshow.AuthorName : null, - ModListDescription = ChangedProperties ? this.Slideshow.Summary : null, - ModListImage = ChangedProperties ? newImagePath : null, - ModListWebsite = ChangedProperties ? this.Slideshow.NexusSiteURL : null, - ModListReadme = ChangedProperties ? readmePath : null, - WabbajackVersion = WabbajackVersion - }; - var th = new Thread(() => - { - UIReady = false; - try - { - compiler.Compile(); - if (compiler.ModList != null && compiler.ModList.ReportHTML != null) - HTMLReport = compiler.ModList.ReportHTML; - } - catch (Exception ex) - { - while (ex.InnerException != null) ex = ex.InnerException; - LogMsg(ex.StackTrace); - LogMsg(ex.ToString()); - LogMsg($"{ex.Message} - Can't continue"); - } - finally - { - UIReady = true; - } - }) - { - Priority = ThreadPriority.BelowNormal - }; - th.Start(); - } - else - { - Utils.Log("Cannot compile modlist: no valid Mod Organizer profile directory selected."); - UIReady = true; - } - } - } - - public class CPUStatus - { - public int Progress { get; internal set; } - public string Msg { get; internal set; } - public int ID { get; internal set; } - } -} \ No newline at end of file diff --git a/Wabbajack/Converters/BoolToVisibilityConverter.cs b/Wabbajack/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 00000000..ad00a890 --- /dev/null +++ b/Wabbajack/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,36 @@ +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; + +namespace Wabbajack +{ + [ValueConversion(typeof(Visibility), typeof(bool))] + public class BoolToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (targetType != typeof(Visibility)) + throw new InvalidOperationException($"The target must be of type {nameof(Visibility)}"); + bool compareTo = true; + if (parameter is bool p) + { + compareTo = p; + } + else if (parameter is string str && str.ToUpper().Equals("FALSE")) + { + compareTo = false; + } + return ((bool)value) == compareTo ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/Wabbajack/Themes/LeftMarginMultiplierConverter.cs b/Wabbajack/Converters/LeftMarginMultiplierConverter.cs similarity index 100% rename from Wabbajack/Themes/LeftMarginMultiplierConverter.cs rename to Wabbajack/Converters/LeftMarginMultiplierConverter.cs diff --git a/Wabbajack/Extensions/ReactiveUIExt.cs b/Wabbajack/Extensions/ReactiveUIExt.cs index 9c0fe1df..372794a8 100644 --- a/Wabbajack/Extensions/ReactiveUIExt.cs +++ b/Wabbajack/Extensions/ReactiveUIExt.cs @@ -5,6 +5,7 @@ using System.Reactive; using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading.Tasks; using DynamicData; using DynamicData.Kernel; using ReactiveUI; @@ -151,6 +152,57 @@ namespace Wabbajack }); } + public static IObservable SelectTask(this IObservable source, Func task) + { + return source + .SelectMany(async i => + { + await task(i).ConfigureAwait(false); + return System.Reactive.Unit.Default; + }); + } + + public static IObservable SelectTask(this IObservable source, Func task) + { + return source + .SelectMany(async _ => + { + await task().ConfigureAwait(false); + return System.Reactive.Unit.Default; + }); + } + + public static IObservable SelectTask(this IObservable source, Func> task) + { + return source + .SelectMany(_ => task()); + } + + public static IObservable SelectTask(this IObservable source, Func> task) + { + return source + .SelectMany(x => task(x)); + } + + public static IObservable DoTask(this IObservable source, Func task) + { + return source + .SelectMany(async (x) => + { + await task(x).ConfigureAwait(false); + return x; + }); + } + + public static IObservable WhereCastable(this IObservable source) + where R : class + where T : class + { + return source + .Select(x => x as R) + .NotNull(); + } + /// These snippets were provided by RolandPheasant (author of DynamicData) /// They'll be going into the official library at some point, but are here for now. #region Dynamic Data EnsureUniqueChanges diff --git a/Wabbajack/FodyWeavers.xml b/Wabbajack/FodyWeavers.xml index 36bc693f..26ec15b8 100644 --- a/Wabbajack/FodyWeavers.xml +++ b/Wabbajack/FodyWeavers.xml @@ -1,4 +1,5 @@  + 7z diff --git a/Wabbajack/FodyWeavers.xsd b/Wabbajack/FodyWeavers.xsd index 44a53744..b2d3e296 100644 --- a/Wabbajack/FodyWeavers.xsd +++ b/Wabbajack/FodyWeavers.xsd @@ -4,6 +4,7 @@ + diff --git a/Wabbajack/UI/Icons/discord.png b/Wabbajack/Resources/Icons/discord.png similarity index 100% rename from Wabbajack/UI/Icons/discord.png rename to Wabbajack/Resources/Icons/discord.png diff --git a/Wabbajack/UI/Icons/github.png b/Wabbajack/Resources/Icons/github.png similarity index 100% rename from Wabbajack/UI/Icons/github.png rename to Wabbajack/Resources/Icons/github.png diff --git a/Wabbajack/UI/Icons/github_light.png b/Wabbajack/Resources/Icons/github_light.png similarity index 100% rename from Wabbajack/UI/Icons/github_light.png rename to Wabbajack/Resources/Icons/github_light.png diff --git a/Wabbajack/UI/Icons/next.png b/Wabbajack/Resources/Icons/next.png similarity index 100% rename from Wabbajack/UI/Icons/next.png rename to Wabbajack/Resources/Icons/next.png diff --git a/Wabbajack/UI/Icons/patreon.png b/Wabbajack/Resources/Icons/patreon.png similarity index 100% rename from Wabbajack/UI/Icons/patreon.png rename to Wabbajack/Resources/Icons/patreon.png diff --git a/Wabbajack/UI/Icons/patreon_light.png b/Wabbajack/Resources/Icons/patreon_light.png similarity index 100% rename from Wabbajack/UI/Icons/patreon_light.png rename to Wabbajack/Resources/Icons/patreon_light.png diff --git a/Wabbajack/UI/Icons/wabbajack.ico b/Wabbajack/Resources/Icons/wabbajack.ico similarity index 100% rename from Wabbajack/UI/Icons/wabbajack.ico rename to Wabbajack/Resources/Icons/wabbajack.ico diff --git a/Wabbajack/Resources/Wabba_Mouth.png b/Wabbajack/Resources/Wabba_Mouth.png new file mode 100644 index 00000000..f2d2615b Binary files /dev/null and b/Wabbajack/Resources/Wabba_Mouth.png differ diff --git a/Wabbajack/UI/banner.png b/Wabbajack/Resources/banner.png similarity index 100% rename from Wabbajack/UI/banner.png rename to Wabbajack/Resources/banner.png diff --git a/Wabbajack/UI/banner_dark.png b/Wabbajack/Resources/banner_dark.png similarity index 100% rename from Wabbajack/UI/banner_dark.png rename to Wabbajack/Resources/banner_dark.png diff --git a/Wabbajack/UI/banner_small.png b/Wabbajack/Resources/banner_small.png similarity index 100% rename from Wabbajack/UI/banner_small.png rename to Wabbajack/Resources/banner_small.png diff --git a/Wabbajack/UI/banner_small_dark.png b/Wabbajack/Resources/banner_small_dark.png similarity index 100% rename from Wabbajack/UI/banner_small_dark.png rename to Wabbajack/Resources/banner_small_dark.png diff --git a/Wabbajack/UI/none.jpg b/Wabbajack/Resources/none.jpg similarity index 100% rename from Wabbajack/UI/none.jpg rename to Wabbajack/Resources/none.jpg diff --git a/Wabbajack/Themes/Styles.xaml b/Wabbajack/Themes/Styles.xaml index 317947ac..14e242b2 100644 --- a/Wabbajack/Themes/Styles.xaml +++ b/Wabbajack/Themes/Styles.xaml @@ -7,7 +7,7 @@ mc:Ignorable="d"> - + @@ -22,12 +22,43 @@ #BDBDBD #525252 - - + + #ffc400 + #e83a40 + #52b545 + + #BB86FC + #3700B3 + #03DAC6 + #C7FC86 + #868CFC + #F686FC + #FC86C7 + #FCBB86 + + + + + + + + + + + + + + + + + + + + @@ -807,7 +838,7 @@ - + + - - - - - - - - - - - - - - - - - - - - - - Enable the Slideshow - - - Show NSFW Mods in the Slideshow - - - - - - - diff --git a/Wabbajack/UI/TextViewer.xaml b/Wabbajack/UI/TextViewer.xaml deleted file mode 100644 index 2c56bb7c..00000000 --- a/Wabbajack/UI/TextViewer.xaml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/Wabbajack/AutoScrollBehavior.cs b/Wabbajack/Util/AutoScrollBehavior.cs similarity index 100% rename from Wabbajack/AutoScrollBehavior.cs rename to Wabbajack/Util/AutoScrollBehavior.cs diff --git a/Wabbajack/Util/CPUStatus.cs b/Wabbajack/Util/CPUStatus.cs new file mode 100644 index 00000000..0a218d7a --- /dev/null +++ b/Wabbajack/Util/CPUStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Wabbajack +{ + public class CPUStatus + { + public int Progress { get; internal set; } + public string Msg { get; internal set; } + public int ID { get; internal set; } + } +} diff --git a/Wabbajack/Themes/TreeViewItemExtensions.cs b/Wabbajack/Util/TreeViewItemExtensions.cs similarity index 100% rename from Wabbajack/Themes/TreeViewItemExtensions.cs rename to Wabbajack/Util/TreeViewItemExtensions.cs diff --git a/Wabbajack/View Models/CompilerVM.cs b/Wabbajack/View Models/CompilerVM.cs new file mode 100644 index 00000000..73e79878 --- /dev/null +++ b/Wabbajack/View Models/CompilerVM.cs @@ -0,0 +1,149 @@ +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Media.Imaging; +using Wabbajack.Common; +using Wabbajack.Lib; + +namespace Wabbajack +{ + public class CompilerVM : ViewModel + { + public MainWindowVM MWVM { get; } + + [Reactive] + public string Mo2Folder { get; set; } + + [Reactive] + public string MOProfile { get; set; } + + [Reactive] + public string ModListName { get; set; } + + [Reactive] + public string Location { get; set; } + + [Reactive] + public bool UIReady { get; set; } = true; + + [Reactive] + public string AuthorName { get; set; } + + [Reactive] + public string Summary { get; set; } = "Description (700 characters max)"; + + [Reactive] + public string ImagePath { get; set; } + + private readonly ObservableAsPropertyHelper _Image; + public BitmapImage Image => _Image.Value; + + [Reactive] + public string NexusSiteURL { get; set; } + + [Reactive] + public string ReadMeText { get; set; } + + [Reactive] + public string HTMLReport { get; set; } + + [Reactive] + public string DownloadLocation { get; set; } + + public IReactiveCommand BeginCommand { get; } + + public CompilerVM(MainWindowVM mainWindowVM, string source) + { + this.MWVM = mainWindowVM; + this.Location = source; + + this.BeginCommand = ReactiveCommand.CreateFromTask( + execute: this.ExecuteBegin, + canExecute: this.WhenAny(x => x.UIReady) + .ObserveOnGuiThread()); + + this._Image = this.WhenAny(x => x.ImagePath) + .Select(path => + { + if (string.IsNullOrWhiteSpace(path)) return UIUtils.BitmapImageFromResource("Wabbajack.Resources.Banner_Dark.png"); + if (UIUtils.TryGetBitmapImageFromFile(path, out var image)) + { + return image; + } + return UIUtils.BitmapImageFromResource("Wabbajack.Resources.none.png"); + }) + .ToProperty(this, nameof(this.Image)); + + ConfigureForBuild(source); + } + + private void ConfigureForBuild(string location) + { + var profile_folder = Path.GetDirectoryName(location); + this.Mo2Folder = Path.GetDirectoryName(Path.GetDirectoryName(profile_folder)); + if (!File.Exists(Path.Combine(this.Mo2Folder, "ModOrganizer.exe"))) + { + this.Log().Error($"Error! No ModOrganizer2.exe found in {this.Mo2Folder}"); + } + + this.MOProfile = Path.GetFileName(profile_folder); + this.ModListName = this.MOProfile; + + var tmp_compiler = new Compiler(this.Mo2Folder); + this.DownloadLocation = tmp_compiler.MO2DownloadsFolder; + } + + private async Task ExecuteBegin() + { + if (this.Mo2Folder != null) + { + var compiler = new Compiler(this.Mo2Folder) + { + MO2Profile = this.MOProfile, + ModListName = this.ModListName, + ModListAuthor = this.AuthorName, + ModListDescription = this.Summary, + ModListImage = this.ImagePath, + ModListWebsite = this.NexusSiteURL, + ModListReadme = this.ReadMeText, + }; + await Task.Run(() => + { + UIReady = false; + try + { + compiler.Compile(); + if (compiler.ModList?.ReportHTML != null) + { + this.HTMLReport = compiler.ModList.ReportHTML; + } + } + catch (Exception ex) + { + while (ex.InnerException != null) ex = ex.InnerException; + this.Log().Warn(ex, "Can't continue"); + } + finally + { + UIReady = true; + } + }); + } + else + { + this.Log().Warn("Cannot compile modlist: no valid Mod Organizer profile directory selected."); + UIReady = true; + } + } + } +} diff --git a/Wabbajack/View Models/InstallerVM.cs b/Wabbajack/View Models/InstallerVM.cs new file mode 100644 index 00000000..7da46ad6 --- /dev/null +++ b/Wabbajack/View Models/InstallerVM.cs @@ -0,0 +1,321 @@ +using Syroot.Windows.IO; +using System; +using ReactiveUI; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net.Http; +using System.Reactive.Subjects; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reflection; +using System.Threading; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media.Imaging; +using System.Windows.Threading; +using Wabbajack.Common; +using Wabbajack.Lib.Downloaders; +using Wabbajack.Lib.NexusApi; +using DynamicData; +using DynamicData.Binding; +using System.Reactive; +using System.Text; +using Wabbajack.Lib; +using Splat; +using ReactiveUI.Fody.Helpers; + +namespace Wabbajack +{ + public class InstallerVM : ViewModel + { + public SlideShow Slideshow { get; } + + public MainWindowVM MWVM { get; } + + public BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromResource("Wabbajack.Resources.Wabba_Mouth.png"); + + private readonly ObservableAsPropertyHelper _ModList; + public ModList ModList => _ModList.Value; + + [Reactive] + public string ModListPath { get; set; } + + [Reactive] + public bool UIReady { get; set; } + + private readonly ObservableAsPropertyHelper _HTMLReport; + public string HTMLReport => _HTMLReport.Value; + + /// + /// Tracks whether an install is currently in progress + /// + [Reactive] + public bool Installing { get; set; } + + /// + /// Tracks whether to show the installing pane + /// + [Reactive] + public bool InstallingMode { get; set; } + + [Reactive] + public string Location { get; set; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + + [Reactive] + public string DownloadLocation { get; set; } + + private readonly ObservableAsPropertyHelper _ProgressPercent; + public float ProgressPercent => _ProgressPercent.Value; + + private readonly ObservableAsPropertyHelper _Image; + public BitmapImage Image => _Image.Value; + + private readonly ObservableAsPropertyHelper _TitleText; + public string TitleText => _TitleText.Value; + + private readonly ObservableAsPropertyHelper _AuthorText; + public string AuthorText => _AuthorText.Value; + + private readonly ObservableAsPropertyHelper _Description; + public string Description => _Description.Value; + + // Command properties + public IReactiveCommand BeginCommand { get; } + public IReactiveCommand ShowReportCommand { get; } + public IReactiveCommand OpenReadmeCommand { get; } + public IReactiveCommand VisitWebsiteCommand { get; } + + public InstallerVM(MainWindowVM mainWindowVM) + { + if (Path.GetDirectoryName(Assembly.GetEntryAssembly().Location.ToLower()) == KnownFolders.Downloads.Path.ToLower()) + { + MessageBox.Show( + "Wabbajack is running inside your Downloads folder. This folder is often highly monitored by antivirus software and these can often " + + "conflict with the operations Wabbajack needs to perform. Please move this executable outside of your Downloads folder and then restart the app.", + "Cannot run inside Downloads", + MessageBoxButton.OK, + MessageBoxImage.Error); + Environment.Exit(1); + } + + this.MWVM = mainWindowVM; + + this._ModList = this.WhenAny(x => x.ModListPath) + .ObserveOn(RxApp.TaskpoolScheduler) + .Select(source => + { + if (source == null) return default; + var modlist = Installer.LoadFromFile(source); + if (modlist == null) + { + MessageBox.Show("Invalid Modlist, or file not found.", "Invalid Modlist", MessageBoxButton.OK, + MessageBoxImage.Error); + Application.Current.Dispatcher.Invoke(() => + { + this.MWVM.MainWindow.ExitWhenClosing = false; + var window = new ModeSelectionWindow + { + ShowActivated = true + }; + window.Show(); + this.MWVM.MainWindow.Close(); + }); + return default; + } + return modlist; + }) + .ObserveOnGuiThread() + .StartWith(default(ModList)) + .ToProperty(this, nameof(this.ModList)); + this._HTMLReport = this.WhenAny(x => x.ModList) + .Select(modList => modList?.ReportHTML) + .ToProperty(this, nameof(this.HTMLReport)); + this._ProgressPercent = Observable.CombineLatest( + this.WhenAny(x => x.Installing), + this.WhenAny(x => x.InstallingMode), + resultSelector: (installing, mode) => !installing && mode) + .Select(show => show ? 1f : 0f) + // Disable for now, until more reliable + //this.WhenAny(x => x.MWVM.QueueProgress) + // .Select(i => i / 100f) + .ToProperty(this, nameof(this.ProgressPercent)); + + this.Slideshow = new SlideShow(this); + + // Locate and create modlist image if it exists + var modListImage = Observable.CombineLatest( + this.WhenAny(x => x.ModList), + this.WhenAny(x => x.ModListPath), + (modList, modListPath) => (modList, modListPath)) + .ObserveOn(RxApp.TaskpoolScheduler) + .Select(u => + { + if (u.modList == null + || u.modListPath == null + || !File.Exists(u.modListPath) + || string.IsNullOrEmpty(u.modList.Image) + || u.modList.Image.Length != 36) + { + return WabbajackLogo; + } + try + { + using (var fs = new FileStream(u.modListPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var ar = new ZipArchive(fs, ZipArchiveMode.Read)) + using (var ms = new MemoryStream()) + { + var entry = ar.GetEntry(u.modList.Image); + using (var e = entry.Open()) + e.CopyTo(ms); + var image = new BitmapImage(); + image.BeginInit(); + image.CacheOption = BitmapCacheOption.OnLoad; + image.StreamSource = ms; + image.EndInit(); + image.Freeze(); + + return image; + } + } + catch (Exception ex) + { + this.Log().Warn(ex, "Error loading modlist splash image."); + return WabbajackLogo; + } + }) + .ObserveOnGuiThread() + .StartWith(default(BitmapImage)) + .Replay(1) + .RefCount(); + + // Set display items to modlist if configuring or complete, + // or to the current slideshow data if installing + this._Image = Observable.CombineLatest( + modListImage + .StartWith(default(BitmapImage)), + this.WhenAny(x => x.Slideshow.Image) + .StartWith(default(BitmapImage)), + this.WhenAny(x => x.Installing), + resultSelector: (modList, slideshow, installing) => installing ? slideshow : modList) + .ToProperty(this, nameof(this.Image)); + this._TitleText = Observable.CombineLatest( + this.WhenAny(x => x.ModList.Name), + this.WhenAny(x => x.Slideshow.ModName), + this.WhenAny(x => x.Installing), + resultSelector: (modList, mod, installing) => installing ? mod : modList) + .ToProperty(this, nameof(this.TitleText)); + this._AuthorText = Observable.CombineLatest( + this.WhenAny(x => x.ModList.Author), + this.WhenAny(x => x.Slideshow.AuthorName), + this.WhenAny(x => x.Installing), + resultSelector: (modList, mod, installing) => installing ? mod : modList) + .ToProperty(this, nameof(this.AuthorText)); + this._Description = Observable.CombineLatest( + this.WhenAny(x => x.ModList.Description), + this.WhenAny(x => x.Slideshow.Description), + this.WhenAny(x => x.Installing), + resultSelector: (modList, mod, installing) => installing ? mod : modList) + .ToProperty(this, nameof(this.Description)); + + // Define commands + this.ShowReportCommand = ReactiveCommand.Create(ShowReport); + this.OpenReadmeCommand = ReactiveCommand.Create( + execute: this.OpenReadmeWindow, + canExecute: this.WhenAny(x => x.ModList) + .Select(modList => !string.IsNullOrEmpty(modList?.Readme)) + .ObserveOnGuiThread()); + this.BeginCommand = ReactiveCommand.Create( + execute: this.ExecuteBegin, + canExecute: this.WhenAny(x => x.Installing) + .Select(installing => !installing) + .ObserveOnGuiThread()); + this.VisitWebsiteCommand = ReactiveCommand.Create( + execute: () => Process.Start(this.ModList.Website), + canExecute: this.WhenAny(x => x.ModList.Website) + .Select(x => x?.StartsWith("https://") ?? false) + .ObserveOnGuiThread()); + + // Have Installation location updates modify the downloads location if empty + this.WhenAny(x => x.Location) + .Subscribe(installPath => + { + if (string.IsNullOrWhiteSpace(this.DownloadLocation)) + { + this.DownloadLocation = Path.Combine(installPath, "downloads"); + } + }) + .DisposeWith(this.CompositeDisposable); + } + + private void ShowReport() + { + var file = Path.GetTempFileName() + ".html"; + File.WriteAllText(file, HTMLReport); + Process.Start(file); + } + + private void OpenReadmeWindow() + { + if (string.IsNullOrEmpty(this.ModList.Readme)) return; + using (var fs = new FileStream(this.ModListPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var ar = new ZipArchive(fs, ZipArchiveMode.Read)) + using (var ms = new MemoryStream()) + { + var entry = ar.GetEntry(this.ModList.Readme); + if (entry == null) + { + Utils.Log($"Tried to open a non-existant readme: {this.ModList.Readme}"); + return; + } + using (var e = entry.Open()) + { + e.CopyTo(ms); + } + ms.Seek(0, SeekOrigin.Begin); + using (var reader = new StreamReader(ms)) + { + var viewer = new TextViewer(reader.ReadToEnd(), this.ModList.Name); + viewer.Show(); + } + } + } + + private void ExecuteBegin() + { + this.Installing = true; + this.InstallingMode = true; + var installer = new Installer(this.ModListPath, this.ModList, Location) + { + DownloadFolder = DownloadLocation + }; + var th = new Thread(() => + { + try + { + installer.Install(); + } + catch (Exception ex) + { + while (ex.InnerException != null) ex = ex.InnerException; + Utils.Log(ex.StackTrace); + Utils.Log(ex.ToString()); + Utils.Log($"{ex.Message} - Can't continue"); + } + finally + { + + this.Installing = false; + } + }) + { + Priority = ThreadPriority.BelowNormal + }; + th.Start(); + } + } +} \ No newline at end of file diff --git a/Wabbajack/View Models/MainWindowVM.cs b/Wabbajack/View Models/MainWindowVM.cs new file mode 100644 index 00000000..64993fd1 --- /dev/null +++ b/Wabbajack/View Models/MainWindowVM.cs @@ -0,0 +1,123 @@ +using DynamicData; +using DynamicData.Binding; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using Wabbajack.Common; +using Wabbajack.Lib; + +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; } + + private readonly ObservableAsPropertyHelper _ActivePane; + public ViewModel ActivePane => _ActivePane.Value; + + [Reactive] + public int QueueProgress { get; set; } + + private readonly Subject _statusSubject = new Subject(); + public IObservable StatusObservable => _statusSubject; + public ObservableCollectionExtended StatusList { get; } = new ObservableCollectionExtended(); + + private Subject _logSubj = new Subject(); + public ObservableCollectionExtended Log { get; } = new ObservableCollectionExtended(); + + [Reactive] + public RunMode Mode { get; set; } + + private readonly Lazy _Compiler; + private readonly Lazy _Installer; + + public MainWindowVM(RunMode mode, string source, MainWindow mainWindow) + { + this.Mode = mode; + this.MainWindow = mainWindow; + this._Installer = new Lazy(() => new InstallerVM(this)); + this._Compiler = new Lazy(() => new CompilerVM(this, source)); + + // Set up logging + _logSubj + .ToObservableChangeSet() + .Buffer(TimeSpan.FromMilliseconds(250)) + .Where(l => l.Count > 0) + .FlattenBufferResult() + .Top(5000) + .ObserveOn(RxApp.MainThreadScheduler) + .Bind(this.Log) + .Subscribe() + .DisposeWith(this.CompositeDisposable); + Utils.SetLoggerFn(s => _logSubj.OnNext(s)); + Utils.SetStatusFn((msg, progress) => WorkQueue.Report(msg, progress)); + + // Wire mode to drive the active pane. + // Note: This is currently made into a derivative property driven by mode, + // but it can be easily changed into a normal property that can be set from anywhere if needed + this._ActivePane = this.WhenAny(x => x.Mode) + .Select(m => + { + switch (m) + { + case RunMode.Compile: + return this._Compiler.Value; + case RunMode.Install: + return this._Installer.Value; + default: + return default; + } + }) + .ToProperty(this, nameof(this.ActivePane)); + this.WhenAny(x => x.ActivePane) + .ObserveOn(RxApp.TaskpoolScheduler) + .WhereCastable() + .Subscribe(vm => vm.ModListPath = source) + .DisposeWith(this.CompositeDisposable); + + // Initialize work queue + WorkQueue.Init( + report_function: (id, msg, progress) => this._statusSubject.OnNext(new CPUStatus() { ID = id, Msg = msg, Progress = progress }), + report_queue_size: (max, current) => this.SetQueueSize(max, current)); + + // Compile progress updates and populate ObservableCollection + this._statusSubject + .ObserveOn(RxApp.TaskpoolScheduler) + .ToObservableChangeSet(x => x.ID) + .Batch(TimeSpan.FromMilliseconds(250)) + .EnsureUniqueChanges() + .ObserveOn(RxApp.MainThreadScheduler) + .Sort(SortExpressionComparer.Ascending(s => s.ID), SortOptimisations.ComparesImmutableValuesOnly) + .Bind(this.StatusList) + .Subscribe() + .DisposeWith(this.CompositeDisposable); + } + + private void SetQueueSize(int max, int current) + { + if (max == 0) + max = 1; + QueueProgress = current * 100 / max; + } + + public override void Dispose() + { + base.Dispose(); + Utils.SetLoggerFn(s => { }); + } + } +} diff --git a/Wabbajack/UI/ModListDefinition.cs b/Wabbajack/View Models/ModListDefinition.cs similarity index 100% rename from Wabbajack/UI/ModListDefinition.cs rename to Wabbajack/View Models/ModListDefinition.cs diff --git a/Wabbajack/UI/ModeSelectionWindowViewModel.cs b/Wabbajack/View Models/ModeSelectionWindowVM.cs similarity index 51% rename from Wabbajack/UI/ModeSelectionWindowViewModel.cs rename to Wabbajack/View Models/ModeSelectionWindowVM.cs index 6b86a7bc..c0f3eed4 100644 --- a/Wabbajack/UI/ModeSelectionWindowViewModel.cs +++ b/Wabbajack/View Models/ModeSelectionWindowVM.cs @@ -1,8 +1,11 @@ using Alphaleonis.Win32.Filesystem; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; using Wabbajack.Common; @@ -11,40 +14,21 @@ using Wabbajack.Lib.ModListRegistry; namespace Wabbajack.UI { - public class ModeSelectionWindowViewModel : ViewModel + public class ModeSelectionWindowVM : ViewModel { + public ObservableCollection ModLists { get; } = new ObservableCollection(ModlistMetadata.LoadFromGithub()); + [Reactive] + public ModlistMetadata SelectedModList { get; set; } - public ModeSelectionWindowViewModel() + private readonly ObservableAsPropertyHelper _CanInstall; + public bool CanInstall => _CanInstall.Value; + + public ModeSelectionWindowVM() { - _modLists = new ObservableCollection(ModlistMetadata.LoadFromGithub()); - } - - private ObservableCollection _modLists; - - public ObservableCollection ModLists - { - get => _modLists; - } - - - private ModlistMetadata _selectedModList; - public ModlistMetadata SelectedModList - { - get => _selectedModList; - set - { - CanInstall = true; - RaiseAndSetIfChanged(ref _selectedModList, value); - } - } - - private bool _canInstall; - - public bool CanInstall - { - get => _canInstall; - set => RaiseAndSetIfChanged(ref _canInstall, value); + this._CanInstall = this.WhenAny(x => x.SelectedModList) + .Select(x => x != null) + .ToProperty(this, nameof(this.CanInstall)); } internal string Download() @@ -60,7 +44,6 @@ namespace Wabbajack.UI if (window.Result == DownloadWindow.WindowResult.Completed) return dest; return null; - } } } diff --git a/Wabbajack/UI/SlideShow.cs b/Wabbajack/View Models/SlideShow.cs similarity index 56% rename from Wabbajack/UI/SlideShow.cs rename to Wabbajack/View Models/SlideShow.cs index 7c1393d1..1349c189 100644 --- a/Wabbajack/UI/SlideShow.cs +++ b/Wabbajack/View Models/SlideShow.cs @@ -1,4 +1,6 @@ -using ReactiveUI; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; using System; using System.Collections.Generic; using System.Diagnostics; @@ -13,6 +15,7 @@ using System.Threading.Tasks; using System.Windows.Media.Imaging; using Wabbajack.Common; using Wabbajack.Lib; +using Wabbajack.Lib.Downloaders; using Wabbajack.Lib.NexusApi; namespace Wabbajack @@ -30,45 +33,44 @@ namespace Wabbajack public Queue SlidesQueue { get; } - public AppState AppState { get; } + public InstallerVM Installer { get; } - public BitmapImage NextIcon { get; } = UIUtils.BitmapImageFromResource("Wabbajack.UI.Icons.next.png"); - public BitmapImage WabbajackLogo { get; } = UIUtils.BitmapImageFromResource("Wabbajack.UI.Banner_Dark.png"); + public BitmapImage NextIcon { get; } = UIUtils.BitmapImageFromResource("Wabbajack.Resources.Icons.next.png"); - private bool _ShowNSFW; - public bool ShowNSFW { get => _ShowNSFW; set => this.RaiseAndSetIfChanged(ref _ShowNSFW, value); } + [Reactive] + public bool ShowNSFW { get; set; } - private bool _GCAfterUpdating = true; - public bool GCAfterUpdating { get => _GCAfterUpdating; set => this.RaiseAndSetIfChanged(ref _GCAfterUpdating, value); } + [Reactive] + public bool GCAfterUpdating { get; set; } = true; - private bool _Enable = true; - public bool Enable { get => _Enable; set => this.RaiseAndSetIfChanged(ref _Enable, value); } + [Reactive] + public bool Enable { get; set; } = true; - private BitmapImage _Image; - public BitmapImage Image { get => _Image; set => this.RaiseAndSetIfChanged(ref _Image, value); } + [Reactive] + public BitmapImage Image { get; set; } - private string _ModName = "Wabbajack"; - public string ModName { get => _ModName; set => this.RaiseAndSetIfChanged(ref _ModName, value); } + [Reactive] + public string ModName { get; set; } = "Wabbajack"; - private string _AuthorName = "Halgari & the Wabbajack Team"; - public string AuthorName { get => _AuthorName; set => this.RaiseAndSetIfChanged(ref _AuthorName, value); } + [Reactive] + public string AuthorName { get; set; } = "Halgari & the Wabbajack Team"; - private string _Summary; - public string Summary { get => _Summary; set => this.RaiseAndSetIfChanged(ref _Summary, value); } + [Reactive] + public string Description { get; set; } - private string _NexusSiteURL = "https://github.com/wabbajack-tools/wabbajack"; - public string NexusSiteURL { get => _NexusSiteURL; set => this.RaiseAndSetIfChanged(ref _NexusSiteURL, value); } + [Reactive] + public string NexusSiteURL { get; set; } = "https://github.com/wabbajack-tools/wabbajack"; public IReactiveCommand SlideShowNextItemCommand { get; } = ReactiveCommand.Create(() => { }); public IReactiveCommand VisitNexusSiteCommand { get; } - public SlideShow(AppState appState) + public SlideShow(InstallerVM appState) { SlideShowElements = NexusApiClient.CachedSlideShow.ToList(); CachedSlides = new Dictionary(); SlidesQueue = new Queue(); _random = new Random(); - AppState = appState; + Installer = appState; this.VisitNexusSiteCommand = ReactiveCommand.Create( execute: () => Process.Start(this.NexusSiteURL), @@ -77,91 +79,44 @@ namespace Wabbajack .ObserveOnGuiThread()); // Apply modlist properties when it changes - this.WhenAny(x => x.AppState.ModList) + this.WhenAny(x => x.Installer.ModList) .NotNull() - .Subscribe(modList => + .ObserveOnGuiThread() + .Do(modList => { - this.NexusSiteURL = modList.Website; - this.ModName = modList.Name; - this.AuthorName = modList.Author; - this.Summary = modList.Description; + this.SlideShowElements = modList.Archives + .Select(m => m.State) + .OfType() + .Select(m => + new Slide(NexusApiUtils.FixupSummary(m.ModName), m.ModID, + NexusApiUtils.FixupSummary(m.Summary), NexusApiUtils.FixupSummary(m.Author), + m.Adult, m.NexusURL, m.SlideShowPic)).ToList(); }) - .DisposeWith(this.CompositeDisposable); - - // Update splashscreen when modlist changes - Observable.CombineLatest( - (this).WhenAny(x => x.AppState.ModList), - (this).WhenAny(x => x.AppState.ModListPath), - (this).WhenAny(x => x.Enable), - (modList, modListPath, enabled) => (modList, modListPath, enabled)) - // Do any potential unzipping on a background thread .ObserveOn(RxApp.TaskpoolScheduler) - .Select(u => + .Do(modList => { - if (u.enabled - && u.modList != null - && u.modListPath != null - && File.Exists(u.modListPath) - && !string.IsNullOrEmpty(u.modList.Image) - && u.modList.Image.Length == 36) - { - try - { - using (var fs = new FileStream(u.modListPath, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (var ar = new ZipArchive(fs, ZipArchiveMode.Read)) - using (var ms = new MemoryStream()) - { - var entry = ar.GetEntry(u.modList.Image); - using (var e = entry.Open()) - e.CopyTo(ms); - var image = new BitmapImage(); - image.BeginInit(); - image.CacheOption = BitmapCacheOption.OnLoad; - image.StreamSource = ms; - image.EndInit(); - image.Freeze(); - - return image; - } - } - catch (Exception) - { - this.AppState.LogMsg("Error loading splash image."); - } - } - return this.WabbajackLogo; + // This takes a while, and is currently blocking + this.PreloadSlideShow(); }) - .ObserveOn(RxApp.MainThreadScheduler) - .StartWith(this.WabbajackLogo) - .Subscribe(bitmap => this.Image = bitmap) + .Subscribe() .DisposeWith(this.CompositeDisposable); /// Wire slideshow updates - var intervalSeconds = 10; - // Compile all the sources that trigger a slideshow update + // Merge all the sources that trigger a slideshow update Observable.Merge( - // If user requests one manually - this.SlideShowNextItemCommand.StartingExecution(), // If the natural timer fires - Observable.Merge( - // Start with an initial timer - Observable.Return(Observable.Interval(TimeSpan.FromSeconds(intervalSeconds))), - // but reset timer if user requests one - this.SlideShowNextItemCommand.StartingExecution() - .Select(_ => Observable.Interval(TimeSpan.FromSeconds(intervalSeconds)))) - // When a new timer comes in, swap to it - .Switch() - .Unit()) - // When filter switch enabled, fire an initial signal + Observable.Interval(TimeSpan.FromSeconds(10)) + .Unit() + // Only if enabled + .FilterSwitch(this.WhenAny(x => x.Enable)), + // If user requests one manually + this.SlideShowNextItemCommand.StartingExecution()) + // When installing fire an initial signal .StartWith(Unit.Default) - // Only subscribe to slideshow triggers if enabled and installing - .FilterSwitch( - Observable.CombineLatest( - this.WhenAny(x => x.Enable), - this.WhenAny(x => x.AppState.Installing), - resultSelector: (enabled, installing) => enabled && installing)) - // Don't ever update more than once every half second. - .Debounce(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler) + // Only subscribe to slideshow triggers if installing + .FilterSwitch(this.WhenAny(x => x.Installer.Installing)) + // Don't ever update more than once every half second. ToDo: Update to debounce + .Throttle(TimeSpan.FromMilliseconds(500), RxApp.MainThreadScheduler) .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(_ => this.UpdateSlideShowItem()) .DisposeWith(this.CompositeDisposable); @@ -203,7 +158,7 @@ namespace Wabbajack if (!slide.IsNSFW || (slide.IsNSFW && ShowNSFW)) { - this.Image = AppState._noneImage; + this.Image = UIUtils.BitmapImageFromResource("Wabbajack.Resources.none.jpg"); if (slide.ImageURL != null && slide.Image != null) { if (!CachedSlides.ContainsKey(slide.ModID)) return; @@ -212,7 +167,7 @@ namespace Wabbajack this.ModName = slide.ModName; this.AuthorName = slide.ModAuthor; - this.Summary = slide.ModDescription; + this.Description = slide.ModDescription; this.NexusSiteURL = slide.ModURL; } diff --git a/Wabbajack/UI/MainWindow.xaml b/Wabbajack/Views/CompilerView.xaml similarity index 55% rename from Wabbajack/UI/MainWindow.xaml rename to Wabbajack/Views/CompilerView.xaml index 944e224c..697fb259 100644 --- a/Wabbajack/UI/MainWindow.xaml +++ b/Wabbajack/Views/CompilerView.xaml @@ -1,21 +1,12 @@ - @@ -30,8 +21,9 @@ - - + + + + Text="Compiling" /> - + - + Margin="0,8,0,8" + IsEnabled="{Binding UIReady}"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Value="{Binding MWVM.QueueProgress}" /> + ItemsSource="{Binding MWVM.Log}" /> @@ -82,12 +147,11 @@ Grid.Row="5" Grid.RowSpan="2" Grid.Column="0" - Margin="-4,10,2,10" + Margin="-4,10,0,10" HorizontalAlignment="Stretch"> - @@ -97,35 +161,27 @@