14 Commits

19 changed files with 723 additions and 0 deletions

View File

@@ -20,6 +20,7 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="2.0.0-preview1-final"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.6"/>
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter" Version="4.12.1"/>
<PackageVersion Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.14.15"/>
<PackageVersion Include="Microsoft.Win32.Registry" Version="5.0.0"/>
<PackageVersion Include="Mono.Fuse.NETStandard" Version="1.1.0"/>
@@ -30,6 +31,7 @@
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.13.1"/>
<PackageVersion Include="SabreTools.Models" Version="1.5.8"/>
<PackageVersion Include="Serilog" Version="4.3.0"/>
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0"/>
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2"/>
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageVersion Include="SharpCompress" Version="0.39.0"/>
@@ -46,5 +48,7 @@
<PackageVersion Include="SharpCompress" Version="0.38.0"/>
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0"/>
<PackageVersion Include="ZstdSharp.Port" Version="0.8.6"/>
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.12.1"/>
<PackageVersion Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.12.1"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<base href="/"/>
<link href="@Assets["app.css"]" rel="stylesheet"/>
<link href="@Assets["RomRepoMgr.Blazor.styles.css"]" rel="stylesheet"/>
<ImportMap/>
<link href="favicon.ico" rel="icon" type="image/x-icon"/>
<HeadOutlet @rendermode="InteractiveServer"/>
</head>
<body>
<Routes @rendermode="InteractiveServer"/>
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,16 @@
@implements IDialogContentComponent
@inject ILogger<ImportDats> Logger
<FluentDialog Width="800px" Height="400px" Title="Import DATs" Modal="true" TrapFocus="true">
<FluentDialogBody>
<p hidden="@IsBusy">DAT files will be imported from @path.</p>
<FluentLabel Color="@StatusColor">@StatusMessage</FluentLabel>
<FluentProgress Max="@ProgressMax" Min="@ProgressMin" Value="@ProgressValue" Visible="@ProgressVisible"/>
</FluentDialogBody>
<FluentDialogFooter>
<FluentStack Orientation="Orientation.Horizontal">
<FluentButton OnClick="@StartAsync" Disabled="@IsBusy">Start</FluentButton>
<FluentButton OnClick="@CloseAsync" Disabled="@CannotClose">Close</FluentButton>
</FluentStack>
</FluentDialogFooter>
</FluentDialog>

View File

@@ -0,0 +1,108 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using RomRepoMgr.Core.Workers;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.Blazor.Components.Dialogs;
public partial class ImportDats : ComponentBase
{
readonly Stopwatch _stopwatch = new();
string[] _datFiles;
int _listPosition;
int _workers;
string path;
public string StatusMessage { get; set; }
public bool IsBusy { get; set; }
[CascadingParameter]
public FluentDialog Dialog { get; set; }
public int? ProgressMax { get; set; }
public int? ProgressMin { get; set; }
public int? ProgressValue { get; set; }
public bool CannotClose { get; set; }
public bool ProgressVisible { get; set; }
public Color? StatusColor { get; set; }
/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
path = Path.Combine(Environment.CurrentDirectory, Consts.IncomingDatFolder);
StatusMessage = string.Empty;
IsBusy = false;
CannotClose = false;
ProgressVisible = false;
}
Task StartAsync()
{
IsBusy = true;
CannotClose = true;
ProgressVisible = true;
ProgressValue = null;
StatusMessage = "Searching for files...";
_stopwatch.Restart();
string[] dats = Directory.GetFiles(path, "*.dat", SearchOption.AllDirectories);
string[] xmls = Directory.GetFiles(path, "*.xml", SearchOption.AllDirectories);
_datFiles = dats.Concat(xmls).Order().ToArray();
_stopwatch.Stop();
Logger.LogDebug("Took {TotalSeconds} to find {Length} DAT files",
_stopwatch.Elapsed.TotalSeconds,
_datFiles.Length);
StatusMessage = string.Format("Found {0} files...", _datFiles.Length);
ProgressMin = 0;
ProgressMax = _datFiles.Length;
ProgressValue = 0;
_listPosition = 0;
_workers = 0;
StateHasChanged();
return ImportAsync();
}
async Task ImportAsync()
{
_stopwatch.Restart();
Logger.LogDebug("Starting to import DAT files...");
Parallel.ForEach(_datFiles,
datFile =>
{
_ = InvokeAsync(() =>
{
StatusMessage = string.Format("Importing {0}...", Path.GetFileName(datFile));
ProgressValue = _listPosition;
StateHasChanged();
});
var worker = new DatImporter(datFile, null, new SerilogLoggerFactory(Log.Logger));
worker.Import();
Interlocked.Increment(ref _listPosition);
});
ProgressVisible = false;
StatusMessage = "Finished";
CannotClose = false;
_stopwatch.Stop();
Logger.LogDebug("Took {TotalSeconds} seconds to import {Length} DAT files",
_stopwatch.Elapsed.TotalSeconds,
_datFiles.Length);
StateHasChanged();
}
Task CloseAsync() => Dialog.CloseAsync();
}

View File

@@ -0,0 +1,24 @@
@inherits LayoutComponentBase
<FluentLayout>
<FluentHeader>
ROM Repository Manager
</FluentHeader>
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<FluentBodyContent Class="body-content">
<div class="content">
@Body
</div>
</FluentBodyContent>
</FluentStack>
<FluentFooter>
<a href="https://www.natportillo.es" target="_blank">Copyright © 2020-2025 Natalia Portillo</a>
</FluentFooter>
<FluentDialogProvider/>
</FluentLayout>
<div data-nosnippet id="blazor-error-ui">
An unhandled error has occurred.
<a class="reload" href=".">Reload</a>
<span class="dismiss">🗙</span>
</div>

View File

@@ -0,0 +1,35 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if(ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() => RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,36 @@
@page "/"
@using RomRepoMgr.Database
@rendermode InteractiveServer
@inject IDialogService DialogService
@inject Context ctx
<PageTitle>ROM Repository Manager</PageTitle>
<FluentToolbar>
<FluentButton OnClick="@ImportDatsAsync">Import DATs</FluentButton>
<FluentButton Disabled="true">Export DAT</FluentButton>
<FluentButton Disabled="true">Remove DAT</FluentButton>
<FluentButton Disabled="true">Import ROMs</FluentButton>
<FluentButton Disabled="true">Export ROMs</FluentButton>
</FluentToolbar>
<FluentDataGrid @ref="romSetsGrid" Items="@RomSets" Style="width: 100%;" AutoFit="true" Pagination="@pagination"
AutoItemsPerPage="true" ResizableColumns="true">
<PropertyColumn Property="@(p => p.Name)" Title="Name"/>
<PropertyColumn Property="@(p => p.Version)" Title="Version"/>
<PropertyColumn Property="@(p => p.Author)" Title="Author"/>
<PropertyColumn Property="@(p => p.Category)" Title="Category"/>
<PropertyColumn Property="@(p => p.Date)" Title="Date"/>
<PropertyColumn Property="@(p => p.Description)" Title="Description"/>
<PropertyColumn Property="@(p => p.Comment)" Title="Comment"/>
<PropertyColumn Property="@(p => p.Homepage)" Title="Homepage"/>
<PropertyColumn Property="@(p => p.TotalMachines)" Title="Total machines"/>
<PropertyColumn Property="@(p => p.CompleteMachines)" Title="Complete machines"/>
<PropertyColumn Property="@(p => p.IncompleteMachines)" Title="Incomplete machines"/>
<PropertyColumn Property="@(p => p.TotalRoms)" Title="Total ROMs"/>
<PropertyColumn Property="@(p => p.HaveRoms)" Title="Have ROMs"/>
<PropertyColumn Property="@(p => p.MissRoms)" Title="Miss ROMs"/>
</FluentDataGrid>
<FluentPaginator State="@pagination"/>

View File

@@ -0,0 +1,59 @@
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using RomRepoMgr.Blazor.Components.Dialogs;
using RomRepoMgr.Core.Models;
namespace RomRepoMgr.Blazor.Components.Pages;
public partial class Home : ComponentBase
{
readonly PaginationState pagination = new()
{
ItemsPerPage = 10
};
FluentDataGrid<RomSetModel>? romSetsGrid;
public IQueryable<RomSetModel>? RomSets { get; set; }
async Task ImportDatsAsync()
{
IDialogReference dialog = await DialogService.ShowDialogAsync<ImportDats>(new DialogParameters());
}
/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
romSetsGrid?.SetLoadingState(true);
RomSets = ctx.RomSets.OrderBy(r => r.Name)
.ThenBy(r => r.Version)
.ThenBy(r => r.Date)
.ThenBy(r => r.Description)
.ThenBy(r => r.Comment)
.ThenBy(r => r.Filename)
.Select(r => new RomSetModel
{
Id = r.Id,
Author = r.Author,
Comment = r.Comment,
Date = r.Date,
Description = r.Description,
Filename = r.Filename,
Homepage = r.Homepage,
Name = r.Name,
Sha384 = r.Sha384,
Version = r.Version,
TotalMachines = r.Statistics.TotalMachines,
CompleteMachines = r.Statistics.CompleteMachines,
IncompleteMachines = r.Statistics.IncompleteMachines,
TotalRoms = r.Statistics.TotalRoms,
HaveRoms = r.Statistics.HaveRoms,
MissRoms = r.Statistics.MissRoms,
Category = r.Category
});
romSetsGrid?.SetLoadingState(false);
}
}

View File

@@ -0,0 +1,8 @@
<FluentDesignTheme StorageName="theme" Mode="DesignThemeModes.Dark">
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView DefaultLayout="typeof(Layout.MainLayout)" RouteData="routeData"/>
<FocusOnNavigate RouteData="routeData" Selector="h1"/>
</Found>
</Router>
</FluentDesignTheme>

View File

@@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.FluentUI.AspNetCore.Components
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
@using Microsoft.JSInterop
@using RomRepoMgr.Blazor
@using RomRepoMgr.Blazor.Components

View File

@@ -0,0 +1,11 @@
namespace RomRepoMgr.Blazor;
public class Consts
{
public const string DbFolder = "db";
public const string DatFolder = "dats";
public const string IncomingDatFolder = "incoming-dats";
public const string RepositoryFolder = "repo";
public const string IncomingRomsFolder = "incoming";
public const string TemporaryFolder = "tmp";
}

View File

@@ -0,0 +1,127 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.FluentUI.AspNetCore.Components;
using RomRepoMgr.Blazor;
using RomRepoMgr.Blazor.Components;
using RomRepoMgr.Database;
using RomRepoMgr.Settings;
using Serilog;
Log.Logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.WriteTo.Console()
.Enrich.FromLogContext()
.CreateLogger();
Log.Information("Welcome to ROM Repository Manager!");
Log.Information("Copyright © 2020-2025 Natalia Portillo");
// Ensure the folders exist
Log.Information("Ensuring folders exist...");
string[] folders =
[
Consts.DbFolder, Consts.DatFolder, Consts.IncomingDatFolder, Consts.RepositoryFolder, Consts.IncomingRomsFolder
];
foreach(string folder in folders)
{
if(!Directory.Exists(folder))
{
Log.Debug("Creating folder: {Folder}", folder);
Directory.CreateDirectory(folder);
}
else
Log.Debug("Folder already exists: {Folder}", folder);
}
// Ensure the temporary folder exists but it can also be a symlink
if(!Directory.Exists(Consts.TemporaryFolder) && !File.Exists(Consts.TemporaryFolder))
{
Log.Debug("Creating folder: {TemporaryFolder}", Consts.TemporaryFolder);
Directory.CreateDirectory(Consts.TemporaryFolder);
}
else
Log.Debug("Folder already exists: {TemporaryFolder}", Consts.TemporaryFolder);
Log.Debug("Creating the builder...");
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog(); // ✅ Plug Serilog into the host
// Add services to the container.
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddFluentUIComponents();
Log.Debug("Creating database context...");
builder.Services.AddDbContextFactory<Context>(options =>
{
options.UseSqlite($"Data Source={Consts.DbFolder}/database.db");
#if DEBUG
options.EnableSensitiveDataLogging();
options.LogTo(Log.Debug);
#else
options.LogTo(Log.Information, LogLevel.Information);
#endif
});
builder.Services.AddDataGridEntityFrameworkAdapter();
Log.Debug("Setting the settings...");
Settings.Current = new SetSettings
{
DatabasePath = Path.Combine(Environment.CurrentDirectory, Consts.DbFolder, "database.db"),
TemporaryFolder = Path.Combine(Environment.CurrentDirectory, Consts.TemporaryFolder),
RepositoryPath = Path.Combine(Environment.CurrentDirectory, Consts.RepositoryFolder),
UseInternalDecompressor = true,
Compression = CompressionType.Zstd // Todo: Read from configuration
};
Log.Debug("Building the application...");
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if(!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
Stopwatch stopwatch = new();
using(IServiceScope scope = app.Services.CreateScope())
{
IServiceProvider services = scope.ServiceProvider;
try
{
Log.Information("Updating the database...");
stopwatch.Start();
Context dbContext = services.GetRequiredService<Context>();
await dbContext.Database.MigrateAsync();
stopwatch.Stop();
Log.Debug("Database migration: {Elapsed} seconds", stopwatch.Elapsed.TotalSeconds);
}
catch(Exception ex)
{
Log.Error(ex, "An error occurred while updating the database");
return;
}
}
Log.Debug("Running the application...");
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5079",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7057;http://localhost:5079",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite"/>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components"/>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.DataGrid.EntityFrameworkAdapter"/>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons"/>
<PackageReference Include="Serilog"/>
<PackageReference Include="Serilog.AspNetCore"/>
<PackageReference Include="Serilog.Sinks.Console"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RomRepoMgr.Core\RomRepoMgr.Core.csproj"/>
<ProjectReference Include="..\RomRepoMgr.Database\RomRepoMgr.Database.csproj"/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"CircuitOptions": {
"DetailedErrors": true
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,191 @@
@import '_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';
body {
--body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
font-family: var(--body-font);
font-size: var(--type-ramp-base-font-size);
line-height: var(--type-ramp-base-line-height);
margin: 0;
}
.navmenu-icon {
display: none;
}
.main {
min-height: calc(100dvh - 86px);
color: var(--neutral-foreground-rest);
align-items: stretch !important;
}
.body-content {
align-self: stretch;
height: calc(100dvh - 86px) !important;
display: flex;
}
.content {
padding: 0.5rem 1.5rem;
align-self: stretch !important;
width: 100%;
}
.manage {
width: 100dvw;
}
footer {
background: var(--neutral-layer-4);
color: var(--neutral-foreground-rest);
align-items: center;
padding: 10px 10px;
}
footer a {
color: var(--neutral-foreground-rest);
text-decoration: none;
}
footer a:focus {
outline: 1px dashed;
outline-offset: 3px;
}
footer a:hover {
text-decoration: underline;
}
.alert {
border: 1px dashed var(--accent-fill-rest);
padding: 5px;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
margin: 20px 0;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::before {
content: "An error has occurred. "
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
@media (max-width: 600px) {
.header-gutters {
margin: 0.5rem 3rem 0.5rem 1.5rem !important;
}
[dir="rtl"] .header-gutters {
margin: 0.5rem 1.5rem 0.5rem 3rem !important;
}
.main {
flex-direction: column !important;
row-gap: 0 !important;
}
nav.sitenav {
width: 100%;
height: 100%;
}
#main-menu {
width: 100% !important;
}
#main-menu > div:first-child:is(.expander) {
display: none;
}
.navmenu {
width: 100%;
}
#navmenu-toggle {
appearance: none;
}
#navmenu-toggle ~ nav {
display: none;
}
#navmenu-toggle:checked ~ nav {
display: block;
}
.navmenu-icon {
cursor: pointer;
z-index: 10;
display: block;
position: absolute;
top: 15px;
left: unset;
right: 20px;
width: 20px;
height: 20px;
border: none;
}
[dir="rtl"] .navmenu-icon {
left: 20px;
right: unset;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.DatTools", "Sabr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.Reports", "SabreTools\SabreTools.Reports\SabreTools.Reports.csproj", "{E73767A7-0A65-4F89-B149-A520874F7B32}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RomRepoMgr.Blazor", "RomRepoMgr.Blazor\RomRepoMgr.Blazor.csproj", "{30DA0637-76C5-43DE-8203-403AECF5F859}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -78,5 +80,9 @@ Global
{E73767A7-0A65-4F89-B149-A520874F7B32}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E73767A7-0A65-4F89-B149-A520874F7B32}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E73767A7-0A65-4F89-B149-A520874F7B32}.Release|Any CPU.Build.0 = Release|Any CPU
{30DA0637-76C5-43DE-8203-403AECF5F859}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{30DA0637-76C5-43DE-8203-403AECF5F859}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30DA0637-76C5-43DE-8203-403AECF5F859}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30DA0637-76C5-43DE-8203-403AECF5F859}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal