This commit is contained in:
erri120 2022-01-21 14:41:37 +01:00
parent 8782aeca94
commit 92a2195ccf
No known key found for this signature in database
GPG Key ID: 7FA9556C936B847C
30 changed files with 548 additions and 449 deletions

View File

@ -6,18 +6,9 @@ insert_final_newline = true
# C# and Razor files
[*.{cs,razor}]
# CS8602: Dereference of a possibly null reference.
# CS8618: Non-nullable member is uninitialized.
# Reason: The compiler/IDE doesn't quite understand Dependency Injection yet.
dotnet_diagnostic.CS8602.severity = none
dotnet_diagnostic.CS8618.severity = none
# RZ10012: Markup element with unexpected name.
# Reason: The component namespace is added to the global _Imports.razor file.
dotnet_diagnostic.RZ10012.severity = none
dotnet_sort_system_directives_first = true
[*.scss]
indent_size = 2

View File

@ -14,16 +14,17 @@ namespace Wabbajack.App.Blazor;
public partial class App
{
private readonly IServiceProvider _serviceProvider;
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder(Array.Empty<string>())
.ConfigureLogging(c => { c.ClearProviders(); })
.ConfigureServices((host, services) => { ConfigureServices(services); })
.Build();
_serviceProvider = _host.Services;
_serviceProvider = Host.CreateDefaultBuilder(Array.Empty<string>())
.ConfigureLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
})
.ConfigureServices(services => ConfigureServices(services))
.Build()
.Services;
}
private static IServiceCollection ConfigureServices(IServiceCollection services)
@ -33,24 +34,18 @@ public partial class App
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
services.AddTransient<MainWindow>();
services.AddSingleton<SystemParametersConstructor>();
services.AddSingleton<GlobalState>();
services.AddSingleton(typeof(IStateContainer), typeof(StateContainer));
return services;
}
private void OnStartup(object sender, StartupEventArgs e)
{
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow!.Show();
mainWindow.Show();
}
private void OnExit(object sender, ExitEventArgs e)
{
Current.Shutdown();
// using (_host)
// {
// _host.StopAsync();
// }
//
// base.OnExit(e);
}
}

View File

@ -1,28 +0,0 @@
@using Wabbajack.DTOs
@namespace Wabbajack.App.Blazor.Components
<div class="item">
<div class="display">
<img src="@Metadata.Links.ImageUri" loading="lazy" class="image" alt="@Metadata.Title">
<div class="interaction">
@ChildContent
</div>
</div>
<div class="info">
<div class="title">@Metadata.Title</div>
<div class="author">@Metadata.Author</div>
<div class="description">@Metadata.Description</div>
</div>
<div class="tags"></div>
</div>
@code {
[Parameter]
public ModlistMetadata Metadata { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}

View File

@ -1,82 +0,0 @@
@import "../Shared/Globals.scss";
$display-height: 225px;
$hover-icon-size: 75px;
.item {
width: 400px;
height: 450px;
overflow: hidden;
margin: 0.5rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(7px);
-webkit-backdrop-filter: blur(7px);
border: 1px solid rgba(255, 255, 255, 0.31);
&:hover .display .image {
filter: blur(2px) brightness(70%);
}
&:hover .display .interaction {
opacity: 1;
}
.display {
position: relative;
height: $display-height;
display: flex;
justify-content: center;
align-items: center;
.image {
position: absolute;
width: 100%;
height: 100%;
object-fit: contain;
transition: all 250ms ease-in-out;
}
.interaction {
position: absolute;
opacity: 0;
transition: all 250ms ease-in-out;
::deep img {
width: $hover-icon-size;
height: $hover-icon-size;
margin: 0;
transition: all 150ms ease-in-out;
}
}
}
.info {
padding-bottom: 1rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
.title {
color: white;
font-weight: 100;
font-size: 2rem;
line-height: 2.5rem;
margin: 0;
}
.author {
color: lightgray;
font-size: 1rem;
}
.description {
color: grey;
font-size: 0.9rem;
}
}
.tags {
border-radius: 0.5rem;
}
}

View File

@ -4,6 +4,7 @@
@* TODO: [Low] Replace logo with SVG? *@
<img class="logo" src="images/Logo_Dark_Transparent.png" alt="Wabbajack Logo">
<div class="socials">
@* TODO: wrap social icons in a button and make it clickable *@
<img src="images/icons/patreon.svg" alt="">
<img src="images/icons/github.svg" alt="">
<img src="images/icons/discord.svg" alt="">

View File

@ -1,42 +1,36 @@
@using Wabbajack.App.Blazor.Pages
@using Wabbajack.App.Blazor.Shared
@using Wabbajack.App.Blazor.State
@inject NavigationManager _navigationManager
@inject IStateContainer _stateContainer
@namespace Wabbajack.App.Blazor.Components
@* TODO: [Low] Clean this up a bit. *@
<header id="top-bar">
<nav class="@(GlobalState.NavigationAllowed ? "" : "disallow")">
<nav class="@(_stateContainer.NavigationAllowed ? "" : "disallow")">
<ul>
<li>
<div class='item @CurrentPage("")' @onclick='() => Navigate("")'>Play</div>
</li>
<li>
<div class='item @CurrentPage("Gallery")' @onclick='() => Navigate("Gallery")'>Gallery</div>
</li>
<li>
<div class='item @CurrentPage("Install")' @onclick='() => Navigate("Install")'>Install</div>
</li>
<li>
<div class='item @CurrentPage("Create")' @onclick='() => Navigate("Create")'>Create</div>
</li>
@foreach (var (name, route) in Pages)
{
<li>
<div class="item @CurrentPage(route)" @onclick="@(() => Navigate(route))">@name</div>
</li>
}
</ul>
</nav>
<div class="settings">
<InteractionIcon Icon="images/icons/adjust.svg" Label="Settings" Size="100%" OnClick="@(() => Navigate("Settings"))"/>
<InteractionIcon Icon="images/icons/adjust.svg" Label="Settings" Size="100%" OnClick="@(() => Navigate(Settings.Route))"/>
</div>
</header>
@code {
[Inject]
NavigationManager _navigationManager { get; set; }
[Inject]
GlobalState GlobalState { get; set; }
[CascadingParameter]
protected MainLayout _mainLayout { get; set; }
private static readonly Dictionary<string, string> Pages = new()
{
{"Play", Play.Route},
{"Gallery", Gallery.Route},
{"Install", Install.Route},
{"Create", Create.Route}
};
private void Navigate(string page)
{
@ -45,14 +39,14 @@
protected override void OnInitialized()
{
_navigationManager.LocationChanged += (o, args) => StateHasChanged();
GlobalState.OnNavigationStateChange += StateHasChanged;
// TODO(erri120): update this
// _navigationManager.LocationChanged += (_, _) => StateHasChanged();
// _globalState.OnNavigationStateChange += StateHasChanged;
}
private string CurrentPage(string page)
{
string relativePath = _navigationManager.ToBaseRelativePath(_navigationManager.Uri).ToLower();
return page.ToLower() == relativePath ? "active" : "";
var relativePath = _navigationManager.ToBaseRelativePath(_navigationManager.Uri);
return page.Equals(relativePath, StringComparison.OrdinalIgnoreCase) ? "active" : string.Empty;
}
}

View File

@ -1,4 +1,5 @@
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Wabbajack.App.Blazor.Models;
using Wabbajack.App.Blazor.State;
@ -11,20 +12,20 @@ namespace Wabbajack.App.Blazor;
public partial class MainWindow
{
private readonly ILogger<MainWindow> _logger;
private readonly LoggerProvider _loggerProvider;
private readonly ILogger<MainWindow> _logger;
private readonly LoggerProvider _loggerProvider;
private readonly SystemParametersConstructor _systemParams;
private readonly GlobalState _globalState;
private readonly IStateContainer _stateContainer;
public MainWindow(ILogger<MainWindow> logger, IServiceProvider serviceProvider, LoggerProvider loggerProvider,
SystemParametersConstructor systemParams, GlobalState globalState)
SystemParametersConstructor systemParams, IStateContainer stateContainer)
{
_logger = logger;
_logger = logger;
_loggerProvider = loggerProvider;
_systemParams = systemParams;
_globalState = globalState;
_systemParams = systemParams;
_stateContainer = stateContainer;
_globalState.OnTaskBarStateChange += state =>
_stateContainer.TaskBarStateObservable.Subscribe(state =>
{
Dispatcher.InvokeAsync(() =>
{
@ -32,7 +33,7 @@ public partial class MainWindow
TaskBarItem.ProgressState = state.State;
TaskBarItem.ProgressValue = state.ProgressValue;
});
};
});
InitializeComponent();
BlazorWebView.Services = serviceProvider;

View File

@ -71,7 +71,7 @@ public class LoggerProvider : ILoggerProvider
private void LogToFile(ILogMessage logMessage)
{
string? line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}";
var line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}";
lock (_logStream)
{
_logStream.Write(line);

View File

@ -1,52 +1,50 @@
@page "/Configure"
@page "/configure"
@using Wabbajack.App.Blazor.State
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
<div class="install-background">
<img src="@Image" alt="">
<img id="background-image" src="" alt=""/>
</div>
<div class="list">
@* TODO: [High] Find a cleaner way to show/hide components based on state. *@
@* TODO: [Low] Split each "side" into their own components? *@
<div class="left-side">
@if (!string.IsNullOrEmpty(ModList.Name))
{
if (InstallState != GlobalState.InstallStateEnum.Installing)
@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"/>
<InfoBlock Title="@Modlist.Name" Subtitle="@Modlist.Author" Comment="@Modlist.Version.ToString()" Description="@Modlist.Description"/>
}
else if (InstallState == GlobalState.InstallStateEnum.Installing)
else
{
<InfoBlock Supertitle="Installing..." Title="@ModList.Name" Subtitle="@StatusText"/>
// TODO: [Low] Step logging.
<InfoBlock Supertitle="Installing..." Title="@Modlist.Name" Subtitle="@StatusText"/>
// TODO: [Low] Step logging
}
}
</div>
<div class="right-side">
@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>
<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 == GlobalState.InstallStateEnum.Installing)
@if (InstallState == InstallState.Installing)
{
<div class="logger-container">
<VirtualLogger Messages="_loggerProvider.Messages"/>
<VirtualLogger Messages="LoggerProvider.Messages"/>
</div>
}
@if (InstallState != GlobalState.InstallStateEnum.Installing)
else
{
<div class="settings">
<div class="locations">
@ -57,9 +55,9 @@
<span>Download Location</span>
</div>
<div class="paths">
<span class="modlist-file">@ModListPath</span>
<span class="install-location" @onclick="SelectInstallFolder">@InstallPath</span>
<span class="download-location" @onclick="SelectDownloadFolder">@DownloadPath</span>
<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>
@ -75,3 +73,7 @@
</div>
}
</div>
@code {
public const string Route = "/configure";
}

View File

@ -1,6 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.Components;
using Wabbajack.DTOs;
@ -12,6 +10,8 @@ using Wabbajack.Downloaders.GameFile;
using Wabbajack.Hashing.xxHash64;
using Wabbajack.Services.OSIntegrated;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using Wabbajack.App.Blazor.Models;
using Wabbajack.App.Blazor.State;
@ -19,71 +19,74 @@ namespace Wabbajack.App.Blazor.Pages;
public partial class Configure
{
[Inject] private NavigationManager NavigationManager { get; set; }
[Inject] private GlobalState GlobalState { get; set; }
[Inject] private DTOSerializer _dtos { get; set; }
[Inject] private IServiceProvider _serviceProvider { get; set; }
[Inject] private SystemParametersConstructor _parametersConstructor { get; set; }
[Inject] private IGameLocator _gameLocator { get; set; }
[Inject] private SettingsManager _settingsManager { get; set; }
[Inject] private LoggerProvider _loggerProvider { get; set; }
[Inject] private ILogger<Configure> Logger { get; set; } = default!;
[Inject] private IStateContainer StateContainer { get; set; } = default!;
[Inject] private DTOSerializer DTOs { get; set; } = default!;
[Inject] private IServiceProvider ServiceProvider { get; set; } = default!;
[Inject] private SystemParametersConstructor ParametersConstructor { get; set; } = default!;
[Inject] private IGameLocator GameLocator { get; set; } = default!;
[Inject] private SettingsManager SettingsManager { get; set; } = default!;
[Inject] private LoggerProvider LoggerProvider { get; set; } = default!;
[Inject] private JSRuntime JSRuntime { get; set; } = default!;
private ModList? Modlist => StateContainer.Modlist;
private string Image { get; set; }
private ModList ModList { get; set; } = new(); // Init a new modlist so we can listen for changes in Blazor components.
private AbsolutePath ModListPath { get; set; }
private AbsolutePath InstallPath { get; set; }
private AbsolutePath ModlistPath => StateContainer.ModlistPath;
private AbsolutePath InstallPath { get; set; }
private AbsolutePath DownloadPath { get; set; }
private string StatusText { get; set; }
public GlobalState.InstallStateEnum InstallState { get; set; }
private LoggerProvider.ILogMessage CurrentLog { get; set; }
private string StatusText { get; set; } = string.Empty;
private InstallState InstallState => StateContainer.InstallState;
// private LoggerProvider.ILogMessage CurrentLog { get; set; }
private const string InstallSettingsPrefix = "install-settings-";
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
protected override async Task OnInitializedAsync()
{
// var Location = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", machineURL).WithExtension(Ext.Wabbajack);
GlobalState.OnInstallStateChange += () => InstallState = GlobalState.InstallState;
await CheckValidInstallPath();
await base.OnInitializedAsync();
_shouldRender = true;
}
private async Task CheckValidInstallPath()
{
if (GlobalState.ModListPath == AbsolutePath.Empty) return;
if (ModlistPath == AbsolutePath.Empty) return;
var modlist = await StandardInstaller.LoadFromFile(DTOs, ModlistPath);
StateContainer.Modlist = modlist;
ModListPath = GlobalState.ModListPath;
ModList = await StandardInstaller.LoadFromFile(_dtos, ModListPath);
GlobalState.ModList = ModList;
var hex = (await ModlistPath.ToString().Hash()).ToHex();
var prevSettings = await SettingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
string hex = (await ModListPath.ToString().Hash()).ToHex();
var prevSettings = await _settingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
if (prevSettings.ModListLocation == ModListPath)
if (prevSettings.ModlistLocation == ModlistPath)
{
ModListPath = prevSettings.ModListLocation;
StateContainer.ModlistPath = prevSettings.ModlistLocation;
InstallPath = prevSettings.InstallLocation;
DownloadPath = prevSettings.DownloadLoadction;
DownloadPath = prevSettings.DownloadLocation;
//ModlistMetadata = metadata ?? prevSettings.Metadata;
}
Stream image = await StandardInstaller.ModListImageStream(ModListPath);
await using var reader = new MemoryStream();
await image.CopyToAsync(reader);
Image = $"data:image/png;base64,{Convert.ToBase64String(reader.ToArray())}";
// 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
{
AbsolutePath? thing = await Dialog.ShowDialogNonBlocking(true);
if (thing != null) InstallPath = (AbsolutePath)thing;
StateHasChanged();
var installPath = await Dialog.ShowDialogNonBlocking(true);
if (installPath is not null) InstallPath = (AbsolutePath)installPath;
}
catch (Exception ex)
catch (Exception e)
{
Debug.Print(ex.Message);
Logger.LogError(e, "Exception selecting install folder");
}
}
@ -91,68 +94,68 @@ public partial class Configure
{
try
{
AbsolutePath? thing = await Dialog.ShowDialogNonBlocking(true);
if (thing != null) DownloadPath = (AbsolutePath)thing;
StateHasChanged();
var downloadPath = await Dialog.ShowDialogNonBlocking(true);
if (downloadPath is not null) DownloadPath = (AbsolutePath)downloadPath;
}
catch (Exception ex)
catch (Exception e)
{
Debug.Print(ex.Message);
Logger.LogError(e, "Exception selecting download folder");
}
}
private async Task Install()
{
GlobalState.InstallState = GlobalState.InstallStateEnum.Installing;
await Task.Run(BeginInstall);
if (Modlist is null) return;
StateContainer.InstallState = InstallState.Installing;
await Task.Run(() => BeginInstall(Modlist));
}
private async Task BeginInstall()
private async Task BeginInstall(ModList modlist)
{
string postfix = (await ModListPath.ToString().Hash()).ToHex();
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
var postfix = (await ModlistPath.ToString().Hash()).ToHex();
await SettingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
{
ModListLocation = ModListPath,
InstallLocation = InstallPath,
DownloadLoadction = DownloadPath
ModlistLocation = ModlistPath,
InstallLocation = InstallPath,
DownloadLocation = DownloadPath
});
try
{
var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration
var installer = StandardInstaller.Create(ServiceProvider, new InstallerConfiguration
{
Game = ModList.GameType,
Downloads = DownloadPath,
Install = InstallPath,
ModList = ModList,
ModlistArchive = ModListPath,
SystemParameters = _parametersConstructor.Create(),
GameFolder = _gameLocator.GameLocation(ModList.GameType)
Game = modlist.GameType,
Downloads = DownloadPath,
Install = InstallPath,
ModList = modlist,
ModlistArchive = ModlistPath,
SystemParameters = ParametersConstructor.Create(),
GameFolder = GameLocator.GameLocation(modlist.GameType)
});
installer.OnStatusUpdate = update =>
{
if (StatusText != update.StatusText)
{
StatusText = update.StatusText;
InvokeAsync(StateHasChanged);
}
var (statusText, _, _) = update;
if (StatusText == statusText) return;
StatusText = statusText;
};
await installer.Begin(CancellationToken.None);
StateContainer.InstallState = InstallState.Success;
}
catch (Exception ex)
catch (Exception e)
{
Debug.Print(ex.Message);
Logger.LogError(e, "Exception installing Modlist");
StateContainer.InstallState = InstallState.Failure;
}
}
}
internal class SavedInstallSettings
{
public AbsolutePath ModListLocation { get; set; }
public AbsolutePath InstallLocation { get; set; }
public AbsolutePath DownloadLoadction { get; set; }
public ModlistMetadata Metadata { get; set; }
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 @@
@page "/Create"
@page "/create"
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
@ -6,3 +6,8 @@
</div>
</div>
@code
{
public const string Route = "/create";
}

View File

@ -1,24 +1,51 @@
@page "/Gallery"
@page "/gallery"
@using Wabbajack.DTOs
@using Wabbajack.RateLimiter
@using System.Globalization
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
@foreach (ModlistMetadata modlist in _listItems)
@if (_errorLoadingModlists)
{
<ModlistItem Metadata=@modlist>
<InteractionIcon Icon="images/icons/install.svg" Label="Install" Size="75px" OnClick="@(() => OnClickDownload(modlist))"/>
<InteractionIcon Icon="images/icons/info.svg" Label="Information" Size="75px" OnClick="@(() => OnClickInformation(modlist))"/>
</ModlistItem>
@* TODO: error *@
}
@if (DownloadProgress != Percent.Zero)
else if (!Modlists.Any())
{
<BottomBar Image="@DownloadingMetaData.Links.ImageUri" Title="Downloading..." Subtitle="@DownloadingMetaData.Title">
@* TODO: loading *@
}
else
{
@foreach (var modlist in Modlists)
{
<div @key="modlist.Title" class="item">
<div class="display">
<img src="@modlist.Links.ImageUri" loading="lazy" class="image" alt="@modlist.Title">
<div class="interaction">
<InteractionIcon Icon="images/icons/install.svg" Label="Install" Size="75px" OnClick="@(() => OnClickDownload(modlist))"/>
<InteractionIcon Icon="images/icons/info.svg" Label="Information" Size="75px" OnClick="@(() => OnClickInformation(modlist))"/>
</div>
</div>
<div class="info">
<div class="title">@modlist.Title</div>
<div class="author">@modlist.Author</div>
<div class="description">@modlist.Description</div>
</div>
<div class="tags"></div>
</div>
}
}
@if (_downloadProgress != Percent.Zero && _downloadingMetaData is not null)
{
<BottomBar Image="@_downloadingMetaData.Links.ImageUri" Title="Downloading..." Subtitle="@_downloadingMetaData.Title">
<div style="height:1.5rem;">
<ProgressBar Percentage="@DownloadProgress" Text="@DownloadProgress.Value.ToString()"/>
<ProgressBar Percentage="@_downloadProgress" Text="@_downloadProgress.Value.ToString(CultureInfo.InvariantCulture)"/>
</div>
</BottomBar>
}
</div>
@code {
public const string Route = "/gallery";
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Shell;
@ -20,33 +21,35 @@ namespace Wabbajack.App.Blazor.Pages;
public partial class Gallery
{
[Inject] private GlobalState GlobalState { get; set; }
[Inject] private NavigationManager NavigationManager { get; set; }
[Inject] private ILogger<Gallery> _logger { get; set; }
[Inject] private Client _client { get; set; }
[Inject] private ModListDownloadMaintainer _maintainer { get; set; }
[Inject] private ILogger<Gallery> Logger { get; set; } = default!;
[Inject] private IStateContainer StateContainer { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private ModListDownloadMaintainer Maintainer { get; set; } = default!;
public Percent DownloadProgress { get; set; } = Percent.Zero;
public ModlistMetadata DownloadingMetaData { get; set; } = new ModlistMetadata();
private Percent _downloadProgress = Percent.Zero;
private ModlistMetadata? _downloadingMetaData;
private List<ModlistMetadata> _listItems { get; set; } = new();
private IEnumerable<ModlistMetadata> Modlists => StateContainer.Modlists;
private bool _errorLoadingModlists;
private bool _shouldRender;
protected override bool ShouldRender() => _shouldRender;
protected override async Task OnInitializedAsync()
{
try
if (!StateContainer.Modlists.Any())
{
_logger.LogInformation("Getting modlists...");
ModlistMetadata[] modLists = await _client.LoadLists();
_listItems.AddRange(modLists.ToList());
StateHasChanged();
var res = await StateContainer.LoadModlistMetadata();
if (!res)
{
_errorLoadingModlists = true;
_shouldRender = true;
return;
}
}
catch (Exception ex)
{
//TODO: [Critical] Figure out why an exception is thrown on first navigation.
_logger.LogError(ex, "Error while loading lists");
}
await base.OnInitializedAsync();
_shouldRender = true;
}
private async void OnClickDownload(ModlistMetadata metadata)
@ -62,38 +65,42 @@ public partial class Gallery
private async Task Download(ModlistMetadata metadata)
{
GlobalState.NavigationAllowed = false;
DownloadingMetaData = metadata;
await using Timer timer = new(_ => InvokeAsync(StateHasChanged));
timer.Change(TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(250));
StateContainer.NavigationAllowed = false;
_downloadingMetaData = metadata;
try
{
(IObservable<Percent> progress, Task task) = _maintainer.DownloadModlist(metadata);
var (progress, task) = Maintainer.DownloadModlist(metadata);
GlobalState.SetTaskBarState(TaskbarItemProgressState.Indeterminate,$"Downloading {metadata.Title}");
var dispose = progress.Subscribe(p =>
var dispose = progress
.DistinctUntilChanged(p => p.Value)
.Throttle(TimeSpan.FromMilliseconds(100))
.Subscribe(p =>
{
DownloadProgress = p;
GlobalState.SetTaskBarState(TaskbarItemProgressState.Indeterminate,$"Downloading {metadata.Title}", p.Value);
});
_downloadProgress = p;
StateContainer.TaskBarState = new TaskBarState
{
Description = $"Downloading {metadata.Title}",
State = TaskbarItemProgressState.Indeterminate,
ProgressValue = p.Value
};
}, () => { StateContainer.TaskBarState = new TaskBarState(); });
await task;
//await _wjClient.SendMetric("downloading", Metadata.Title);
dispose.Dispose();
GlobalState.SetTaskBarState();
AbsolutePath path = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
GlobalState.ModListPath = path;
NavigationManager.NavigateTo("/Configure");
var path = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
StateContainer.ModlistPath = path;
NavigationManager.NavigateTo(Configure.Route);
}
catch (Exception e)
{
Debug.Print(e.Message);
Logger.LogError(e, "Exception downloading Modlist {Name}", metadata.Title);
}
finally
{
StateContainer.TaskBarState = new TaskBarState();
StateContainer.NavigationAllowed = true;
}
await timer.DisposeAsync();
GlobalState.NavigationAllowed = true;
}
}

View File

@ -1,6 +1,89 @@
#content {
@import "../Shared/Globals.scss";
#content {
width: 100%;
display: flex;
flex-flow: row wrap;
justify-content: space-evenly;
}
}
$display-height: 225px;
$hover-icon-size: 75px;
.item {
width: 400px;
height: 450px;
overflow: hidden;
margin: 0.5rem;
padding: 1rem;
background: rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(7px);
-webkit-backdrop-filter: blur(7px);
border: 1px solid rgba(255, 255, 255, 0.31);
&:hover .display .image {
filter: blur(2px) brightness(70%);
}
&:hover .display .interaction {
opacity: 1;
}
.display {
position: relative;
height: $display-height;
display: flex;
justify-content: center;
align-items: center;
.image {
position: absolute;
width: 100%;
height: 100%;
object-fit: contain;
transition: all 250ms ease-in-out;
}
.interaction {
position: absolute;
opacity: 0;
transition: all 250ms ease-in-out;
::deep img {
width: $hover-icon-size;
height: $hover-icon-size;
margin: 0;
transition: all 150ms ease-in-out;
}
}
}
.info {
padding-bottom: 1rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
.title {
color: white;
font-weight: 100;
font-size: 2rem;
line-height: 2.5rem;
margin: 0;
}
.author {
color: lightgray;
font-size: 1rem;
}
.description {
color: grey;
font-size: 0.9rem;
}
}
.tags {
border-radius: 0.5rem;
}
}

View File

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

View File

@ -8,8 +8,8 @@ namespace Wabbajack.App.Blazor.Pages;
public partial class Install
{
[Inject] private NavigationManager NavigationManager { get; set; }
[Inject] private GlobalState GlobalState { get; set; }
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private IStateContainer StateContainer { get; set; } = default!;
private void SelectFile()
{
@ -18,10 +18,10 @@ public partial class Install
dialog.Multiselect = false;
dialog.Filters.Add(new CommonFileDialogFilter("Wabbajack File", "*" + Ext.Wabbajack));
if (dialog.ShowDialog() != CommonFileDialogResult.Ok) return;
GlobalState.ModListPath = dialog.FileName.ToAbsolutePath();
StateContainer.ModlistPath = dialog.FileName.ToAbsolutePath();
}
NavigationManager.NavigateTo("/Configure");
NavigationManager.NavigateTo(Configure.Route);
}
private void VerifyFile(AbsolutePath path) { }

View File

@ -46,4 +46,4 @@
}
}
}
}
}

View File

@ -21,3 +21,7 @@
// }
// }
}
@code {
public const string Route = "/";
}

View File

@ -1,4 +1,4 @@
@page "/Settings"
@page "/settings"
@namespace Wabbajack.App.Blazor.Pages
<div id="content">
@ -6,3 +6,7 @@
</div>
</div>
@code {
public const string Route = "/settings";
}

View File

@ -13,7 +13,7 @@ public partial class Settings
{
try
{
ResourceSettingsManager.ResourceSetting resource = await _resourceSettingsManager.GetSettings("Downloads");
var resource = await _resourceSettingsManager.GetSettings("Downloads");
StateHasChanged();
}
catch (Exception ex) { }

View File

@ -1,90 +0,0 @@
using System;
using System.Windows.Shell;
using Wabbajack.DTOs;
using Wabbajack.Paths;
namespace Wabbajack.App.Blazor.State;
public class GlobalState
{
#region Navigation Allowed
private bool _navigationAllowed = true;
public event Action OnNavigationStateChange;
public bool NavigationAllowed
{
get => _navigationAllowed;
set
{
_navigationAllowed = value;
OnNavigationStateChange?.Invoke();
}
}
#endregion
#region Install
private InstallStateEnum _installState;
private AbsolutePath _modListPath;
private ModList _modList;
public event Action OnModListPathChange;
public event Action OnModListChange;
public event Action OnInstallStateChange;
public event Action<TaskBarState> OnTaskBarStateChange;
public void SetTaskBarState(TaskbarItemProgressState state = TaskbarItemProgressState.None, string description="", double progress = 0)
{
OnTaskBarStateChange?.Invoke(new TaskBarState
{
State = state,
ProgressValue = progress,
Description = description
});
}
public AbsolutePath ModListPath
{
get => _modListPath;
set
{
_modListPath = value;
OnModListPathChange?.Invoke();
}
}
public ModList ModList
{
get => _modList;
set
{
_modList = value;
OnModListChange?.Invoke();
}
}
public InstallStateEnum InstallState
{
get => _installState;
set
{
_installState = value;
OnInstallStateChange?.Invoke();
}
}
public enum InstallStateEnum
{
Waiting,
Configuration,
Installing,
Success,
Failure
}
#endregion
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Wabbajack.DTOs;
using Wabbajack.Paths;
namespace Wabbajack.App.Blazor.State;
public interface IStateContainer
{
IEnumerable<ModlistMetadata> Modlists { get; }
Task<bool> LoadModlistMetadata();
IObservable<bool> NavigationAllowedObservable { get; }
bool NavigationAllowed { get; set; }
IObservable<AbsolutePath> ModlistPathObservable { get; }
AbsolutePath ModlistPath { get; set; }
IObservable<ModList?> ModlistObservable { get; }
ModList? Modlist { get; set; }
IObservable<InstallState> InstallStateObservable { get; }
InstallState InstallState { get; set; }
IObservable<TaskBarState> TaskBarStateObservable { get; }
TaskBarState TaskBarState { get; set; }
}

View File

@ -0,0 +1,10 @@
namespace Wabbajack.App.Blazor.State;
public enum InstallState
{
Waiting,
Configuration,
Installing,
Success,
Failure
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Common;
using Wabbajack.DTOs;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
namespace Wabbajack.App.Blazor.State;
public class StateContainer : IStateContainer
{
private readonly ILogger<StateContainer> _logger;
private readonly Client _client;
public StateContainer(ILogger<StateContainer> logger, Client client)
{
_logger = logger;
_client = client;
}
private ModlistMetadata[] _modlists = Array.Empty<ModlistMetadata>();
public IEnumerable<ModlistMetadata> Modlists => _modlists;
public async Task<bool> LoadModlistMetadata()
{
try
{
var lists = await _client.LoadLists();
_modlists = lists;
return _modlists.Any();
}
catch (Exception e)
{
_logger.LogError(e, "Exception loading Modlists");
return false;
}
}
private readonly CustomObservable<bool> _navigationAllowedObservable = new(true);
public IObservable<bool> NavigationAllowedObservable => _navigationAllowedObservable;
public bool NavigationAllowed
{
get => _navigationAllowedObservable.Value;
set => _navigationAllowedObservable.Value = value;
}
private readonly CustomObservable<AbsolutePath> _modlistPathObservable = new(AbsolutePath.Empty);
public IObservable<AbsolutePath> ModlistPathObservable => _modlistPathObservable;
public AbsolutePath ModlistPath
{
get => _modlistPathObservable.Value;
set => _modlistPathObservable.Value = value;
}
private readonly CustomObservable<ModList?> _modlistObservable = new(null);
public IObservable<ModList?> ModlistObservable => _modlistObservable;
public ModList? Modlist
{
get => _modlistObservable.Value;
set => _modlistObservable.Value = value;
}
private readonly CustomObservable<InstallState> _installStateObservable = new(InstallState.Waiting);
public IObservable<InstallState> InstallStateObservable => _installStateObservable;
public InstallState InstallState
{
get => _installStateObservable.Value;
set => _installStateObservable.Value = value;
}
private readonly CustomObservable<TaskBarState> _taskBarStateObservable = new(new TaskBarState());
public IObservable<TaskBarState> TaskBarStateObservable => _taskBarStateObservable;
public TaskBarState TaskBarState
{
get => _taskBarStateObservable.Value;
set => _taskBarStateObservable.Value = value;
}
}

View File

@ -4,7 +4,7 @@ namespace Wabbajack.App.Blazor.State;
public class TaskBarState
{
public string Description { get; set; }
public string Description { get; set; } = string.Empty;
public double ProgressValue { get; set; }
public TaskbarItemProgressState State { get; set; }
public TaskbarItemProgressState State { get; set; } = TaskbarItemProgressState.None;
}

View File

@ -20,7 +20,7 @@ public static class Dialog
var dialog = new CommonOpenFileDialog();
dialog.IsFolderPicker = isFolderPicker;
dialog.Multiselect = false;
CommonFileDialogResult result = dialog.ShowDialog(newWindow);
var result = dialog.ShowDialog(newWindow);
return result == CommonFileDialogResult.Ok ? dialog.FileName : null;
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext())
.ContinueWith(result => result.Result?.ToAbsolutePath())

View File

@ -40,14 +40,14 @@ public class SystemParametersConstructor
SetProcessDPIAware();
unsafe
{
List<(int Width, int Height, bool IsPrimary)>? col = new List<(int Width, int Height, bool IsPrimary)>();
var col = new List<(int Width, int Height, bool IsPrimary)>();
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
delegate(IntPtr hMonitor, IntPtr hdcMonitor, RECT* lprcMonitor, void* dwData)
{
var mi = new MONITORINFOEX();
mi.cbSize = Marshal.SizeOf(mi);
bool success = GetMonitorInfo(hMonitor, (MONITORINFO*)&mi);
var success = GetMonitorInfo(hMonitor, (MONITORINFO*)&mi);
if (success)
col.Add((mi.Monitor.right - mi.Monitor.left, mi.Monitor.bottom - mi.Monitor.top,
mi.Flags == MONITORINFO_Flags.MONITORINFOF_PRIMARY));
@ -60,17 +60,17 @@ public class SystemParametersConstructor
public SystemParameters Create()
{
(int width, int height, _) = GetDisplays().First(d => d.IsPrimary);
(var width, var height, _) = GetDisplays().First(d => d.IsPrimary);
/*using var f = new SharpDX.DXGI.Factory1();
var video_memory = f.Adapters1.Select(a =>
Math.Max(a.Description.DedicatedSystemMemory, (long)a.Description.DedicatedVideoMemory)).Max();*/
ulong dxgiMemory = 0UL;
var dxgiMemory = 0UL;
unsafe
{
using DXGI? api = DXGI.GetApi();
using var api = DXGI.GetApi();
IDXGIFactory1* factory1 = default;
@ -79,15 +79,15 @@ public class SystemParametersConstructor
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-createdxgifactory1
SilkMarshal.ThrowHResult(api.CreateDXGIFactory1(SilkMarshal.GuidPtrOf<IDXGIFactory1>(), (void**)&factory1));
uint i = 0u;
var i = 0u;
while (true)
{
IDXGIAdapter1* adapter1 = default;
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgifactory1-enumadapters1
int res = factory1->EnumAdapters1(i, &adapter1);
var res = factory1->EnumAdapters1(i, &adapter1);
Exception? exception = Marshal.GetExceptionForHR(res);
var exception = Marshal.GetExceptionForHR(res);
if (exception != null) break;
AdapterDesc1 adapterDesc = default;
@ -95,10 +95,10 @@ public class SystemParametersConstructor
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiadapter1-getdesc1
SilkMarshal.ThrowHResult(adapter1->GetDesc1(&adapterDesc));
ulong systemMemory = (ulong)adapterDesc.DedicatedSystemMemory;
ulong videoMemory = (ulong)adapterDesc.DedicatedVideoMemory;
var systemMemory = (ulong)adapterDesc.DedicatedSystemMemory;
var videoMemory = (ulong)adapterDesc.DedicatedVideoMemory;
ulong maxMemory = Math.Max(systemMemory, videoMemory);
var maxMemory = Math.Max(systemMemory, videoMemory);
if (maxMemory > dxgiMemory)
dxgiMemory = maxMemory;
@ -117,7 +117,7 @@ public class SystemParametersConstructor
}
}
MEMORYSTATUSEX? memory = GetMemoryStatus();
var memory = GetMemoryStatus();
var p = new SystemParameters
{
ScreenWidth = width,

View File

@ -22,6 +22,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" />
<PackageReference Include="PInvoke.User32" Version="0.7.104" />
<PackageReference Include="Silk.NET.DXGI" Version="2.12.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
</ItemGroup>
<PropertyGroup>

View File

@ -32,6 +32,5 @@
<div id="app"></div>
<script src="_framework/blazor.webview.js"></script>
<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
</body>
</html>
</html>

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Reactive;
namespace Wabbajack.Common;
public class CustomObservable<T> : ObservableBase<T>
{
private readonly List<IObserver<T>> _observers = new();
private T _value;
public T Value
{
get => _value;
set
{
if (EqualityComparer<T>.Default.Equals(value, _value)) return;
_value = value;
foreach (var observer in _observers)
{
observer.OnNext(value);
}
}
}
public CustomObservable(T value)
{
_value = value;
}
protected override IDisposable SubscribeCore(IObserver<T> observer)
{
if (!_observers.Contains(observer))
{
_observers.Add(observer);
observer.OnNext(Value);
}
return new Unsubscriber<T>(_observers, observer);
}
}
internal sealed class Unsubscriber<T> : IDisposable
{
private readonly List<IObserver<T>> _observers;
private readonly IObserver<T> _observer;
public Unsubscriber(List<IObserver<T>> observers, IObserver<T> observer)
{
_observers = observers;
_observer = observer;
}
public void Dispose()
{
if (_observers.Contains(_observer)) _observers.Remove(_observer);
}
}