Merge pull request #1847 from Unnoen/more-blazor

Blazor updates and minor changes to other internal projects.
This commit is contained in:
Timothy Baldridge 2022-01-27 22:32:46 -07:00 committed by GitHub
commit a725688495
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 574 additions and 256 deletions

View File

@ -8,6 +8,8 @@ using NLog.Targets;
using Wabbajack.App.Blazor.State;
using Wabbajack.App.Blazor.Utility;
using Wabbajack.Services.OSIntegrated;
using Blazored.Modal;
using Blazored.Toast;
namespace Wabbajack.App.Blazor;
@ -22,6 +24,7 @@ public partial class App
.ConfigureServices(services => ConfigureServices(services))
.Build()
.Services;
_serviceProvider.GetRequiredService<SystemParametersConstructor>();
}
private static void SetupLogging(ILoggingBuilder loggingBuilder)
@ -30,30 +33,43 @@ public partial class App
var fileTarget = new FileTarget("file")
{
FileName = "log.log"
FileName = "logs/Wabbajack.current.log",
ArchiveFileName = "logs/Wabbajack.{##}.log",
ArchiveOldFileOnStartup = true,
MaxArchiveFiles = 10,
Layout = "${processtime} [${level:uppercase=true}] (${logger}) ${message:withexception=true}",
Header = "############ Wabbajack log file - ${longdate} ############"
};
var consoleTarget = new ConsoleTarget("console");
var uiTarget = new MemoryTarget("ui");
var uiTarget = new MemoryTarget
{
Name = "ui",
Layout = "${message}",
};
var blackholeTarget = new NullTarget("blackhole");
if (!string.Equals("TRUE", Environment.GetEnvironmentVariable("DEBUG_BLAZOR", EnvironmentVariableTarget.Process), StringComparison.OrdinalIgnoreCase))
{
config.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Debug, blackholeTarget, "Microsoft.AspNetCore.Components.*", true);
}
config.AddRuleForAllLevels(fileTarget);
config.AddRuleForAllLevels(consoleTarget);
config.AddRuleForAllLevels(uiTarget);
loggingBuilder.ClearProviders();
loggingBuilder.SetMinimumLevel(LogLevel.Trace);
loggingBuilder.AddNLog(config);
}
private static IServiceCollection ConfigureServices(IServiceCollection services)
{
services.AddOSIntegrated();
services.AddBlazorWebView();
services.AddBlazoredModal();
services.AddBlazoredToast();
services.AddTransient<MainWindow>();
services.AddSingleton<SystemParametersConstructor>();
services.AddSingleton<IStateContainer, StateContainer>();

View File

@ -0,0 +1,9 @@
@namespace Wabbajack.App.Blazor.Components
<h3>[TBI] Model Component</h3>
<p>@Content</p>
@code {
[Parameter]
public string Content { get; set; }
}

View File

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

View File

@ -28,7 +28,7 @@
{
{"Play", Play.Route},
{"Gallery", Gallery.Route},
{"Install", Install.Route},
{"Install", Select.Route},
{"Create", Create.Route}
};

View File

@ -1,13 +1,14 @@
<Fluxor.Blazor.Web.StoreInitializer/>
@using Wabbajack.App.Blazor.Shared
<Router AppAssembly="@GetType().Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<h1>Not found</h1>
<p>Sorry, there's nothing here.</p>
</NotFound>
</Router>
<CascadingBlazoredModal>
<Router AppAssembly="@GetType().Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
</Found>
<NotFound>
<h1>Not found</h1>
<p>Sorry, there's nothing here.</p>
</NotFound>
</Router>
</CascadingBlazoredModal>

View File

@ -1,79 +0,0 @@
@page "/configure"
@using Wabbajack.App.Blazor.State
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
<div class="install-background">
<img id="background-image" src="" alt=""/>
</div>
<div class="list">
@if (Modlist is not null)
{
<div class="left-side">
@if (InstallState != InstallState.Installing)
{
<InfoBlock Title="@Modlist.Name" Subtitle="@Modlist.Author" Comment="@Modlist.Version.ToString()" Description="@Modlist.Description"/>
}
else
{
<InfoBlock Supertitle="Installing..." Title="@Modlist.Name" Subtitle="@StatusText"/>
// TODO: [Low] Step logging
}
</div>
<div class="right-side">
@* TODO: whatever this is *@
@* @if (!string.IsNullOrEmpty(Image)) *@
@* { *@
@* if (InstallState != GlobalState.InstallStateEnum.Installing) *@
@* { *@
@* <InfoImage Image="@Image"/> *@
@* } *@
@* else if (InstallState == GlobalState.InstallStateEnum.Installing) *@
@* { *@
@* // TODO: [Low] Implement featured mod slideshow. *@
@* <InfoImage Image="@Image" Title="Some Mod Title" Subtitle="Author and others" Description="This mod adds something cool but I'm not going to tell you anything."/> *@
@* } *@
@* } *@
</div>
}
</div>
@if (InstallState == InstallState.Installing)
{
<div class="logger-container">
<VirtualLogger/>
</div>
}
else
{
<div class="settings">
<div class="locations">
@* TODO: [High] Turn path selectors into components. *@
<div class="labels">
<span>Target Modlist</span>
<span>Install Location</span>
<span>Download Location</span>
</div>
<div class="paths">
<span class="modlist-file">@ModlistPath.ToString()</span>
<span class="install-location" @onclick="SelectInstallFolder">@InstallPath.ToString()</span>
<span class="download-location" @onclick="SelectDownloadFolder">@DownloadPath.ToString()</span>
</div>
</div>
<div class="options">
<OptionCheckbox Label="Overwrite Installation"/>
<OptionCheckbox Label="NTFS Compression"/>
<OptionCheckbox Label="Do a sweet trick"/>
<OptionCheckbox Label="Something else"/>
</div>
<div class="install">
<img src="images/icons/play.svg" @onclick="Install" alt="Browse Gallery">
</div>
</div>
}
</div>
@code {
public const string Route = "/configure";
}

View File

@ -1,7 +1,5 @@
@page "/gallery"
@using System.Globalization
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
@ -39,7 +37,7 @@
{
<BottomBar Image="@DownloadingMetaData.Links.ImageUri" Title="Downloading..." Subtitle="@DownloadingMetaData.Title">
<div style="height:1.5rem;">
<ProgressBar Percentage="@DownloadProgress" Text="@DownloadProgress.Value.ToString(CultureInfo.InvariantCulture)"/>
<ProgressBar ProgressObserver="@DownloadProgress"/>
</div>
</BottomBar>
}

View File

@ -4,12 +4,13 @@ using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows.Shell;
using Blazored.Modal;
using Blazored.Modal.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Wabbajack.App.Blazor.Components;
using Wabbajack.App.Blazor.State;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.Paths.IO;
using Wabbajack.RateLimiter;
using Wabbajack.Services.OSIntegrated.Services;
@ -22,13 +23,14 @@ public partial class Gallery
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private ModListDownloadMaintainer Maintainer { get; set; } = default!;
private Percent DownloadProgress { get; set; } = Percent.Zero;
[Inject] private IModalService Modal { get; set; } = default!;
private IObservable<Percent> DownloadProgress { get; set; }
private ModlistMetadata? DownloadingMetaData { get; set; }
private IEnumerable<ModlistMetadata> Modlists => StateContainer.Modlists;
private bool _errorLoadingModlists;
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
@ -44,19 +46,24 @@ public partial class Gallery
return;
}
}
_shouldRender = true;
}
private async Task OnClickDownload(ModlistMetadata metadata)
{
// GlobalState.NavigationAllowed = !GlobalState.NavigationAllowed;
await Download(metadata);
if (!await Maintainer.HaveModList(metadata)) await Download(metadata);
StateContainer.ModlistPath = Maintainer.ModListPath(metadata);
StateContainer.Modlist = null;
NavigationManager.NavigateTo(Configure.Route);
}
private async Task OnClickInformation(ModlistMetadata metadata)
{
// TODO: [High] Implement information modal.
var parameters = new ModalParameters();
parameters.Add(nameof(InfoModal.Content), metadata.Description);
Modal.Show<InfoModal>("Information", parameters);
}
private async Task Download(ModlistMetadata metadata)
@ -64,35 +71,26 @@ public partial class Gallery
StateContainer.NavigationAllowed = false;
DownloadingMetaData = metadata;
// TODO: download progress should be in ProgressBar component so it can refresh independently
try
{
var (progress, task) = Maintainer.DownloadModlist(metadata);
var dispose = progress
.DistinctUntilChanged(p => p.Value)
.Throttle(TimeSpan.FromMilliseconds(100))
.Subscribe(p =>
{
DownloadProgress = p;
StateContainer.TaskBarState = new TaskBarState
{
Description = $"Downloading {metadata.Title}",
State = TaskbarItemProgressState.Indeterminate,
ProgressValue = p.Value
};
InvokeAsync(StateHasChanged);
// Dispatcher.CreateDefault().InvokeAsync(StateHasChanged);
}, () => { StateContainer.TaskBarState = new TaskBarState(); });
DownloadProgress = progress;
var dispose = progress
.Sample(TimeSpan.FromMilliseconds(250))
.DistinctUntilChanged(p => p.Value)
.Subscribe(p => {
StateContainer.TaskBarState = new TaskBarState
{
Description = $"Downloading {metadata.Title}",
State = TaskbarItemProgressState.Normal,
ProgressValue = p.Value
};
}, () => { StateContainer.TaskBarState = new TaskBarState(); });
await task;
dispose.Dispose();
var path = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
StateContainer.ModlistPath = path;
NavigationManager.NavigateTo(Configure.Route);
}
catch (Exception e)
{

View File

@ -0,0 +1,54 @@
@page "/install/configure"
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
<div class="install-background">
@if (ModlistImage is not null)
{
<img id="background-image" src="@ModlistImage" alt=""/>
}
</div>
<div class="list">
@if (Modlist is not null)
{
<div class="left-side">
<InfoBlock Title="@Modlist.Name" Subtitle="@Modlist.Author" Comment="@Modlist.Version.ToString()" Description="@Modlist.Description"/>
</div>
<div class="right-side">
@if (ModlistImage is not null)
{
<InfoImage Image="@ModlistImage"/>
}
</div>
}
</div>
<div class="settings">
<div class="locations">
<div class="labels">
<span>Target Modlist</span>
<span>Install Location</span>
<span>Download Location</span>
</div>
<div class="paths">
<span class="modlist-file">@ModlistPath.ToString()</span>
<span class="install-location" @onclick="SelectInstallFolder">@InstallPath.ToString()</span>
<span class="download-location" @onclick="SelectDownloadFolder">@DownloadPath.ToString()</span>
</div>
</div>
<div class="options">
<OptionCheckbox Label="Overwrite Installation"/>
<OptionCheckbox Label="NTFS Compression"/>
<OptionCheckbox Label="Do a sweet trick"/>
<OptionCheckbox Label="Something else"/>
</div>
<div class="install">
<img src="images/icons/play.svg" @onclick="Install" alt="Install">
</div>
</div>
</div>
@code {
public const string Route = "/install/configure";
}

View File

@ -0,0 +1,131 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Components;
using Wabbajack.DTOs;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Installer;
using Wabbajack.Paths;
using Wabbajack.App.Blazor.Utility;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Services.OSIntegrated;
using System.Threading.Tasks;
using Blazored.Toast.Services;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using Wabbajack.App.Blazor.State;
namespace Wabbajack.App.Blazor.Pages;
public partial class Configure
{
[Inject] private ILogger<Configure> Logger { get; set; } = default!;
[Inject] private IStateContainer StateContainer { get; set; } = default!;
[Inject] private DTOSerializer DTOs { get; set; } = default!;
[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; }
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 _shouldRender;
protected override bool ShouldRender() => _shouldRender;
protected override async Task OnInitializedAsync()
{
await LoadModlist();
_shouldRender = true;
}
private async Task LoadModlist()
{
try
{
if (ModlistPath == AbsolutePath.Empty) throw new FileNotFoundException("Modlist path was empty.");
var modlist = await StandardInstaller.LoadFromFile(DTOs, ModlistPath);
StateContainer.Modlist = modlist;
}
catch (Exception e)
{
toastService.ShowError("Could not load modlist!");
Logger.LogError(e, "Exception loading Modlist file {Name}", ModlistPath);
NavigationManager.NavigateTo(Select.Route);
return;
}
try
{
var hex = (await ModlistPath.ToString().Hash()).ToHex();
var prevSettings = await SettingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
if (prevSettings.ModlistLocation == ModlistPath)
{
StateContainer.ModlistPath = prevSettings.ModlistLocation;
StateContainer.InstallPath = prevSettings.InstallLocation;
StateContainer.DownloadPath = prevSettings.DownloadLocation;
}
}
catch (Exception e)
{
Logger.LogWarning(e, "Exception loading previous settings for {Name}", ModlistPath);
}
try
{
var imageStream = await StandardInstaller.ModListImageStream(ModlistPath);
var dotnetImageStream = new DotNetStreamReference(imageStream);
StateContainer.ModlistImage = await JSRuntime.InvokeAsync<string>("getBlobUrlFromStream", dotnetImageStream);
}
catch (Exception e)
{
toastService.ShowWarning("Could not load modlist image.");
Logger.LogWarning(e, "Exception loading modlist image for {Name}", ModlistPath);
}
}
private async Task SelectInstallFolder()
{
try
{
var installPath = await Dialog.ShowDialogNonBlocking(true);
if (installPath is not null) StateContainer.InstallPath = (AbsolutePath) installPath;
}
catch (Exception e)
{
Logger.LogError(e, "Exception selecting install folder");
}
}
private async Task SelectDownloadFolder()
{
try
{
var downloadPath = await Dialog.ShowDialogNonBlocking(true);
if (downloadPath is not null) StateContainer.DownloadPath = (AbsolutePath) downloadPath;
}
catch (Exception e)
{
Logger.LogError(e, "Exception selecting download folder");
}
}
private void Install()
{
NavigationManager.NavigateTo(Installing.Route);
}
}
internal class SavedInstallSettings
{
public AbsolutePath ModlistLocation { get; set; } = AbsolutePath.Empty;
public AbsolutePath InstallLocation { get; set; } = AbsolutePath.Empty;
public AbsolutePath DownloadLocation { get; set; } = AbsolutePath.Empty;
// public ModlistMetadata Metadata { get; set; }
}

View File

@ -1,4 +1,4 @@
@import "../Shared/Globals.scss";
@import "../../Shared/Globals.scss";
$checkbox-background: rgba(255, 255, 255, 0.2);
$checkbox-background-hover: darkgrey;

View File

@ -0,0 +1,33 @@
@page "/install/installing"
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
<div class="install-background">
<img id="background-image" src="@ModlistImage" alt=""/>
</div>
<div class="list">
@if (Modlist is not null)
{
<div class="left-side">
<InfoBlock Supertitle="@StatusCategory" Title="@Modlist.Name"/>
<div class="step-logger">
@foreach (var step in StatusStep.Take(3))
{
<div class="step">@step</div>
}
</div>
</div>
<div class="right-side">
<InfoImage Image="@ModlistImage" Title="Some Mod Title" Subtitle="Author and others" Description="This mod adds something cool but I'm not going to tell you anything."/>
</div>
}
</div>
<div class="logger-container">
<VirtualLogger/>
</div>
</div>
@code {
public const string Route = "/install/installing";
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Microsoft.AspNetCore.Components;
using Wabbajack.DTOs;
@ -16,7 +17,7 @@ using Wabbajack.App.Blazor.State;
namespace Wabbajack.App.Blazor.Pages;
public partial class Configure
public partial class Installing
{
[Inject] private ILogger<Configure> Logger { get; set; } = default!;
[Inject] private IStateContainer StateContainer { get; set; } = default!;
@ -26,14 +27,19 @@ public partial class Configure
[Inject] private IGameLocator GameLocator { get; set; } = default!;
[Inject] private SettingsManager SettingsManager { get; set; } = default!;
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
private ModList? Modlist => StateContainer.Modlist;
private string ModlistImage => StateContainer.ModlistImage;
private AbsolutePath ModlistPath => StateContainer.ModlistPath;
private AbsolutePath InstallPath { get; set; }
private AbsolutePath DownloadPath { get; set; }
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 string StatusText { get; set; } = string.Empty;
private InstallState InstallState => StateContainer.InstallState;
private const string InstallSettingsPrefix = "install-settings-";
@ -41,69 +47,16 @@ public partial class Configure
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
protected override async Task OnInitializedAsync()
protected override void OnInitialized()
{
// var Location = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", machineURL).WithExtension(Ext.Wabbajack);
await CheckValidInstallPath();
Install();
_shouldRender = true;
}
private async Task CheckValidInstallPath()
{
if (ModlistPath == AbsolutePath.Empty) return;
var modlist = await StandardInstaller.LoadFromFile(DTOs, ModlistPath);
StateContainer.Modlist = modlist;
var hex = (await ModlistPath.ToString().Hash()).ToHex();
var prevSettings = await SettingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
if (prevSettings.ModlistLocation == ModlistPath)
{
StateContainer.ModlistPath = prevSettings.ModlistLocation;
InstallPath = prevSettings.InstallLocation;
DownloadPath = prevSettings.DownloadLocation;
//ModlistMetadata = metadata ?? prevSettings.Metadata;
}
// see https://docs.microsoft.com/en-us/aspnet/core/blazor/images?view=aspnetcore-6.0#streaming-examples
var imageStream = await StandardInstaller.ModListImageStream(ModlistPath);
var dotnetImageStream = new DotNetStreamReference(imageStream);
// setImageUsingStreaming accepts the img id and the data stream
await JSRuntime.InvokeVoidAsync("setImageUsingStreaming", "background-image", dotnetImageStream);
}
private async void SelectInstallFolder()
{
try
{
var installPath = await Dialog.ShowDialogNonBlocking(true);
if (installPath is not null) InstallPath = (AbsolutePath)installPath;
}
catch (Exception e)
{
Logger.LogError(e, "Exception selecting install folder");
}
}
private async void SelectDownloadFolder()
{
try
{
var downloadPath = await Dialog.ShowDialogNonBlocking(true);
if (downloadPath is not null) DownloadPath = (AbsolutePath)downloadPath;
}
catch (Exception e)
{
Logger.LogError(e, "Exception selecting download folder");
}
}
private async Task Install()
{
if (Modlist is null) return;
StateContainer.InstallState = InstallState.Installing;
await Task.Run(() => BeginInstall(Modlist));
}
@ -130,12 +83,14 @@ public partial class Configure
SystemParameters = ParametersConstructor.Create(),
GameFolder = GameLocator.GameLocation(modlist.GameType)
});
installer.OnStatusUpdate = update =>
{
var (statusText, _, _) = update;
if (StatusText == statusText) return;
StatusText = statusText;
if (LastStatus == update.StatusText) return;
StatusStep.Insert(0, update.StatusText);
StatusCategory = update.StatusCategory;
LastStatus = update.StatusText;
InvokeAsync(StateHasChanged);
};
await installer.Begin(CancellationToken.None);
@ -148,11 +103,3 @@ public partial class Configure
}
}
}
internal class SavedInstallSettings
{
public AbsolutePath ModlistLocation { get; set; } = AbsolutePath.Empty;
public AbsolutePath InstallLocation { get; set; } = AbsolutePath.Empty;
public AbsolutePath DownloadLocation { get; set; } = AbsolutePath.Empty;
// public ModlistMetadata Metadata { get; set; }
}

View File

@ -0,0 +1,149 @@
@import "../../Shared/Globals.scss";
$checkbox-background: rgba(255, 255, 255, 0.2);
$checkbox-background-hover: darkgrey;
$checkbox-background-checked: $accent-color;
$checkbox-size: 0.75rem;
@mixin path-span {
display: block;
height: 2rem;
padding: 0.25rem;
margin: 0.25rem;
white-space: pre;
cursor: pointer;
text-overflow: ellipsis;
overflow: hidden;
}
#content {
display: flex;
height: 100%;
align-content: center;
justify-content: space-around;
align-items: center;
color: white;
flex-direction: column;
.install-background {
position: absolute;
width: calc(100% - #{$sidebar-width});
height: calc(100% - #{$header-height});
filter: blur(25px) brightness(50%);
z-index: -1;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.list {
display: flex;
flex: 1;
overflow: hidden;
align-items: center;
.left-side, .right-side {
flex: 1;
margin: 1rem;
.step-logger {
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: center;
align-content: center;
.step {
&:nth-child(1) {
margin-left: 0.5rem;
font-size: 2rem;
font-weight: 100;
color: rgba(255, 255, 255, 0.9);
}
&:nth-child(2) {
margin-left: 0.75rem;
font-size: 1.85rem;
font-weight: 100;
color: rgba(255, 255, 255, 0.6);
filter: blur(1px);
}
&:nth-child(3) {
margin-left: 1rem;
font-size: 1.7rem;
font-weight: 100;
color: rgba(255, 255, 255, 0.4);
filter: blur(1.5px);
}
}
}
}
}
.logger-container {
height: 200px;
width: 100%;
padding: 0.5rem;
background: rgba(0, 0, 0, 0.2);
color: lightgrey;
border: solid 1px black;
}
.settings {
font-size: 0.85rem;
display: flex;
align-items: center;
width: 100%;
padding: 1rem;
backdrop-filter: brightness(0.5);
.locations {
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
.labels {
span {
@include path-span;
}
}
.paths {
flex: 1;
margin-left: 1rem;
overflow: hidden;
span {
@include path-span;
border: solid 1px rgba(255, 255, 255, 0.3);
}
}
}
.options {
display: flex;
flex-flow: row wrap;
flex-direction: column;
margin-left: 2rem;
}
.install {
display: flex;
flex-direction: column;
align-items: center;
margin: 0.5rem;
cursor: pointer;
img {
width: 5rem;
height: 5rem;
}
}
}
}

View File

@ -1,4 +1,4 @@
@page "/install"
@page "/install/select"
@namespace Wabbajack.App.Blazor.Pages
@ -26,5 +26,5 @@
</div>
@code {
public const string Route = "/install";
public const string Route = "/install/select";
}

View File

@ -6,7 +6,7 @@ using Wabbajack.Paths;
namespace Wabbajack.App.Blazor.Pages;
public partial class Install
public partial class Select
{
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IStateContainer StateContainer { get; set; } = default!;
@ -23,6 +23,4 @@ public partial class Install
NavigationManager.NavigateTo(Configure.Route);
}
private void VerifyFile(AbsolutePath path) { }
}

View File

@ -1,7 +1,8 @@
@inherits LayoutComponentBase
@using Blazored.Toast.Configuration
@inherits LayoutComponentBase
@namespace Wabbajack.App.Blazor.Shared
<BlazoredToasts Position="ToastPosition.BottomRight"/>
<div id="background"></div>
<SideBar/>
<div id="wrapper">

View File

@ -17,9 +17,18 @@ public interface IStateContainer
IObservable<AbsolutePath> ModlistPathObservable { get; }
AbsolutePath ModlistPath { get; set; }
IObservable<AbsolutePath> InstallPathObservable { get; }
AbsolutePath InstallPath { get; set; }
IObservable<AbsolutePath> DownloadPathObservable { get; }
AbsolutePath DownloadPath { get; set; }
IObservable<ModList?> ModlistObservable { get; }
ModList? Modlist { get; set; }
IObservable<string?> ModlistImageObservable { get; }
string? ModlistImage { get; set; }
IObservable<InstallState> InstallStateObservable { get; }
InstallState InstallState { get; set; }

View File

@ -55,6 +55,22 @@ public class StateContainer : IStateContainer
set => _modlistPathObservable.Value = value;
}
private readonly CustomObservable<AbsolutePath> _installPathObservable = new(AbsolutePath.Empty);
public IObservable<AbsolutePath> InstallPathObservable => _installPathObservable;
public AbsolutePath InstallPath
{
get => _installPathObservable.Value;
set => _installPathObservable.Value = value;
}
private readonly CustomObservable<AbsolutePath> _downloadPathObservable = new(AbsolutePath.Empty);
public IObservable<AbsolutePath> DownloadPathObservable => _downloadPathObservable;
public AbsolutePath DownloadPath
{
get => _downloadPathObservable.Value;
set => _downloadPathObservable.Value = value;
}
private readonly CustomObservable<ModList?> _modlistObservable = new(null);
public IObservable<ModList?> ModlistObservable => _modlistObservable;
public ModList? Modlist
@ -62,6 +78,14 @@ public class StateContainer : IStateContainer
get => _modlistObservable.Value;
set => _modlistObservable.Value = value;
}
private readonly CustomObservable<string?> _modlistImageObservable = new(string.Empty);
public IObservable<string?> ModlistImageObservable => _modlistImageObservable;
public string? ModlistImage
{
get => _modlistImageObservable.Value;
set => _modlistImageObservable.Value = value;
}
private readonly CustomObservable<InstallState> _installStateObservable = new(InstallState.Waiting);
public IObservable<InstallState> InstallStateObservable => _installStateObservable;

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<OutputType>WinExe</OutputType>
@ -14,6 +14,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazored.Modal" Version="6.0.1" />
<PackageReference Include="Blazored.Toast" Version="3.2.2" />
<PackageReference Include="DynamicData" Version="7.4.9" />
<PackageReference Include="GitInfo" Version="2.2.0" />
<PackageReference Include="Microsoft-WindowsAPICodePack-Shell" Version="1.1.4" />

View File

@ -5,4 +5,8 @@
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Wabbajack.App.Blazor.Components
@using Wabbajack.App.Blazor.Components
@using Blazored.Modal
@using Blazored.Modal.Services
@using Blazored.Toast
@using Blazored.Toast.Services

View File

@ -7,6 +7,8 @@
<title>Wabbajack</title>
<base href="/"/>
<link href="Wabbajack.App.Blazor.styles.css" rel="stylesheet" />
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
<link href="_content/Blazored.Toast/blazored-toast.css" rel="stylesheet" />
</head>
<style>
@ -32,11 +34,12 @@
<div id="app"></div>
<script src="_framework/blazor.webview.js"></script>
<script src="_content/Blazored.Modal/blazored.modal.js"></script>
<script>
async function setImageUsingStreaming(imageElementId, imageStream) {
async function getBlobUrlFromStream(imageStream) {
const arrayBuffer = await imageStream.arrayBuffer();
const blob = new Blob([arrayBuffer]);
document.getElementById(imageElementId).src = URL.createObjectURL(blob);
return URL.createObjectURL(blob);
}
</script>
</body>

View File

@ -51,6 +51,7 @@ public abstract class ACompiler
public ConcurrentDictionary<Directive, RawSourceFile> _sourceFileLinks;
private string _statusText;
private string _statusCategory;
public List<IndexedArchive> IndexedArchives = new();
public Dictionary<Hash, IEnumerable<VirtualFile>> IndexedFiles = new();
@ -106,16 +107,17 @@ public abstract class ACompiler
public event EventHandler<StatusUpdate> OnStatusUpdate;
public void NextStep(string statusText, long maxStepProgress = 1)
public void NextStep(string statusCategory, string statusText, long maxStepProgress = 1)
{
_updateStopWatch.Restart();
_maxStepProgress = maxStepProgress;
_currentStep += 1;
_statusText = statusText;
_statusCategory = statusCategory;
_logger.LogInformation("Compiler Step: {Step}", statusText);
if (OnStatusUpdate != null)
OnStatusUpdate(this, new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
OnStatusUpdate(this, new StatusUpdate(statusCategory, $"[{_currentStep}/{MaxSteps}] " + statusText,
Percent.FactoryPutInRange(_currentStep, MaxSteps),
Percent.Zero));
}
@ -131,7 +133,7 @@ public abstract class ACompiler
}
if (OnStatusUpdate != null)
OnStatusUpdate(this, new StatusUpdate(_statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
OnStatusUpdate(this, new StatusUpdate(_statusCategory, _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
Percent.FactoryPutInRange(_currentStepProgress, _maxStepProgress)));
}
@ -195,7 +197,7 @@ public abstract class ACompiler
public async Task<bool> GatherMetaData()
{
_logger.LogInformation("Getting meta data for {count} archives", SelectedArchives.Count);
NextStep("Gathering Metadata", SelectedArchives.Count);
NextStep("Building", "Gathering Metadata", SelectedArchives.Count);
await SelectedArchives.PDoAll(CompilerLimiter, async a =>
{
UpdateProgress(1);
@ -208,7 +210,7 @@ public abstract class ACompiler
protected async Task IndexGameFileHashes()
{
NextStep("Indexing Game Files");
NextStep("Compiling", "Indexing Game Files");
if (_settings.UseGamePaths)
{
//taking the games in Settings.IncludedGames + currently compiling game so you can eg
@ -258,7 +260,7 @@ public abstract class ACompiler
protected async Task CleanInvalidArchivesAndFillState()
{
NextStep("Cleaning Invalid Archives");
NextStep("Compiling", "Cleaning Invalid Archives");
var remove = await IndexedArchives.PMapAll(CompilerLimiter, async a =>
{
try
@ -313,7 +315,7 @@ public abstract class ACompiler
.Where(f => f.FileExists())
.ToList();
NextStep("InferMetas", toFind.Count);
NextStep("Initializing", "InferMetas", toFind.Count);
if (toFind.Count == 0) return;
_logger.LogInformation("Attempting to infer {count} metas from the server.", toFind.Count);
@ -353,7 +355,7 @@ public abstract class ACompiler
protected async Task ExportModList(CancellationToken token)
{
NextStep("Exporting Modlist");
NextStep("Finalizing", "Exporting Modlist");
_logger.LogInformation("Exporting ModList to {location}", _settings.OutputFile);
// Modify readme and ModList image to relative paths if they exist
@ -431,7 +433,7 @@ public abstract class ACompiler
}))
.ToArray();
NextStep("Generating Patches", toBuild.Length);
NextStep("Compiling","Generating Patches", toBuild.Length);
if (toBuild.Length == 0) return;
// Extract all the source files
@ -512,7 +514,7 @@ public abstract class ACompiler
public async Task GenerateManifest()
{
NextStep("Generating Manifest");
NextStep("Finalizing", "Generating Manifest");
var manifest = new Manifest(ModList);
await using var of = _settings.OutputFile.Open(FileMode.Create, FileAccess.Write);
await _dtos.Serialize(manifest, of);
@ -520,7 +522,7 @@ public abstract class ACompiler
public async Task GatherArchives()
{
NextStep("Gathering Archives");
NextStep("Building", "Gathering Archives");
_logger.LogInformation("Building a list of archives based on the files required");
var hashes = InstallDirectives.OfType<FromArchive>()
@ -621,7 +623,7 @@ public abstract class ACompiler
.GroupBy(f => _sourceFileLinks[f].File)
.ToDictionary(k => k.Key);
NextStep("Inlining Files");
NextStep("Building", "Inlining Files");
if (grouped.Count == 0) return;
await _vfs.Extract(grouped.Keys.ToHashSet(), async (vf, sfn) =>
{

View File

@ -59,12 +59,12 @@ public class MO2Compiler : ACompiler
var roots = new List<AbsolutePath> {Settings.Source, Settings.Downloads};
roots.AddRange(Settings.OtherGames.Append(Settings.Game).Select(g => _locator.GameLocation(g)));
NextStep("Add Roots");
NextStep("Initializing", "Add Roots");
await _vfs.AddRoots(roots, token); // Step 1
await InferMetas(token); // Step 2
NextStep("Add Download Roots");
NextStep("Initializing", "Add Download Roots");
await _vfs.AddRoot(Settings.Downloads, token); // Step 3
// Find all Downloads
@ -125,14 +125,14 @@ public class MO2Compiler : ACompiler
var stack = MakeStack();
NextStep("Running Compilation Stack", AllFiles.Count);
NextStep("Compiling", "Running Compilation Stack", AllFiles.Count);
var results = await AllFiles.PMapAll(CompilerLimiter, f =>
{
UpdateProgress(1);
return RunStack(stack, f);
}).ToList();
NextStep("Updating Extra files");
NextStep("Compiling", "Updating Extra files");
// Add the extra files that were generated by the stack
results = results.Concat(ExtraFiles).ToList();
@ -184,7 +184,7 @@ public class MO2Compiler : ACompiler
private async Task RunValidation(ModList modList)
{
NextStep("Validating Archives", modList.Archives.Length);
NextStep("Finalizing", "Validating Archives", modList.Archives.Length);
var allowList = await _wjClient.LoadDownloadAllowList();
foreach (var archive in modList.Archives)
{

View File

@ -26,7 +26,7 @@ using Wabbajack.VFS;
namespace Wabbajack.Installer;
public record StatusUpdate(string StatusText, Percent StepsProgress, Percent StepProgress)
public record StatusUpdate(string StatusCategory, string StatusText, Percent StepsProgress, Percent StepProgress)
{
}
@ -57,6 +57,7 @@ public abstract class AInstaller<T>
protected long MaxStepProgress { get; set; }
private string _statusCategory;
private string _statusText;
private readonly Stopwatch _updateStopWatch = new();
@ -92,15 +93,16 @@ public abstract class AInstaller<T>
public ModList ModList => _configuration.ModList;
public void NextStep(string statusText, long maxStepProgress)
public void NextStep(string statusCategory, string statusText, long maxStepProgress)
{
_updateStopWatch.Restart();
MaxStepProgress = maxStepProgress;
_currentStep += 1;
_statusText = statusText;
_statusCategory = statusCategory;
_logger.LogInformation("Next Step: {Step}", statusText);
OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + statusText,
OnStatusUpdate?.Invoke(new StatusUpdate(statusCategory, statusText,
Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.Zero));
}
@ -108,8 +110,7 @@ public abstract class AInstaller<T>
{
Interlocked.Add(ref _currentStepProgress, stepProgress);
OnStatusUpdate?.Invoke(new StatusUpdate($"[{_currentStep}/{MaxSteps}] " + _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps),
Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
OnStatusUpdate?.Invoke(new StatusUpdate(_statusCategory, _statusText, Percent.FactoryPutInRange(_currentStep, MaxSteps), Percent.FactoryPutInRange(_currentStepProgress, MaxStepProgress)));
}
public abstract Task<bool> Begin(CancellationToken token);
@ -119,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("Extracting Modlist", archive.Entries.Count);
NextStep("Preparing","Extracting Modlist", archive.Entries.Count);
foreach (var entry in archive.Entries)
{
var path = entry.FullName.ToRelativePath().RelativeTo(ExtractedModlistFolder);
@ -181,7 +182,7 @@ public abstract class AInstaller<T>
/// </summary>
protected async Task PrimeVFS()
{
NextStep("Priming VFS", 0);
NextStep("Preparing","Priming VFS", 0);
_vfs.AddKnown(_configuration.ModList.Directives.OfType<FromArchive>().Select(d => d.ArchiveHashPath),
HashedArchives);
await _vfs.BackfillMissing();
@ -189,7 +190,7 @@ public abstract class AInstaller<T>
public async Task BuildFolderStructure()
{
NextStep("Building Folder Structure", 0);
NextStep("Preparing", "Building Folder Structure", 0);
_logger.LogInformation("Building Folder Structure");
ModList.Directives
.Where(d => d.To.Depth > 1)
@ -200,7 +201,7 @@ public abstract class AInstaller<T>
public async Task InstallArchives(CancellationToken token)
{
NextStep("Installing files", ModList.Directives.Sum(d => d.Size));
NextStep("Installing", "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,7 +303,7 @@ public abstract class AInstaller<T>
}
_logger.LogInformation("Downloading {count} archives", missing.Count);
NextStep("Downloading files", missing.Count);
NextStep("Downloading", "Downloading files", missing.Count);
await missing
.OrderBy(a => a.Size)
@ -363,7 +364,7 @@ public abstract class AInstaller<T>
public async Task HashArchives(CancellationToken token)
{
NextStep("Hashing Archives", 0);
NextStep("Hashing", "Hashing Archives", 0);
_logger.LogInformation("Looking for files to hash");
var allFiles = _configuration.Downloads.EnumerateFiles()
@ -414,7 +415,7 @@ public abstract class AInstaller<T>
var savePath = (RelativePath) "saves";
var existingFiles = _configuration.Install.EnumerateFiles().ToList();
NextStep("Optimizing Modlist: Looking for files to delete", existingFiles.Count);
NextStep("Preparing", "Looking for files to delete", existingFiles.Count);
await existingFiles
.PDoAll(async f =>
{
@ -428,12 +429,12 @@ public abstract class AInstaller<T>
if (NoDeleteRegex.IsMatch(f.ToString()))
return;
_logger.LogInformation("Deleting {relativeTo} it's not part of this ModList", relativeTo);
_logger.LogTrace("Deleting {relativeTo} it's not part of this ModList", relativeTo);
f.Delete();
});
_logger.LogInformation("Cleaning empty folders");
NextStep("Optimizing Modlist: Cleaning empty folders", indexed.Keys.Count);
NextStep("Preparing", "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
@ -468,7 +469,7 @@ public abstract class AInstaller<T>
var existingfiles = _configuration.Install.EnumerateFiles().ToHashSet();
NextStep("Optimizing Modlist: Removing redundant directives", indexed.Count);
NextStep("Preparing", "Removing redundant directives", indexed.Count);
await indexed.Values.PMapAll<Directive, Directive?>(async d =>
{
// Bit backwards, but we want to return null for
@ -487,7 +488,7 @@ public abstract class AInstaller<T>
_logger.LogInformation("Optimized {optimized} directives to {indexed} required", ModList.Directives.Length,
indexed.Count);
NextStep("Finalizing modlist optimization", 0);
NextStep("Preparing", "Finalizing modlist optimization", 0);
var requiredArchives = indexed.Values.OfType<FromArchive>()
.GroupBy(d => d.ArchiveHashPath.Hash)
.Select(d => d.Key)

View File

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using IniParser;
@ -60,7 +61,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
{
if (token.IsCancellationRequested) return false;
await _wjClient.SendMetric(MetricNames.BeginInstall, ModList.Name);
NextStep("Configuring Installer", 0);
NextStep("Preparing", "Configuring Installer", 0);
_logger.LogInformation("Configuring Processor");
if (_configuration.GameFolder == default)
@ -144,7 +145,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
await ExtractedModlistFolder!.DisposeAsync();
await _wjClient.SendMetric(MetricNames.FinishInstall, ModList.Name);
NextStep("Finished", 1);
NextStep("Finished", "Finished", 1);
_logger.LogInformation("Finished Installation");
return true;
}
@ -274,7 +275,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
private async Task InstallIncludedFiles(CancellationToken token)
{
_logger.LogInformation("Writing inline files");
NextStep("Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
NextStep("Installing", "Installing Included Files", ModList.Directives.OfType<InlineFile>().Count());
await ModList.Directives
.OfType<InlineFile>()
.PDoAll(async directive =>
@ -301,6 +302,7 @@ public class StandardInstaller : AInstaller<StandardInstaller>
_logger.LogWarning("No SystemParameters set, ignoring ini settings for system parameters");
var config = new IniParserConfiguration {AllowDuplicateKeys = true, AllowDuplicateSections = true};
config.CommentRegex = new Regex(@"^(#|;)(.*)");
var oblivionPath = (RelativePath) "Oblivion.ini";
foreach (var file in _configuration.Install.Combine("profiles").EnumerateFiles()
.Where(f => ((string) f.FileName).EndsWith("refs.ini") || f.FileName == oblivionPath))
@ -327,8 +329,9 @@ public class StandardInstaller : AInstaller<StandardInstaller>
modified = true;
}
if (modified)
parser.WriteFile(file.ToString(), data);
if (!modified) continue;
parser.WriteFile(file.ToString(), data);
_logger.LogTrace("Remapped screen size in {file}", file);
}
catch (Exception ex)
{