Implement installer UI error messaging

This commit is contained in:
Timothy Baldridge 2022-07-11 14:55:54 -06:00
parent 3edf568ef7
commit 165760e082
12 changed files with 90 additions and 21 deletions

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -85,6 +86,12 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
[Reactive] [Reactive]
public InstallState InstallState { get; set; } public InstallState InstallState { get; set; }
[Reactive]
protected ErrorResponse[] Errors { get; private set; }
[Reactive]
public ErrorResponse Error { get; private set; }
/// <summary> /// <summary>
/// Slideshow Data /// Slideshow Data
/// </summary> /// </summary>
@ -114,6 +121,9 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
[Reactive] [Reactive]
public bool Installing { get; set; } public bool Installing { get; set; }
[Reactive]
public ErrorResponse ErrorState { get; set; }
[Reactive] [Reactive]
public bool ShowNSFWSlides { get; set; } public bool ShowNSFWSlides { get; set; }
@ -221,10 +231,45 @@ public class InstallerVM : BackNavigatingVM, IBackNavigatingVM, ICpuStatusVM
BeginSlideShow(token.Token).FireAndForget(); BeginSlideShow(token.Token).FireAndForget();
Disposable.Create(() => token.Cancel()) Disposable.Create(() => token.Cancel())
.DisposeWith(disposables); .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<ErrorResponse> 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) private async Task BeginSlideShow(CancellationToken token)
{ {
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)

View File

@ -39,19 +39,27 @@ namespace Wabbajack
this.WhenAny(x => x.ViewModel.BeginCommand) this.WhenAny(x => x.ViewModel.BeginCommand)
.BindToStrict(this, x => x.BeginButton.Command) .BindToStrict(this, x => x.BeginButton.Command)
.DisposeWith(dispose); .DisposeWith(dispose);
// Error icon display // Error handling
var vis = this.WhenAny(x => x.ViewModel.Installer.CanInstall)
.Select(err => err.Failed ? Visibility.Visible : Visibility.Hidden) this.WhenAnyValue(x => x.ViewModel.ErrorState)
.Replay(1) .Select(v => !v.Failed)
.RefCount(); .BindToStrict(this, view => view.BeginButton.IsEnabled)
vis.BindToStrict(this, x => x.ErrorSummaryIconGlow.Visibility)
.DisposeWith(dispose); .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); .DisposeWith(dispose);
this.WhenAny(x => x.ViewModel.Installer.CanInstall)
.Select(x => x.Reason) this.WhenAnyValue(x => x.ViewModel.ErrorState)
.BindToStrict(this, x => x.ErrorSummaryIcon.ToolTip) .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); .DisposeWith(dispose);
}); });
} }

View File

@ -78,6 +78,7 @@ namespace Wabbajack
.BindToStrict(this, view => view.TopProgressBar.ProgressPercent) .BindToStrict(this, view => view.TopProgressBar.ProgressPercent)
.DisposeWith(disposables); .DisposeWith(disposables);
// Slideshow // Slideshow
ViewModel.WhenAnyValue(vm => vm.SlideShowTitle) ViewModel.WhenAnyValue(vm => vm.SlideShowTitle)
.Select(f => f) .Select(f => f)

View File

@ -40,4 +40,6 @@ public class NativeFileStreamFactory : IStreamFactory
} }
public IPath Name { get; } public IPath Name { get; }
public AbsolutePath FullPath => (AbsolutePath) Name;
} }

View File

@ -3,9 +3,13 @@ using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.DTOs.Streams;
using Wabbajack.DTOs.Vfs; using Wabbajack.DTOs.Vfs;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Networking.Http; using Wabbajack.Networking.Http;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.VFS.Interfaces; using Wabbajack.VFS.Interfaces;
namespace Wabbajack.Networking.WabbajackClientApi; namespace Wabbajack.Networking.WabbajackClientApi;
@ -14,6 +18,7 @@ public class CesiVFSCache : IVfsCache
{ {
private readonly Client _client; private readonly Client _client;
private readonly ILogger<CesiVFSCache> _logger; private readonly ILogger<CesiVFSCache> _logger;
private const int Threshold = 1024 * 1024 * 128;
public CesiVFSCache(ILogger<CesiVFSCache> logger, Client client) public CesiVFSCache(ILogger<CesiVFSCache> logger, Client client)
{ {
@ -21,8 +26,12 @@ public class CesiVFSCache : IVfsCache
_client = client; _client = client;
} }
public async Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token) public async Task<IndexedVirtualFile?> Get(Hash hash, IStreamFactory sf, CancellationToken token)
{ {
if (sf is not NativeFileStreamFactory nf)
return null;
if (nf.FullPath.Size() < Threshold) return null;
try try
{ {
var result = await _client.GetCesiVfsEntry(hash, token); var result = await _client.GetCesiVfsEntry(hash, token);

View File

@ -281,6 +281,7 @@ public static class AbsolutePathExtensions
public static IEnumerable<AbsolutePath> EnumerateDirectories(this AbsolutePath path, bool recursive = true) public static IEnumerable<AbsolutePath> EnumerateDirectories(this AbsolutePath path, bool recursive = true)
{ {
if (!path.DirectoryExists()) return Array.Empty<AbsolutePath>();
return Directory.EnumerateDirectories(path.ToString(), "*", return Directory.EnumerateDirectories(path.ToString(), "*",
recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.Select(p => (AbsolutePath) p); .Select(p => (AbsolutePath) p);

View File

@ -67,7 +67,7 @@ public struct AbsolutePath : IPath, IComparable<AbsolutePath>, IEquatable<Absolu
} }
} }
public int Depth => Parts.Length; public int Depth => Parts?.Length ?? 0;
public AbsolutePath ReplaceExtension(Extension newExtension) public AbsolutePath ReplaceExtension(Extension newExtension)
{ {

View File

@ -6,8 +6,8 @@ public static class ArrayExtensions
{ {
public static bool AreEqual<T>(T[] a, int startA, T[] b, int startB, int length) public static bool AreEqual<T>(T[] a, int startA, T[] b, int startB, int length)
{ {
if (startA + length > a.Length) return false; if (startA + length > (a?.Length ?? 0)) return false;
if (startB + length > b.Length) return false; if (startB + length > (b?.Length ?? 0)) return false;
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
if (!a[startA + i]!.Equals(b[startB + i])) if (!a[startA + i]!.Equals(b[startB + i]))

View File

@ -1,10 +1,12 @@
using Wabbajack.DTOs.Vfs; using Wabbajack.DTOs.Streams;
using Wabbajack.DTOs.Vfs;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
namespace Wabbajack.VFS.Interfaces; namespace Wabbajack.VFS.Interfaces;
public interface IVfsCache public interface IVfsCache
{ {
public Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token); public Task<IndexedVirtualFile?> Get(Hash hash, IStreamFactory sf, CancellationToken token);
public Task Put(IndexedVirtualFile file, CancellationToken token); public Task Put(IndexedVirtualFile file, CancellationToken token);
} }

View File

@ -1,5 +1,6 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Wabbajack.DTOs.Streams;
using Wabbajack.DTOs.Vfs; using Wabbajack.DTOs.Vfs;
using Wabbajack.Hashing.xxHash64; using Wabbajack.Hashing.xxHash64;
using Wabbajack.VFS.Interfaces; using Wabbajack.VFS.Interfaces;
@ -15,14 +16,14 @@ public class FallthroughVFSCache : IVfsCache
_caches = caches; _caches = caches;
} }
public async Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token) public async Task<IndexedVirtualFile?> Get(Hash hash, IStreamFactory sf, CancellationToken token)
{ {
IndexedVirtualFile? result = null; IndexedVirtualFile? result = null;
foreach (var cache in _caches) foreach (var cache in _caches)
{ {
if (result == null) if (result == null)
{ {
result = await cache.Get(hash, token); result = await cache.Get(hash, sf, token);
if (result == null) continue; if (result == null) continue;
foreach (var upperCache in _caches) foreach (var upperCache in _caches)
{ {

View File

@ -41,7 +41,7 @@ public class VFSDiskCache : IVfsCache
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
public async Task<IndexedVirtualFile?> Get(Hash hash, CancellationToken token) public async Task<IndexedVirtualFile?> Get(Hash hash, IStreamFactory sfn, CancellationToken token)
{ {
if (hash == default) if (hash == default)
throw new ArgumentException("Cannot cache default hashes"); throw new ArgumentException("Cannot cache default hashes");

View File

@ -177,7 +177,7 @@ public class VirtualFile
hash = await hstream.HashingCopy(Stream.Null, token, job); 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) if (found != null)
{ {
var file = ConvertFromIndexedFile(context, found!, relPath, parent!, extractedFile); var file = ConvertFromIndexedFile(context, found!, relPath, parent!, extractedFile);