From 165760e08270b2c53bc3e4d61aefb683fc89e710 Mon Sep 17 00:00:00 2001 From: Timothy Baldridge Date: Mon, 11 Jul 2022 14:55:54 -0600 Subject: [PATCH] Implement installer UI error messaging --- .../View Models/Installers/InstallerVM.cs | 45 +++++++++++++++++++ .../InstallationConfigurationView.xaml.cs | 30 ++++++++----- .../Views/Installers/InstallationView.xaml.cs | 1 + Wabbajack.Common/NativeFileStreamFactory.cs | 2 + .../CesiVFSCache.cs | 11 ++++- Wabbajack.Paths.IO/AbsolutePathExtensions.cs | 1 + Wabbajack.Paths/AbsolutePath.cs | 2 +- Wabbajack.Paths/ArrayExtensions.cs | 4 +- Wabbajack.VFS.Interfaces/IVfsCache.cs | 6 ++- Wabbajack.VFS/FallthroughVFSCache.cs | 5 ++- Wabbajack.VFS/VFSCache.cs | 2 +- Wabbajack.VFS/VirtualFile.cs | 2 +- 12 files changed, 90 insertions(+), 21 deletions(-) diff --git a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs index 11b55c15..9ee7c521 100644 --- a/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Installers/InstallerVM.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; @@ -85,6 +86,12 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM [Reactive] public InstallState InstallState { get; set; } + [Reactive] + protected ErrorResponse[] Errors { get; private set; } + + [Reactive] + public ErrorResponse Error { get; private set; } + /// /// Slideshow Data /// @@ -114,6 +121,9 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM [Reactive] public bool Installing { get; set; } + [Reactive] + public ErrorResponse ErrorState { get; set; } + [Reactive] public bool ShowNSFWSlides { get; set; } @@ -221,10 +231,45 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM 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"); + } + + private async Task BeginSlideShow(CancellationToken token) { while (!token.IsCancellationRequested) diff --git a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs index 7e298345..121da58f 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationConfigurationView.xaml.cs @@ -39,19 +39,27 @@ namespace Wabbajack this.WhenAny(x => x.ViewModel.BeginCommand) .BindToStrict(this, x => x.BeginButton.Command) .DisposeWith(dispose); - - // Error icon display - var vis = this.WhenAny(x => x.ViewModel.Installer.CanInstall) - .Select(err => err.Failed ? Visibility.Visible : Visibility.Hidden) - .Replay(1) - .RefCount(); - vis.BindToStrict(this, x => x.ErrorSummaryIconGlow.Visibility) + + // Error handling + + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => !v.Failed) + .BindToStrict(this, view => view.BeginButton.IsEnabled) .DisposeWith(dispose); - vis.BindToStrict(this, x => x.ErrorSummaryIcon.Visibility) + + this.WhenAnyValue(x => x.ViewModel.ErrorState) + .Select(v => v.Failed ? Visibility.Visible : Visibility.Hidden) + .BindToStrict(this, view => view.ErrorSummaryIcon.Visibility) .DisposeWith(dispose); - this.WhenAny(x => x.ViewModel.Installer.CanInstall) - .Select(x => x.Reason) - .BindToStrict(this, x => x.ErrorSummaryIcon.ToolTip) + + 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.cs b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs index 1a9deb48..961e9366 100644 --- a/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/Installers/InstallationView.xaml.cs @@ -78,6 +78,7 @@ namespace Wabbajack .BindToStrict(this, view => view.TopProgressBar.ProgressPercent) .DisposeWith(disposables); + // Slideshow ViewModel.WhenAnyValue(vm => vm.SlideShowTitle) .Select(f => f) diff --git a/Wabbajack.Common/NativeFileStreamFactory.cs b/Wabbajack.Common/NativeFileStreamFactory.cs index 4566b15d..07ea40d9 100644 --- a/Wabbajack.Common/NativeFileStreamFactory.cs +++ b/Wabbajack.Common/NativeFileStreamFactory.cs @@ -40,4 +40,6 @@ public class NativeFileStreamFactory : IStreamFactory } public IPath Name { get; } + + public AbsolutePath FullPath => (AbsolutePath) Name; } \ No newline at end of file diff --git a/Wabbajack.Networking.WabbajackClientApi/CesiVFSCache.cs b/Wabbajack.Networking.WabbajackClientApi/CesiVFSCache.cs index 0eb0840e..a5c08e3f 100644 --- a/Wabbajack.Networking.WabbajackClientApi/CesiVFSCache.cs +++ b/Wabbajack.Networking.WabbajackClientApi/CesiVFSCache.cs @@ -3,9 +3,13 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Wabbajack.Common; +using Wabbajack.DTOs.Streams; using Wabbajack.DTOs.Vfs; using Wabbajack.Hashing.xxHash64; using Wabbajack.Networking.Http; +using Wabbajack.Paths; +using Wabbajack.Paths.IO; using Wabbajack.VFS.Interfaces; namespace Wabbajack.Networking.WabbajackClientApi; @@ -14,6 +18,7 @@ public class CesiVFSCache : IVfsCache { private readonly Client _client; private readonly ILogger _logger; + private const int Threshold = 1024 * 1024 * 128; public CesiVFSCache(ILogger logger, Client client) { @@ -21,8 +26,12 @@ public class CesiVFSCache : IVfsCache _client = client; } - public async Task Get(Hash hash, CancellationToken token) + public async Task Get(Hash hash, IStreamFactory sf, CancellationToken token) { + if (sf is not NativeFileStreamFactory nf) + return null; + if (nf.FullPath.Size() < Threshold) return null; + try { var result = await _client.GetCesiVfsEntry(hash, token); diff --git a/Wabbajack.Paths.IO/AbsolutePathExtensions.cs b/Wabbajack.Paths.IO/AbsolutePathExtensions.cs index 012f53e6..25b06a87 100644 --- a/Wabbajack.Paths.IO/AbsolutePathExtensions.cs +++ b/Wabbajack.Paths.IO/AbsolutePathExtensions.cs @@ -281,6 +281,7 @@ public static class AbsolutePathExtensions public static IEnumerable EnumerateDirectories(this AbsolutePath path, bool recursive = true) { + if (!path.DirectoryExists()) return Array.Empty(); return Directory.EnumerateDirectories(path.ToString(), "*", recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .Select(p => (AbsolutePath) p); diff --git a/Wabbajack.Paths/AbsolutePath.cs b/Wabbajack.Paths/AbsolutePath.cs index 42f6c0bb..e3905cc0 100644 --- a/Wabbajack.Paths/AbsolutePath.cs +++ b/Wabbajack.Paths/AbsolutePath.cs @@ -67,7 +67,7 @@ public struct AbsolutePath : IPath, IComparable, IEquatable Parts.Length; + public int Depth => Parts?.Length ?? 0; public AbsolutePath ReplaceExtension(Extension newExtension) { diff --git a/Wabbajack.Paths/ArrayExtensions.cs b/Wabbajack.Paths/ArrayExtensions.cs index bf816b45..6bc76b5f 100644 --- a/Wabbajack.Paths/ArrayExtensions.cs +++ b/Wabbajack.Paths/ArrayExtensions.cs @@ -6,8 +6,8 @@ public static class ArrayExtensions { public static bool AreEqual(T[] a, int startA, T[] b, int startB, int length) { - if (startA + length > a.Length) return false; - if (startB + length > b.Length) return false; + if (startA + length > (a?.Length ?? 0)) return false; + if (startB + length > (b?.Length ?? 0)) return false; for (var i = 0; i < length; i++) if (!a[startA + i]!.Equals(b[startB + i])) diff --git a/Wabbajack.VFS.Interfaces/IVfsCache.cs b/Wabbajack.VFS.Interfaces/IVfsCache.cs index a25c0f3d..b1e56e90 100644 --- a/Wabbajack.VFS.Interfaces/IVfsCache.cs +++ b/Wabbajack.VFS.Interfaces/IVfsCache.cs @@ -1,10 +1,12 @@ -using Wabbajack.DTOs.Vfs; +using Wabbajack.DTOs.Streams; +using Wabbajack.DTOs.Vfs; using Wabbajack.Hashing.xxHash64; +using Wabbajack.Paths; namespace Wabbajack.VFS.Interfaces; public interface IVfsCache { - public Task Get(Hash hash, CancellationToken token); + public Task Get(Hash hash, IStreamFactory sf, CancellationToken token); public Task Put(IndexedVirtualFile file, CancellationToken token); } \ No newline at end of file diff --git a/Wabbajack.VFS/FallthroughVFSCache.cs b/Wabbajack.VFS/FallthroughVFSCache.cs index c711f60d..df4655cf 100644 --- a/Wabbajack.VFS/FallthroughVFSCache.cs +++ b/Wabbajack.VFS/FallthroughVFSCache.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Wabbajack.DTOs.Streams; using Wabbajack.DTOs.Vfs; using Wabbajack.Hashing.xxHash64; using Wabbajack.VFS.Interfaces; @@ -15,14 +16,14 @@ public class FallthroughVFSCache : IVfsCache _caches = caches; } - public async Task Get(Hash hash, CancellationToken token) + public async Task Get(Hash hash, IStreamFactory sf, CancellationToken token) { IndexedVirtualFile? result = null; foreach (var cache in _caches) { if (result == null) { - result = await cache.Get(hash, token); + result = await cache.Get(hash, sf, token); if (result == null) continue; foreach (var upperCache in _caches) { diff --git a/Wabbajack.VFS/VFSCache.cs b/Wabbajack.VFS/VFSCache.cs index 1a9fe2e3..e3c5677b 100644 --- a/Wabbajack.VFS/VFSCache.cs +++ b/Wabbajack.VFS/VFSCache.cs @@ -41,7 +41,7 @@ public class VFSDiskCache : IVfsCache cmd.ExecuteNonQuery(); } - public async Task Get(Hash hash, CancellationToken token) + public async Task Get(Hash hash, IStreamFactory sfn, CancellationToken token) { if (hash == default) throw new ArgumentException("Cannot cache default hashes"); diff --git a/Wabbajack.VFS/VirtualFile.cs b/Wabbajack.VFS/VirtualFile.cs index 67af6d2a..9f6719bb 100644 --- a/Wabbajack.VFS/VirtualFile.cs +++ b/Wabbajack.VFS/VirtualFile.cs @@ -177,7 +177,7 @@ public class VirtualFile hash = await hstream.HashingCopy(Stream.Null, token, job); } - var found = await context.VfsCache.Get(hash, token); + var found = await context.VfsCache.Get(hash, extractedFile, token); if (found != null) { var file = ConvertFromIndexedFile(context, found!, relPath, parent!, extractedFile);