mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Update
This commit is contained in:
parent
8782aeca94
commit
92a2195ccf
@ -6,18 +6,9 @@ insert_final_newline = true
|
|||||||
|
|
||||||
# C# and Razor files
|
# C# and Razor files
|
||||||
[*.{cs,razor}]
|
[*.{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.
|
# RZ10012: Markup element with unexpected name.
|
||||||
# Reason: The component namespace is added to the global _Imports.razor file.
|
# Reason: The component namespace is added to the global _Imports.razor file.
|
||||||
dotnet_diagnostic.RZ10012.severity = none
|
dotnet_diagnostic.RZ10012.severity = none
|
||||||
|
|
||||||
dotnet_sort_system_directives_first = true
|
|
||||||
|
|
||||||
[*.scss]
|
[*.scss]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
@ -14,16 +14,17 @@ namespace Wabbajack.App.Blazor;
|
|||||||
public partial class App
|
public partial class App
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly IHost _host;
|
|
||||||
|
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
_host = Host.CreateDefaultBuilder(Array.Empty<string>())
|
_serviceProvider = Host.CreateDefaultBuilder(Array.Empty<string>())
|
||||||
.ConfigureLogging(c => { c.ClearProviders(); })
|
.ConfigureLogging(loggingBuilder =>
|
||||||
.ConfigureServices((host, services) => { ConfigureServices(services); })
|
{
|
||||||
.Build();
|
loggingBuilder.ClearProviders();
|
||||||
|
})
|
||||||
_serviceProvider = _host.Services;
|
.ConfigureServices(services => ConfigureServices(services))
|
||||||
|
.Build()
|
||||||
|
.Services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceCollection ConfigureServices(IServiceCollection services)
|
private static IServiceCollection ConfigureServices(IServiceCollection services)
|
||||||
@ -33,24 +34,18 @@ public partial class App
|
|||||||
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
|
services.AddAllSingleton<ILoggerProvider, LoggerProvider>();
|
||||||
services.AddTransient<MainWindow>();
|
services.AddTransient<MainWindow>();
|
||||||
services.AddSingleton<SystemParametersConstructor>();
|
services.AddSingleton<SystemParametersConstructor>();
|
||||||
services.AddSingleton<GlobalState>();
|
services.AddSingleton(typeof(IStateContainer), typeof(StateContainer));
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnStartup(object sender, StartupEventArgs e)
|
private void OnStartup(object sender, StartupEventArgs e)
|
||||||
{
|
{
|
||||||
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
|
||||||
mainWindow!.Show();
|
mainWindow.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExit(object sender, ExitEventArgs e)
|
private void OnExit(object sender, ExitEventArgs e)
|
||||||
{
|
{
|
||||||
Current.Shutdown();
|
Current.Shutdown();
|
||||||
// using (_host)
|
|
||||||
// {
|
|
||||||
// _host.StopAsync();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// base.OnExit(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,6 +4,7 @@
|
|||||||
@* TODO: [Low] Replace logo with SVG? *@
|
@* TODO: [Low] Replace logo with SVG? *@
|
||||||
<img class="logo" src="images/Logo_Dark_Transparent.png" alt="Wabbajack Logo">
|
<img class="logo" src="images/Logo_Dark_Transparent.png" alt="Wabbajack Logo">
|
||||||
<div class="socials">
|
<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/patreon.svg" alt="">
|
||||||
<img src="images/icons/github.svg" alt="">
|
<img src="images/icons/github.svg" alt="">
|
||||||
<img src="images/icons/discord.svg" alt="">
|
<img src="images/icons/discord.svg" alt="">
|
||||||
|
@ -1,42 +1,36 @@
|
|||||||
@using Wabbajack.App.Blazor.Pages
|
@using Wabbajack.App.Blazor.Pages
|
||||||
@using Wabbajack.App.Blazor.Shared
|
@using Wabbajack.App.Blazor.Shared
|
||||||
@using Wabbajack.App.Blazor.State
|
@using Wabbajack.App.Blazor.State
|
||||||
|
@inject NavigationManager _navigationManager
|
||||||
|
@inject IStateContainer _stateContainer
|
||||||
|
|
||||||
@namespace Wabbajack.App.Blazor.Components
|
@namespace Wabbajack.App.Blazor.Components
|
||||||
|
|
||||||
@* TODO: [Low] Clean this up a bit. *@
|
|
||||||
<header id="top-bar">
|
<header id="top-bar">
|
||||||
<nav class="@(GlobalState.NavigationAllowed ? "" : "disallow")">
|
<nav class="@(_stateContainer.NavigationAllowed ? "" : "disallow")">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
@foreach (var (name, route) in Pages)
|
||||||
<div class='item @CurrentPage("")' @onclick='() => Navigate("")'>Play</div>
|
{
|
||||||
</li>
|
<li>
|
||||||
<li>
|
<div class="item @CurrentPage(route)" @onclick="@(() => Navigate(route))">@name</div>
|
||||||
<div class='item @CurrentPage("Gallery")' @onclick='() => Navigate("Gallery")'>Gallery</div>
|
</li>
|
||||||
</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>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="settings">
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
[Inject]
|
private static readonly Dictionary<string, string> Pages = new()
|
||||||
NavigationManager _navigationManager { get; set; }
|
{
|
||||||
|
{"Play", Play.Route},
|
||||||
[Inject]
|
{"Gallery", Gallery.Route},
|
||||||
GlobalState GlobalState { get; set; }
|
{"Install", Install.Route},
|
||||||
|
{"Create", Create.Route}
|
||||||
[CascadingParameter]
|
};
|
||||||
protected MainLayout _mainLayout { get; set; }
|
|
||||||
|
|
||||||
private void Navigate(string page)
|
private void Navigate(string page)
|
||||||
{
|
{
|
||||||
@ -45,14 +39,14 @@
|
|||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_navigationManager.LocationChanged += (o, args) => StateHasChanged();
|
// TODO(erri120): update this
|
||||||
GlobalState.OnNavigationStateChange += StateHasChanged;
|
// _navigationManager.LocationChanged += (_, _) => StateHasChanged();
|
||||||
|
// _globalState.OnNavigationStateChange += StateHasChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CurrentPage(string page)
|
private string CurrentPage(string page)
|
||||||
{
|
{
|
||||||
string relativePath = _navigationManager.ToBaseRelativePath(_navigationManager.Uri).ToLower();
|
var relativePath = _navigationManager.ToBaseRelativePath(_navigationManager.Uri);
|
||||||
return page.ToLower() == relativePath ? "active" : "";
|
return page.Equals(relativePath, StringComparison.OrdinalIgnoreCase) ? "active" : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Wabbajack.App.Blazor.Models;
|
using Wabbajack.App.Blazor.Models;
|
||||||
using Wabbajack.App.Blazor.State;
|
using Wabbajack.App.Blazor.State;
|
||||||
@ -11,20 +12,20 @@ namespace Wabbajack.App.Blazor;
|
|||||||
|
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private readonly ILogger<MainWindow> _logger;
|
private readonly ILogger<MainWindow> _logger;
|
||||||
private readonly LoggerProvider _loggerProvider;
|
private readonly LoggerProvider _loggerProvider;
|
||||||
private readonly SystemParametersConstructor _systemParams;
|
private readonly SystemParametersConstructor _systemParams;
|
||||||
private readonly GlobalState _globalState;
|
private readonly IStateContainer _stateContainer;
|
||||||
|
|
||||||
public MainWindow(ILogger<MainWindow> logger, IServiceProvider serviceProvider, LoggerProvider loggerProvider,
|
public MainWindow(ILogger<MainWindow> logger, IServiceProvider serviceProvider, LoggerProvider loggerProvider,
|
||||||
SystemParametersConstructor systemParams, GlobalState globalState)
|
SystemParametersConstructor systemParams, IStateContainer stateContainer)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_loggerProvider = loggerProvider;
|
_loggerProvider = loggerProvider;
|
||||||
_systemParams = systemParams;
|
_systemParams = systemParams;
|
||||||
_globalState = globalState;
|
_stateContainer = stateContainer;
|
||||||
|
|
||||||
_globalState.OnTaskBarStateChange += state =>
|
_stateContainer.TaskBarStateObservable.Subscribe(state =>
|
||||||
{
|
{
|
||||||
Dispatcher.InvokeAsync(() =>
|
Dispatcher.InvokeAsync(() =>
|
||||||
{
|
{
|
||||||
@ -32,7 +33,7 @@ public partial class MainWindow
|
|||||||
TaskBarItem.ProgressState = state.State;
|
TaskBarItem.ProgressState = state.State;
|
||||||
TaskBarItem.ProgressValue = state.ProgressValue;
|
TaskBarItem.ProgressValue = state.ProgressValue;
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BlazorWebView.Services = serviceProvider;
|
BlazorWebView.Services = serviceProvider;
|
||||||
|
@ -71,7 +71,7 @@ public class LoggerProvider : ILoggerProvider
|
|||||||
|
|
||||||
private void LogToFile(ILogMessage logMessage)
|
private void LogToFile(ILogMessage logMessage)
|
||||||
{
|
{
|
||||||
string? line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}";
|
var line = $"[{logMessage.TimeStamp - _startupTime}] {logMessage.LongMessage}";
|
||||||
lock (_logStream)
|
lock (_logStream)
|
||||||
{
|
{
|
||||||
_logStream.Write(line);
|
_logStream.Write(line);
|
||||||
|
@ -1,52 +1,50 @@
|
|||||||
@page "/Configure"
|
@page "/configure"
|
||||||
|
|
||||||
@using Wabbajack.App.Blazor.State
|
@using Wabbajack.App.Blazor.State
|
||||||
|
|
||||||
@namespace Wabbajack.App.Blazor.Pages
|
@namespace Wabbajack.App.Blazor.Pages
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<div class="install-background">
|
<div class="install-background">
|
||||||
<img src="@Image" alt="">
|
<img id="background-image" src="" alt=""/>
|
||||||
</div>
|
</div>
|
||||||
<div class="list">
|
<div class="list">
|
||||||
@* TODO: [High] Find a cleaner way to show/hide components based on state. *@
|
@if (Modlist is not null)
|
||||||
@* TODO: [Low] Split each "side" into their own components? *@
|
{
|
||||||
<div class="left-side">
|
<div class="left-side">
|
||||||
@if (!string.IsNullOrEmpty(ModList.Name))
|
@if (InstallState != InstallState.Installing)
|
||||||
{
|
|
||||||
if (InstallState != GlobalState.InstallStateEnum.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"/>
|
<InfoBlock Supertitle="Installing..." Title="@Modlist.Name" Subtitle="@StatusText"/>
|
||||||
// TODO: [Low] Step logging.
|
// TODO: [Low] Step logging
|
||||||
}
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
<div class="right-side">
|
||||||
<div class="right-side">
|
@* TODO: whatever this is *@
|
||||||
@if (!string.IsNullOrEmpty(Image))
|
@* @if (!string.IsNullOrEmpty(Image)) *@
|
||||||
{
|
@* { *@
|
||||||
if (InstallState != GlobalState.InstallStateEnum.Installing)
|
@* if (InstallState != GlobalState.InstallStateEnum.Installing) *@
|
||||||
{
|
@* { *@
|
||||||
<InfoImage Image="@Image"/>
|
@* <InfoImage Image="@Image"/> *@
|
||||||
}
|
@* } *@
|
||||||
else if (InstallState == GlobalState.InstallStateEnum.Installing)
|
@* else if (InstallState == GlobalState.InstallStateEnum.Installing) *@
|
||||||
{
|
@* { *@
|
||||||
// TODO: [Low] Implement featured mod slideshow.
|
@* // 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."/>
|
@* <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>
|
</div>
|
||||||
@if (InstallState == GlobalState.InstallStateEnum.Installing)
|
@if (InstallState == InstallState.Installing)
|
||||||
{
|
{
|
||||||
<div class="logger-container">
|
<div class="logger-container">
|
||||||
<VirtualLogger Messages="_loggerProvider.Messages"/>
|
<VirtualLogger Messages="LoggerProvider.Messages"/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (InstallState != GlobalState.InstallStateEnum.Installing)
|
else
|
||||||
{
|
{
|
||||||
<div class="settings">
|
<div class="settings">
|
||||||
<div class="locations">
|
<div class="locations">
|
||||||
@ -57,9 +55,9 @@
|
|||||||
<span>Download Location</span>
|
<span>Download Location</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="paths">
|
<div class="paths">
|
||||||
<span class="modlist-file">@ModListPath</span>
|
<span class="modlist-file">@ModlistPath.ToString()</span>
|
||||||
<span class="install-location" @onclick="SelectInstallFolder">@InstallPath</span>
|
<span class="install-location" @onclick="SelectInstallFolder">@InstallPath.ToString()</span>
|
||||||
<span class="download-location" @onclick="SelectDownloadFolder">@DownloadPath</span>
|
<span class="download-location" @onclick="SelectDownloadFolder">@DownloadPath.ToString()</span>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -75,3 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public const string Route = "/configure";
|
||||||
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Wabbajack.DTOs;
|
using Wabbajack.DTOs;
|
||||||
@ -12,6 +10,8 @@ using Wabbajack.Downloaders.GameFile;
|
|||||||
using Wabbajack.Hashing.xxHash64;
|
using Wabbajack.Hashing.xxHash64;
|
||||||
using Wabbajack.Services.OSIntegrated;
|
using Wabbajack.Services.OSIntegrated;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
using Wabbajack.App.Blazor.Models;
|
using Wabbajack.App.Blazor.Models;
|
||||||
using Wabbajack.App.Blazor.State;
|
using Wabbajack.App.Blazor.State;
|
||||||
|
|
||||||
@ -19,71 +19,74 @@ namespace Wabbajack.App.Blazor.Pages;
|
|||||||
|
|
||||||
public partial class Configure
|
public partial class Configure
|
||||||
{
|
{
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; }
|
[Inject] private ILogger<Configure> Logger { get; set; } = default!;
|
||||||
[Inject] private GlobalState GlobalState { get; set; }
|
[Inject] private IStateContainer StateContainer { get; set; } = default!;
|
||||||
[Inject] private DTOSerializer _dtos { get; set; }
|
[Inject] private DTOSerializer DTOs { get; set; } = default!;
|
||||||
[Inject] private IServiceProvider _serviceProvider { get; set; }
|
[Inject] private IServiceProvider ServiceProvider { get; set; } = default!;
|
||||||
[Inject] private SystemParametersConstructor _parametersConstructor { get; set; }
|
[Inject] private SystemParametersConstructor ParametersConstructor { get; set; } = default!;
|
||||||
[Inject] private IGameLocator _gameLocator { get; set; }
|
[Inject] private IGameLocator GameLocator { get; set; } = default!;
|
||||||
[Inject] private SettingsManager _settingsManager { get; set; }
|
[Inject] private SettingsManager SettingsManager { get; set; } = default!;
|
||||||
[Inject] private LoggerProvider _loggerProvider { get; set; }
|
[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 AbsolutePath ModlistPath => StateContainer.ModlistPath;
|
||||||
private ModList ModList { get; set; } = new(); // Init a new modlist so we can listen for changes in Blazor components.
|
private AbsolutePath InstallPath { get; set; }
|
||||||
private AbsolutePath ModListPath { get; set; }
|
|
||||||
private AbsolutePath InstallPath { get; set; }
|
|
||||||
private AbsolutePath DownloadPath { get; set; }
|
private AbsolutePath DownloadPath { get; set; }
|
||||||
|
|
||||||
private string StatusText { get; set; }
|
private string StatusText { get; set; } = string.Empty;
|
||||||
public GlobalState.InstallStateEnum InstallState { get; set; }
|
private InstallState InstallState => StateContainer.InstallState;
|
||||||
private LoggerProvider.ILogMessage CurrentLog { get; set; }
|
// private LoggerProvider.ILogMessage CurrentLog { get; set; }
|
||||||
|
|
||||||
private const string InstallSettingsPrefix = "install-settings-";
|
private const string InstallSettingsPrefix = "install-settings-";
|
||||||
|
|
||||||
|
private bool _shouldRender;
|
||||||
|
protected override bool ShouldRender() => _shouldRender;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// var Location = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", machineURL).WithExtension(Ext.Wabbajack);
|
// var Location = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", machineURL).WithExtension(Ext.Wabbajack);
|
||||||
GlobalState.OnInstallStateChange += () => InstallState = GlobalState.InstallState;
|
|
||||||
await CheckValidInstallPath();
|
await CheckValidInstallPath();
|
||||||
await base.OnInitializedAsync();
|
_shouldRender = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task CheckValidInstallPath()
|
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;
|
var hex = (await ModlistPath.ToString().Hash()).ToHex();
|
||||||
ModList = await StandardInstaller.LoadFromFile(_dtos, ModListPath);
|
var prevSettings = await SettingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
|
||||||
GlobalState.ModList = ModList;
|
|
||||||
|
|
||||||
string hex = (await ModListPath.ToString().Hash()).ToHex();
|
if (prevSettings.ModlistLocation == ModlistPath)
|
||||||
var prevSettings = await _settingsManager.Load<SavedInstallSettings>(InstallSettingsPrefix + hex);
|
|
||||||
|
|
||||||
if (prevSettings.ModListLocation == ModListPath)
|
|
||||||
{
|
{
|
||||||
ModListPath = prevSettings.ModListLocation;
|
StateContainer.ModlistPath = prevSettings.ModlistLocation;
|
||||||
InstallPath = prevSettings.InstallLocation;
|
InstallPath = prevSettings.InstallLocation;
|
||||||
DownloadPath = prevSettings.DownloadLoadction;
|
DownloadPath = prevSettings.DownloadLocation;
|
||||||
//ModlistMetadata = metadata ?? prevSettings.Metadata;
|
//ModlistMetadata = metadata ?? prevSettings.Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream image = await StandardInstaller.ModListImageStream(ModListPath);
|
// see https://docs.microsoft.com/en-us/aspnet/core/blazor/images?view=aspnetcore-6.0#streaming-examples
|
||||||
await using var reader = new MemoryStream();
|
var imageStream = await StandardInstaller.ModListImageStream(ModlistPath);
|
||||||
await image.CopyToAsync(reader);
|
var dotnetImageStream = new DotNetStreamReference(imageStream);
|
||||||
Image = $"data:image/png;base64,{Convert.ToBase64String(reader.ToArray())}";
|
// setImageUsingStreaming accepts the img id and the data stream
|
||||||
|
await JSRuntime.InvokeVoidAsync("setImageUsingStreaming", "background-image", dotnetImageStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void SelectInstallFolder()
|
private async void SelectInstallFolder()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AbsolutePath? thing = await Dialog.ShowDialogNonBlocking(true);
|
var installPath = await Dialog.ShowDialogNonBlocking(true);
|
||||||
if (thing != null) InstallPath = (AbsolutePath)thing;
|
if (installPath is not null) InstallPath = (AbsolutePath)installPath;
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
AbsolutePath? thing = await Dialog.ShowDialogNonBlocking(true);
|
var downloadPath = await Dialog.ShowDialogNonBlocking(true);
|
||||||
if (thing != null) DownloadPath = (AbsolutePath)thing;
|
if (downloadPath is not null) DownloadPath = (AbsolutePath)downloadPath;
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Debug.Print(ex.Message);
|
Logger.LogError(e, "Exception selecting download folder");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Install()
|
private async Task Install()
|
||||||
{
|
{
|
||||||
GlobalState.InstallState = GlobalState.InstallStateEnum.Installing;
|
if (Modlist is null) return;
|
||||||
await Task.Run(BeginInstall);
|
|
||||||
|
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();
|
var postfix = (await ModlistPath.ToString().Hash()).ToHex();
|
||||||
await _settingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
|
await SettingsManager.Save(InstallSettingsPrefix + postfix, new SavedInstallSettings
|
||||||
{
|
{
|
||||||
ModListLocation = ModListPath,
|
ModlistLocation = ModlistPath,
|
||||||
InstallLocation = InstallPath,
|
InstallLocation = InstallPath,
|
||||||
DownloadLoadction = DownloadPath
|
DownloadLocation = DownloadPath
|
||||||
});
|
});
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var installer = StandardInstaller.Create(_serviceProvider, new InstallerConfiguration
|
var installer = StandardInstaller.Create(ServiceProvider, new InstallerConfiguration
|
||||||
{
|
{
|
||||||
Game = ModList.GameType,
|
Game = modlist.GameType,
|
||||||
Downloads = DownloadPath,
|
Downloads = DownloadPath,
|
||||||
Install = InstallPath,
|
Install = InstallPath,
|
||||||
ModList = ModList,
|
ModList = modlist,
|
||||||
ModlistArchive = ModListPath,
|
ModlistArchive = ModlistPath,
|
||||||
SystemParameters = _parametersConstructor.Create(),
|
SystemParameters = ParametersConstructor.Create(),
|
||||||
GameFolder = _gameLocator.GameLocation(ModList.GameType)
|
GameFolder = GameLocator.GameLocation(modlist.GameType)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
installer.OnStatusUpdate = update =>
|
installer.OnStatusUpdate = update =>
|
||||||
{
|
{
|
||||||
if (StatusText != update.StatusText)
|
var (statusText, _, _) = update;
|
||||||
{
|
if (StatusText == statusText) return;
|
||||||
StatusText = update.StatusText;
|
StatusText = statusText;
|
||||||
InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await installer.Begin(CancellationToken.None);
|
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
|
internal class SavedInstallSettings
|
||||||
{
|
{
|
||||||
public AbsolutePath ModListLocation { get; set; }
|
public AbsolutePath ModlistLocation { get; set; } = AbsolutePath.Empty;
|
||||||
public AbsolutePath InstallLocation { get; set; }
|
public AbsolutePath InstallLocation { get; set; } = AbsolutePath.Empty;
|
||||||
public AbsolutePath DownloadLoadction { get; set; }
|
public AbsolutePath DownloadLocation { get; set; } = AbsolutePath.Empty;
|
||||||
public ModlistMetadata Metadata { get; set; }
|
// public ModlistMetadata Metadata { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@page "/Create"
|
@page "/create"
|
||||||
@namespace Wabbajack.App.Blazor.Pages
|
@namespace Wabbajack.App.Blazor.Pages
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
@ -6,3 +6,8 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
public const string Route = "/create";
|
||||||
|
}
|
||||||
|
@ -1,24 +1,51 @@
|
|||||||
@page "/Gallery"
|
@page "/gallery"
|
||||||
|
|
||||||
@using Wabbajack.DTOs
|
|
||||||
@using Wabbajack.RateLimiter
|
@using Wabbajack.RateLimiter
|
||||||
|
@using System.Globalization
|
||||||
|
|
||||||
@namespace Wabbajack.App.Blazor.Pages
|
@namespace Wabbajack.App.Blazor.Pages
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
@foreach (ModlistMetadata modlist in _listItems)
|
@if (_errorLoadingModlists)
|
||||||
{
|
{
|
||||||
<ModlistItem Metadata=@modlist>
|
@* TODO: error *@
|
||||||
<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>
|
|
||||||
}
|
}
|
||||||
@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;">
|
<div style="height:1.5rem;">
|
||||||
<ProgressBar Percentage="@DownloadProgress" Text="@DownloadProgress.Value.ToString()"/>
|
<ProgressBar Percentage="@_downloadProgress" Text="@_downloadProgress.Value.ToString(CultureInfo.InvariantCulture)"/>
|
||||||
</div>
|
</div>
|
||||||
</BottomBar>
|
</BottomBar>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public const string Route = "/gallery";
|
||||||
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Shell;
|
using System.Windows.Shell;
|
||||||
@ -20,33 +21,35 @@ namespace Wabbajack.App.Blazor.Pages;
|
|||||||
|
|
||||||
public partial class Gallery
|
public partial class Gallery
|
||||||
{
|
{
|
||||||
[Inject] private GlobalState GlobalState { get; set; }
|
[Inject] private ILogger<Gallery> Logger { get; set; } = default!;
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; }
|
[Inject] private IStateContainer StateContainer { get; set; } = default!;
|
||||||
[Inject] private ILogger<Gallery> _logger { get; set; }
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
[Inject] private Client _client { get; set; }
|
[Inject] private ModListDownloadMaintainer Maintainer { get; set; } = default!;
|
||||||
[Inject] private ModListDownloadMaintainer _maintainer { get; set; }
|
|
||||||
|
|
||||||
public Percent DownloadProgress { get; set; } = Percent.Zero;
|
private Percent _downloadProgress = Percent.Zero;
|
||||||
public ModlistMetadata DownloadingMetaData { get; set; } = new ModlistMetadata();
|
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()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
try
|
if (!StateContainer.Modlists.Any())
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Getting modlists...");
|
var res = await StateContainer.LoadModlistMetadata();
|
||||||
ModlistMetadata[] modLists = await _client.LoadLists();
|
if (!res)
|
||||||
_listItems.AddRange(modLists.ToList());
|
{
|
||||||
StateHasChanged();
|
_errorLoadingModlists = true;
|
||||||
|
_shouldRender = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
_shouldRender = true;
|
||||||
//TODO: [Critical] Figure out why an exception is thrown on first navigation.
|
|
||||||
_logger.LogError(ex, "Error while loading lists");
|
|
||||||
}
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnClickDownload(ModlistMetadata metadata)
|
private async void OnClickDownload(ModlistMetadata metadata)
|
||||||
@ -62,38 +65,42 @@ public partial class Gallery
|
|||||||
|
|
||||||
private async Task Download(ModlistMetadata metadata)
|
private async Task Download(ModlistMetadata metadata)
|
||||||
{
|
{
|
||||||
GlobalState.NavigationAllowed = false;
|
StateContainer.NavigationAllowed = false;
|
||||||
DownloadingMetaData = metadata;
|
_downloadingMetaData = metadata;
|
||||||
await using Timer timer = new(_ => InvokeAsync(StateHasChanged));
|
|
||||||
timer.Change(TimeSpan.FromMilliseconds(250), TimeSpan.FromMilliseconds(250));
|
|
||||||
try
|
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
|
||||||
|
.DistinctUntilChanged(p => p.Value)
|
||||||
var dispose = progress.Subscribe(p =>
|
.Throttle(TimeSpan.FromMilliseconds(100))
|
||||||
|
.Subscribe(p =>
|
||||||
{
|
{
|
||||||
DownloadProgress = p;
|
_downloadProgress = p;
|
||||||
GlobalState.SetTaskBarState(TaskbarItemProgressState.Indeterminate,$"Downloading {metadata.Title}", p.Value);
|
StateContainer.TaskBarState = new TaskBarState
|
||||||
});
|
{
|
||||||
|
Description = $"Downloading {metadata.Title}",
|
||||||
|
State = TaskbarItemProgressState.Indeterminate,
|
||||||
|
ProgressValue = p.Value
|
||||||
|
};
|
||||||
|
}, () => { StateContainer.TaskBarState = new TaskBarState(); });
|
||||||
|
|
||||||
await task;
|
await task;
|
||||||
//await _wjClient.SendMetric("downloading", Metadata.Title);
|
|
||||||
dispose.Dispose();
|
dispose.Dispose();
|
||||||
GlobalState.SetTaskBarState();
|
|
||||||
|
var path = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
|
||||||
|
StateContainer.ModlistPath = path;
|
||||||
AbsolutePath path = KnownFolders.EntryPoint.Combine("downloaded_mod_lists", metadata.Links.MachineURL).WithExtension(Ext.Wabbajack);
|
NavigationManager.NavigateTo(Configure.Route);
|
||||||
GlobalState.ModListPath = path;
|
|
||||||
NavigationManager.NavigateTo("/Configure");
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,89 @@
|
|||||||
#content {
|
@import "../Shared/Globals.scss";
|
||||||
|
|
||||||
|
#content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: space-evenly;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@page "/Install"
|
@page "/install"
|
||||||
|
|
||||||
@namespace Wabbajack.App.Blazor.Pages
|
@namespace Wabbajack.App.Blazor.Pages
|
||||||
|
|
||||||
@ -24,3 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public const string Route = "/install";
|
||||||
|
}
|
||||||
|
@ -8,8 +8,8 @@ namespace Wabbajack.App.Blazor.Pages;
|
|||||||
|
|
||||||
public partial class Install
|
public partial class Install
|
||||||
{
|
{
|
||||||
[Inject] private NavigationManager NavigationManager { get; set; }
|
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
|
||||||
[Inject] private GlobalState GlobalState { get; set; }
|
[Inject] private IStateContainer StateContainer { get; set; } = default!;
|
||||||
|
|
||||||
private void SelectFile()
|
private void SelectFile()
|
||||||
{
|
{
|
||||||
@ -18,10 +18,10 @@ public partial class Install
|
|||||||
dialog.Multiselect = false;
|
dialog.Multiselect = false;
|
||||||
dialog.Filters.Add(new CommonFileDialogFilter("Wabbajack File", "*" + Ext.Wabbajack));
|
dialog.Filters.Add(new CommonFileDialogFilter("Wabbajack File", "*" + Ext.Wabbajack));
|
||||||
if (dialog.ShowDialog() != CommonFileDialogResult.Ok) return;
|
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) { }
|
private void VerifyFile(AbsolutePath path) { }
|
||||||
|
@ -46,4 +46,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,3 +21,7 @@
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public const string Route = "/";
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@page "/Settings"
|
@page "/settings"
|
||||||
@namespace Wabbajack.App.Blazor.Pages
|
@namespace Wabbajack.App.Blazor.Pages
|
||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
@ -6,3 +6,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
public const string Route = "/settings";
|
||||||
|
}
|
||||||
|
@ -13,7 +13,7 @@ public partial class Settings
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ResourceSettingsManager.ResourceSetting resource = await _resourceSettingsManager.GetSettings("Downloads");
|
var resource = await _resourceSettingsManager.GetSettings("Downloads");
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
catch (Exception ex) { }
|
catch (Exception ex) { }
|
||||||
|
@ -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
|
|
||||||
}
|
|
28
Wabbajack.App.Blazor/State/IStateContainer.cs
Normal file
28
Wabbajack.App.Blazor/State/IStateContainer.cs
Normal 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; }
|
||||||
|
}
|
10
Wabbajack.App.Blazor/State/InstallState.cs
Normal file
10
Wabbajack.App.Blazor/State/InstallState.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
namespace Wabbajack.App.Blazor.State;
|
||||||
|
|
||||||
|
public enum InstallState
|
||||||
|
{
|
||||||
|
Waiting,
|
||||||
|
Configuration,
|
||||||
|
Installing,
|
||||||
|
Success,
|
||||||
|
Failure
|
||||||
|
}
|
81
Wabbajack.App.Blazor/State/StateContainer.cs
Normal file
81
Wabbajack.App.Blazor/State/StateContainer.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ namespace Wabbajack.App.Blazor.State;
|
|||||||
|
|
||||||
public class TaskBarState
|
public class TaskBarState
|
||||||
{
|
{
|
||||||
public string Description { get; set; }
|
public string Description { get; set; } = string.Empty;
|
||||||
public double ProgressValue { get; set; }
|
public double ProgressValue { get; set; }
|
||||||
public TaskbarItemProgressState State { get; set; }
|
public TaskbarItemProgressState State { get; set; } = TaskbarItemProgressState.None;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ public static class Dialog
|
|||||||
var dialog = new CommonOpenFileDialog();
|
var dialog = new CommonOpenFileDialog();
|
||||||
dialog.IsFolderPicker = isFolderPicker;
|
dialog.IsFolderPicker = isFolderPicker;
|
||||||
dialog.Multiselect = false;
|
dialog.Multiselect = false;
|
||||||
CommonFileDialogResult result = dialog.ShowDialog(newWindow);
|
var result = dialog.ShowDialog(newWindow);
|
||||||
return result == CommonFileDialogResult.Ok ? dialog.FileName : null;
|
return result == CommonFileDialogResult.Ok ? dialog.FileName : null;
|
||||||
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext())
|
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext())
|
||||||
.ContinueWith(result => result.Result?.ToAbsolutePath())
|
.ContinueWith(result => result.Result?.ToAbsolutePath())
|
||||||
|
@ -40,14 +40,14 @@ public class SystemParametersConstructor
|
|||||||
SetProcessDPIAware();
|
SetProcessDPIAware();
|
||||||
unsafe
|
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,
|
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
|
||||||
delegate(IntPtr hMonitor, IntPtr hdcMonitor, RECT* lprcMonitor, void* dwData)
|
delegate(IntPtr hMonitor, IntPtr hdcMonitor, RECT* lprcMonitor, void* dwData)
|
||||||
{
|
{
|
||||||
var mi = new MONITORINFOEX();
|
var mi = new MONITORINFOEX();
|
||||||
mi.cbSize = Marshal.SizeOf(mi);
|
mi.cbSize = Marshal.SizeOf(mi);
|
||||||
bool success = GetMonitorInfo(hMonitor, (MONITORINFO*)&mi);
|
var success = GetMonitorInfo(hMonitor, (MONITORINFO*)&mi);
|
||||||
if (success)
|
if (success)
|
||||||
col.Add((mi.Monitor.right - mi.Monitor.left, mi.Monitor.bottom - mi.Monitor.top,
|
col.Add((mi.Monitor.right - mi.Monitor.left, mi.Monitor.bottom - mi.Monitor.top,
|
||||||
mi.Flags == MONITORINFO_Flags.MONITORINFOF_PRIMARY));
|
mi.Flags == MONITORINFO_Flags.MONITORINFOF_PRIMARY));
|
||||||
@ -60,17 +60,17 @@ public class SystemParametersConstructor
|
|||||||
|
|
||||||
public SystemParameters Create()
|
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();
|
/*using var f = new SharpDX.DXGI.Factory1();
|
||||||
var video_memory = f.Adapters1.Select(a =>
|
var video_memory = f.Adapters1.Select(a =>
|
||||||
Math.Max(a.Description.DedicatedSystemMemory, (long)a.Description.DedicatedVideoMemory)).Max();*/
|
Math.Max(a.Description.DedicatedSystemMemory, (long)a.Description.DedicatedVideoMemory)).Max();*/
|
||||||
|
|
||||||
ulong dxgiMemory = 0UL;
|
var dxgiMemory = 0UL;
|
||||||
|
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
using DXGI? api = DXGI.GetApi();
|
using var api = DXGI.GetApi();
|
||||||
|
|
||||||
IDXGIFactory1* factory1 = default;
|
IDXGIFactory1* factory1 = default;
|
||||||
|
|
||||||
@ -79,15 +79,15 @@ public class SystemParametersConstructor
|
|||||||
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-createdxgifactory1
|
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-createdxgifactory1
|
||||||
SilkMarshal.ThrowHResult(api.CreateDXGIFactory1(SilkMarshal.GuidPtrOf<IDXGIFactory1>(), (void**)&factory1));
|
SilkMarshal.ThrowHResult(api.CreateDXGIFactory1(SilkMarshal.GuidPtrOf<IDXGIFactory1>(), (void**)&factory1));
|
||||||
|
|
||||||
uint i = 0u;
|
var i = 0u;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
IDXGIAdapter1* adapter1 = default;
|
IDXGIAdapter1* adapter1 = default;
|
||||||
|
|
||||||
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgifactory1-enumadapters1
|
//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;
|
if (exception != null) break;
|
||||||
|
|
||||||
AdapterDesc1 adapterDesc = default;
|
AdapterDesc1 adapterDesc = default;
|
||||||
@ -95,10 +95,10 @@ public class SystemParametersConstructor
|
|||||||
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiadapter1-getdesc1
|
//https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiadapter1-getdesc1
|
||||||
SilkMarshal.ThrowHResult(adapter1->GetDesc1(&adapterDesc));
|
SilkMarshal.ThrowHResult(adapter1->GetDesc1(&adapterDesc));
|
||||||
|
|
||||||
ulong systemMemory = (ulong)adapterDesc.DedicatedSystemMemory;
|
var systemMemory = (ulong)adapterDesc.DedicatedSystemMemory;
|
||||||
ulong videoMemory = (ulong)adapterDesc.DedicatedVideoMemory;
|
var videoMemory = (ulong)adapterDesc.DedicatedVideoMemory;
|
||||||
|
|
||||||
ulong maxMemory = Math.Max(systemMemory, videoMemory);
|
var maxMemory = Math.Max(systemMemory, videoMemory);
|
||||||
if (maxMemory > dxgiMemory)
|
if (maxMemory > dxgiMemory)
|
||||||
dxgiMemory = maxMemory;
|
dxgiMemory = maxMemory;
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ public class SystemParametersConstructor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MEMORYSTATUSEX? memory = GetMemoryStatus();
|
var memory = GetMemoryStatus();
|
||||||
var p = new SystemParameters
|
var p = new SystemParameters
|
||||||
{
|
{
|
||||||
ScreenWidth = width,
|
ScreenWidth = width,
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="6.0.1" />
|
||||||
<PackageReference Include="PInvoke.User32" Version="0.7.104" />
|
<PackageReference Include="PInvoke.User32" Version="0.7.104" />
|
||||||
<PackageReference Include="Silk.NET.DXGI" Version="2.12.0" />
|
<PackageReference Include="Silk.NET.DXGI" Version="2.12.0" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -32,6 +32,5 @@
|
|||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
||||||
<script src="_framework/blazor.webview.js"></script>
|
<script src="_framework/blazor.webview.js"></script>
|
||||||
<script src="_content/Fluxor.Blazor.Web/scripts/index.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
59
Wabbajack.Common/CustomObservable.cs
Normal file
59
Wabbajack.Common/CustomObservable.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user