Merge pull request #1848 from erri120/blazor-3

Blazor 3
This commit is contained in:
Timothy Baldridge 2022-01-28 06:20:46 -07:00 committed by GitHub
commit dc1fd60bfd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 163 additions and 115 deletions

View File

@ -4,11 +4,5 @@ indent_style = space
indent_size = 4
insert_final_newline = true
# C# and Razor files
[*.{cs,razor}]
# RZ10012: Markup element with unexpected name.
# Reason: The component namespace is added to the global _Imports.razor file.
dotnet_diagnostic.RZ10012.severity = none
[*.scss]
indent_size = 2

View File

@ -40,12 +40,13 @@ public partial class App
Layout = "${processtime} [${level:uppercase=true}] (${logger}) ${message:withexception=true}",
Header = "############ Wabbajack log file - ${longdate} ############"
};
var consoleTarget = new ConsoleTarget("console");
var uiTarget = new MemoryTarget
var uiTarget = new UiLoggerTarget
{
Name = "ui",
Layout = "${message}",
};
var blackholeTarget = new NullTarget("blackhole");

View File

@ -16,15 +16,15 @@
@code {
[Parameter]
public string Title { get; set; }
public string? Title { get; set; }
[Parameter]
public string Subtitle { get; set; }
public string? Subtitle { get; set; }
[Parameter]
public string Image { get; set; }
public string? Image { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
public RenderFragment? ChildContent { get; set; }
}

View File

@ -1,7 +1,7 @@
@namespace Wabbajack.App.Blazor.Components
<div id="info-block">
@if (Supertitle != string.Empty)
@if (Supertitle is not null)
{
<span class="supertitle">@Supertitle</span>
}
@ -14,18 +14,18 @@
@code {
[Parameter]
public string Supertitle { get; set; } = string.Empty;
public string? Supertitle { get; set; }
[Parameter]
public string Title { get; set; }
public string? Title { get; set; }
[Parameter]
public string Subtitle { get; set; }
public string? Subtitle { get; set; }
[Parameter]
public string Comment { get; set; }
public string? Comment { get; set; }
[Parameter]
public string Description { get; set; }
public string? Description { get; set; }
}

View File

@ -23,15 +23,14 @@
@code {
[Parameter]
public string Image { get; set; }
public string? Image { get; set; }
[Parameter]
public string Title { get; set; }
public string? Title { get; set; }
[Parameter]
public string Subtitle { get; set; }
public string? Subtitle { get; set; }
[Parameter]
public string Description { get; set; }
public string? Description { get; set; }
}

View File

@ -5,5 +5,5 @@
@code {
[Parameter]
public string Content { get; set; }
public string? Content { get; set; }
}

View File

@ -5,15 +5,14 @@
@code {
[Parameter]
public string Icon { get; set; }
public string? Icon { get; set; }
[Parameter]
public string Label { get; set; }
public string? Label { get; set; }
[Parameter]
public string Size { get; set; }
public string? Size { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
}

View File

@ -2,26 +2,28 @@
<label class="option">
@Label
<input type="checkbox" checked="@IsChecked" @onchange="CheckBoxChanged">
<input type="checkbox" value="@IsChecked" @onchange="CheckBoxChanged">
<span class="checkmark"></span>
</label>
@code {
// TODO: [Low] Implement parameters to customize style.
// TODO: [High] Implement a way to set a passed bool without using callback function.
[Parameter]
public string Label { get; set; }
public string? Label { get; set; }
[Parameter]
public EventCallback<bool> OnChecked { get; set; }
private bool IsChecked { get; set; }
public bool IsChecked { get; set; }
[Parameter]
public EventCallback<bool> IsCheckedChanged { get; set; }
private async Task CheckBoxChanged(ChangeEventArgs e)
{
IsChecked = (bool)(e.Value ?? false);
await OnChecked.InvokeAsync(IsChecked);
if (e.Value is not bool newValue) return;
if (IsChecked == newValue) return;
IsChecked = newValue;
await IsCheckedChanged.InvokeAsync(IsChecked);
}
}

View File

@ -1,32 +1,38 @@
@using Wabbajack.RateLimiter
@using System
@using System.Reactive.Linq
@implements IDisposable
@namespace Wabbajack.App.Blazor.Components
<div id="progress-bar">
<progress value="@CurrentProgress"></progress>
<progress max="1" value="@CurrentProgress.ToString("F")"></progress>
<span class="text">@Text</span>
</div>
@code {
[Parameter] public IObservable<Percent>? ProgressObserver { get; set; }
private double CurrentProgress { get; set; }
private string Text { get; set; } = string.Empty;
[Parameter] public IObservable<Percent> ProgressObserver { get; set; }
[Parameter] public string? Text { get; set; }
private IDisposable? _disposable;
protected override void OnInitialized()
{
var textPercentage = string.IsNullOrEmpty(Text);
ProgressObserver
if (ProgressObserver is null) return;
_disposable = ProgressObserver
.Sample(TimeSpan.FromMilliseconds(250))
.DistinctUntilChanged(p => p.Value)
.Subscribe(p => {
.Subscribe(p =>
{
CurrentProgress = p.Value;
if (textPercentage) Text = p.ToString();
Text = p.ToString();
InvokeAsync(StateHasChanged);
});
}
public void Dispose() => _disposable?.Dispose();
}

View File

@ -1,4 +1,5 @@
@code {
@using Wabbajack.App.Blazor.Utility
@code {
private void OpenPatreonPage() => UIUtils.OpenWebsite(new Uri("https://www.patreon.com/user?u=11907933"));
private void OpenGithubPage() => UIUtils.OpenWebsite(new Uri("https://github.com/wabbajack-tools/wabbajack"));
private void OpenDiscord() => UIUtils.OpenWebsite(new Uri("https://discord.gg/wabbajack"));

View File

@ -1,23 +1,41 @@
@using NLog
@using NLog.Targets
@using Wabbajack.App.Blazor.Utility
@using System.Reactive.Linq
@namespace Wabbajack.App.Blazor.Components
@implements IDisposable
<div id="virtual-logger">
<Virtualize Items="@Logs" Context="logItem" OverscanCount="3">
<span @key="logItem">@logItem</span>
<Virtualize Items="_logs" Context="logItem" OverscanCount="5" ItemSize="24">
<span style="height: 24px">@logItem</span>
</Virtualize>
</div>
@code {
// TODO: [Low] More parameters to customise the logger. E.g. Reverse order.
// TODO: [High] Find a way to auto-scroll. (JS interop?)
private MemoryTarget? _memoryTarget;
private ICollection<string> Logs => _memoryTarget?.Logs ?? Array.Empty<string>();
private UiLoggerTarget? _loggerTarget;
private ICollection<string> _logs = new List<string>();
protected override void OnInitialized()
{
_memoryTarget = LogManager.Configuration.FindTargetByName<MemoryTarget>("ui");
private bool _shouldRender = false;
protected override bool ShouldRender() => _shouldRender;
private IDisposable? _disposable;
protected override Task OnInitializedAsync()
{
_loggerTarget = LogManager.Configuration.FindTargetByName<UiLoggerTarget>("ui");
_disposable = _loggerTarget.Logs.Sample(TimeSpan.FromMilliseconds(250)).Subscribe(next =>
{
_logs.Add(next);
InvokeAsync(StateHasChanged);
});
_shouldRender = true;
return Task.CompletedTask;
}
public void Dispose() => _disposable?.Dispose();
}

View File

@ -1,6 +1,5 @@
<Fluxor.Blazor.Web.StoreInitializer/>
@using Wabbajack.App.Blazor.Shared
@using Wabbajack.App.Blazor.Shared
<CascadingBlazoredModal>
<Router AppAssembly="@GetType().Assembly">
<Found Context="routeData">

View File

@ -22,9 +22,9 @@ public partial class Gallery
[Inject] private IStateContainer StateContainer { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private ModListDownloadMaintainer Maintainer { get; set; } = default!;
[Inject] private IModalService Modal { get; set; } = default!;
private IObservable<Percent> DownloadProgress { get; set; }
private IObservable<Percent>? DownloadProgress { get; set; }
private ModlistMetadata? DownloadingMetaData { get; set; }
private IEnumerable<ModlistMetadata> Modlists => StateContainer.Modlists;
@ -58,7 +58,7 @@ public partial class Gallery
NavigationManager.NavigateTo(Configure.Route);
}
private async Task OnClickInformation(ModlistMetadata metadata)
private void OnClickInformation(ModlistMetadata metadata)
{
// TODO: [High] Implement information modal.
var parameters = new ModalParameters();

View File

@ -38,8 +38,8 @@
</div>
</div>
<div class="options">
<OptionCheckbox Label="Overwrite Installation"/>
<OptionCheckbox Label="NTFS Compression"/>
<OptionCheckbox Label="Overwrite Installation" @bind-IsChecked="OverwriteInstallation"/>
<OptionCheckbox Label="NTFS Compression" @bind-IsChecked="UseCompression"/>
<OptionCheckbox Label="Do a sweet trick"/>
<OptionCheckbox Label="Something else"/>
</div>

View File

@ -24,18 +24,19 @@ public partial class Configure
[Inject] private SettingsManager SettingsManager { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
[Inject] private IToastService toastService { get; set; }
[Inject] private IToastService ToastService { get; set; } = default!;
private ModList? Modlist => StateContainer.Modlist;
private string? ModlistImage => StateContainer.ModlistImage;
private AbsolutePath ModlistPath => StateContainer.ModlistPath;
private AbsolutePath InstallPath => StateContainer.InstallPath;
private AbsolutePath DownloadPath => StateContainer.DownloadPath;
private InstallState InstallState => StateContainer.InstallState;
private const string InstallSettingsPrefix = "install-settings-";
private bool OverwriteInstallation { get; set; }
private bool UseCompression { get; set; }
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
@ -55,8 +56,8 @@ public partial class Configure
}
catch (Exception e)
{
toastService.ShowError("Could not load modlist!");
Logger.LogError(e, "Exception loading Modlist file {Name}", ModlistPath);
ToastService.ShowError("Could not load modlist!");
Logger.LogError(e, "Exception loading Modlist file {Name}", ModlistPath.ToString());
NavigationManager.NavigateTo(Select.Route);
return;
}
@ -74,19 +75,19 @@ public partial class Configure
}
catch (Exception e)
{
Logger.LogWarning(e, "Exception loading previous settings for {Name}", ModlistPath);
Logger.LogWarning(e, "Exception loading previous settings for {Name}", ModlistPath.ToString());
}
try
{
var imageStream = await StandardInstaller.ModListImageStream(ModlistPath);
var dotnetImageStream = new DotNetStreamReference(imageStream);
StateContainer.ModlistImage = await JSRuntime.InvokeAsync<string>("getBlobUrlFromStream", dotnetImageStream);
StateContainer.ModlistImage = await JSRuntime.InvokeAsync<string>(JsInterop.GetBlobUrlFromStream, dotnetImageStream);
}
catch (Exception e)
{
toastService.ShowWarning("Could not load modlist image.");
Logger.LogWarning(e, "Exception loading modlist image for {Name}", ModlistPath);
ToastService.ShowWarning("Could not load modlist image.");
Logger.LogWarning(e, "Exception loading modlist image for {Name}", ModlistPath.ToString());
}
}

View File

@ -4,7 +4,10 @@
<div id="content">
<div class="install-background">
<img id="background-image" src="@ModlistImage" alt=""/>
@if (ModlistImage is not null)
{
<img id="background-image" src="@ModlistImage" alt=""/>
}
</div>
<div class="list">
@if (Modlist is not null)

View File

@ -29,36 +29,31 @@ public partial class Installing
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
private ModList? Modlist => StateContainer.Modlist;
private string ModlistImage => StateContainer.ModlistImage;
private string? ModlistImage => StateContainer.ModlistImage;
private AbsolutePath ModlistPath => StateContainer.ModlistPath;
private AbsolutePath InstallPath => StateContainer.InstallPath;
private AbsolutePath DownloadPath => StateContainer.DownloadPath;
public string StatusCategory { get; set; }
private string LastStatus { get; set; }
public List<string> StatusStep { get; set; } = new();
private InstallState InstallState => StateContainer.InstallState;
private string? StatusCategory { get; set; }
private string? LastStatus { get; set; }
private List<string> StatusStep { get; set; } = new();
private const string InstallSettingsPrefix = "install-settings-";
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
Install();
_shouldRender = true;
await Task.Run(Install);
}
private async Task Install()
{
if (Modlist is null) return;
StateContainer.InstallState = InstallState.Installing;
await Task.Run(() => BeginInstall(Modlist));
await BeginInstall(Modlist);
}
private async Task BeginInstall(ModList modlist)

View File

@ -2,6 +2,7 @@
@inherits LayoutComponentBase
@namespace Wabbajack.App.Blazor.Shared
<BlazoredToasts Position="ToastPosition.BottomRight"/>
<div id="background"></div>
<SideBar/>

View File

@ -0,0 +1,12 @@
using Microsoft.JSInterop;
namespace Wabbajack.App.Blazor.Utility;
public static class JsInterop
{
/// <summary>
/// Converts a <see cref="DotNetStreamReference"/> into a blob URL. Useful for streaming images.
/// <code>async function getBlobUrlFromStream(imageStream: DotNetStreamReference)</code>
/// </summary>
public const string GetBlobUrlFromStream = "getBlobUrlFromStream";
}

View File

@ -0,0 +1,17 @@
using System;
using System.Reactive.Subjects;
using NLog;
using NLog.Targets;
namespace Wabbajack.App.Blazor.Utility;
public class UiLoggerTarget : TargetWithLayout
{
private readonly Subject<string> _logs = new();
public IObservable<string> Logs => _logs;
protected override void Write(LogEventInfo logEvent)
{
_logs.OnNext(RenderLogEvent(Layout, logEvent));
}
}

View File

@ -1,16 +1,10 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Media.Imaging;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack
namespace Wabbajack.App.Blazor.Utility
{
public static class UIUtils
{
@ -31,11 +25,11 @@ namespace Wabbajack
}
public static AbsolutePath OpenFileDialog(string filter, string initialDirectory = null)
public static AbsolutePath OpenFileDialog(string filter, string? initialDirectory = null)
{
OpenFileDialog ofd = new OpenFileDialog();
var ofd = new OpenFileDialog();
ofd.Filter = filter;
ofd.InitialDirectory = initialDirectory;
ofd.InitialDirectory = initialDirectory ?? string.Empty;
if (ofd.ShowDialog() == DialogResult.OK)
return (AbsolutePath)ofd.FileName;
return default;

View File

@ -120,7 +120,7 @@ public abstract class AInstaller<T>
ExtractedModlistFolder = _manager.CreateFolder();
await using var stream = _configuration.ModlistArchive.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
using var archive = new ZipArchive(stream, ZipArchiveMode.Read);
NextStep("Preparing","Extracting Modlist", archive.Entries.Count);
NextStep(Consts.StepPreparing,"Extracting Modlist", archive.Entries.Count);
foreach (var entry in archive.Entries)
{
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
@ -182,7 +182,7 @@ public abstract class AInstaller<T>
/// </summary>
protected async Task PrimeVFS()
{
NextStep("Preparing","Priming VFS", 0);
NextStep(Consts.StepPreparing,"Priming VFS", 0);
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
HashedArchives);
await _vfs.BackfillMissing();
@ -190,7 +190,7 @@ public abstract class AInstaller<T>
public async Task BuildFolderStructure()
{
NextStep("Preparing", "Building Folder Structure", 0);
NextStep(Consts.StepPreparing, "Building Folder Structure", 0);
_logger.LogInformation("Building Folder Structure");
ModList.Directives
.Where(d => d.To.Depth > 1)
@ -201,7 +201,7 @@ public abstract class AInstaller<T>
public async Task InstallArchives(CancellationToken token)
{
NextStep("Installing", "Installing files", ModList.Directives.Sum(d => d.Size));
NextStep(Consts.StepInstalling, "Installing files", ModList.Directives.Sum(d => d.Size));
var grouped = ModList.Directives
.OfType<FromArchive>()
.Select(a => new {VF = _vfs.Index.FileForArchiveHashPath(a.ArchiveHashPath), Directive = a})
@ -302,15 +302,15 @@ public abstract class AInstaller<T>
}
}
_logger.LogInformation("Downloading {count} archives", missing.Count);
NextStep("Downloading", "Downloading files", missing.Count);
_logger.LogInformation("Downloading {Count} archives", missing.Count.ToString());
NextStep(Consts.StepDownloading, "Downloading files", missing.Count);
await missing
.OrderBy(a => a.Size)
.Where(a => a.State is not Manual)
.PDoAll(async archive =>
{
_logger.LogInformation("Downloading {archive}", archive.Name);
_logger.LogInformation("Downloading {Archive}", archive.Name);
var outputPath = _configuration.Downloads.Combine(archive.Name);
if (download)
@ -364,7 +364,7 @@ public abstract class AInstaller<T>
public async Task HashArchives(CancellationToken token)
{
NextStep("Hashing", "Hashing Archives", 0);
NextStep(Consts.StepHashing, "Hashing Archives", 0);
_logger.LogInformation("Looking for files to hash");
var allFiles = _configuration.Downloads.EnumerateFiles()
@ -415,7 +415,7 @@ public abstract class AInstaller<T>
var savePath = (RelativePath) "saves";
var existingFiles = _configuration.Install.EnumerateFiles().ToList();
NextStep("Preparing", "Looking for files to delete", existingFiles.Count);
NextStep(Consts.StepPreparing, "Looking for files to delete", existingFiles.Count);
await existingFiles
.PDoAll(async f =>
{
@ -434,7 +434,7 @@ public abstract class AInstaller<T>
});
_logger.LogInformation("Cleaning empty folders");
NextStep("Preparing", "Cleaning empty folders", indexed.Keys.Count);
NextStep(Consts.StepPreparing, "Cleaning empty folders", indexed.Keys.Count);
var expectedFolders = (indexed.Keys
.Select(f => f.RelativeTo(_configuration.Install))
// We ignore the last part of the path, so we need a dummy file name
@ -469,7 +469,7 @@ public abstract class AInstaller<T>
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
NextStep("Preparing", "Removing redundant directives", indexed.Count);
NextStep(Consts.StepPreparing, "Removing redundant directives", indexed.Count);
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
{
// Bit backwards, but we want to return null for
@ -488,7 +488,7 @@ public abstract class AInstaller<T>
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
indexed.Count);
NextStep("Preparing", "Finalizing modlist optimization", 0);
NextStep(Consts.StepPreparing, "Finalizing modlist optimization", 0);
var requiredArchives = indexed.Values.OfType<FromArchive>()
.GroupBy(d => d.ArchiveHashPath.Hash)
.Select(d => d.Key)

View File

@ -2,7 +2,7 @@ using Wabbajack.Paths;
namespace Wabbajack.Installer;
public class Consts
public static class Consts
{
public static string GAME_PATH_MAGIC_BACK = "{--||GAME_PATH_MAGIC_BACK||--}";
public static string GAME_PATH_MAGIC_DOUBLE_BACK = "{--||GAME_PATH_MAGIC_DOUBLE_BACK||--}";
@ -20,4 +20,10 @@ public class Consts
public static RelativePath MO2ModFolderName = "mods".ToRelativePath();
public static RelativePath MO2ProfilesFolderName = "profiles".ToRelativePath();
public const string StepPreparing = "Preparing";
public const string StepInstalling = "Installing";
public const string StepDownloading = "Downloading";
public const string StepHashing = "Hashing";
public const string StepFinished = "Finished";
}

View File

@ -61,7 +61,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
{
if (token.IsCancellationRequested) return false;
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
NextStep("Preparing", "Configuring Installer", 0);
NextStep(Consts.StepPreparing, "Configuring Installer", 0);
_logger.LogInformation("Configuring Processor");
if (_configuration.GameFolder == default)
@ -145,7 +145,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
await ExtractedModlistFolder!.DisposeAsync();
await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name);
NextStep("Finished", "Finished", 1);
NextStep(Consts.StepFinished, "Finished", 1);
_logger.LogInformation("Finished Installation");
return true;
}
@ -275,7 +275,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
private async Task InstallIncludedFiles(CancellationToken token)
{
_logger.LogInformation("Writing inline files");
NextStep("Installing", "Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
NextStep(Consts.StepInstalling, "Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
await ModList.Directives
.OfType<InlineFile>()
.PDoAll(async directive =>