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.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; }
/// <summary>
/// Slideshow Data
/// </summary>
@ -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<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)
{
while (!token.IsCancellationRequested)

View File

@ -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);
});
}

View File

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

View File

@ -40,4 +40,6 @@ public class NativeFileStreamFactory : IStreamFactory
}
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.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<CesiVFSCache> _logger;
private const int Threshold = 1024 * 1024 * 128;
public CesiVFSCache(ILogger<CesiVFSCache> logger, Client client)
{
@ -21,8 +26,12 @@ public class CesiVFSCache : IVfsCache
_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
{
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)
{
if (!path.DirectoryExists()) return Array.Empty<AbsolutePath>();
return Directory.EnumerateDirectories(path.ToString(), "*",
recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly)
.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)
{

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)
{
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]))

View File

@ -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<IndexedVirtualFile?> Get(Hash hash, CancellationToken token);
public Task<IndexedVirtualFile?> Get(Hash hash, IStreamFactory sf, CancellationToken token);
public Task Put(IndexedVirtualFile file, CancellationToken token);
}

View File

@ -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<IndexedVirtualFile?> Get(Hash hash, CancellationToken token)
public async Task<IndexedVirtualFile?> 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)
{

View File

@ -41,7 +41,7 @@ public class VFSDiskCache : IVfsCache
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)
throw new ArgumentException("Cannot cache default hashes");

View File

@ -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);