47 Commits

Author SHA1 Message Date
475da761a5 [App] When an error occurs in a DAT importer worker, keep raising workers. 2025-07-29 16:39:34 +01:00
5d1ef578a1 [App] Ensure DAT importer model events run on UI thread. 2025-07-29 16:38:59 +01:00
0903d2b5cc [App] Create bundle when publishing for macOS. 2025-07-29 01:06:30 +01:00
6f54ed2e80 Add application icon for Windows. 2025-07-29 01:05:52 +01:00
244bf4c7a1 Add README 2025-07-28 18:38:46 +01:00
4eef291174 [App] Ensure Linux uses specific Skia version.
Yes, again.
2025-07-28 05:03:54 +01:00
627c59bcd0 [Project] Add global.json to allow it to compile if .NET 10 SDK is installed. 2025-07-28 05:02:04 +01:00
0c5760e745 Add more missing usings..... 2025-07-27 18:30:48 +01:00
34c8edbd91 Add version and copyright to assemblies. 2025-07-27 18:30:24 +01:00
bdcb5e72f7 [Blazor] Fix missing usings
(what is happening?)
2025-07-27 18:29:58 +01:00
8c5e0ef99e [App] Publish single file 2025-07-27 18:26:27 +01:00
ab804239bb [Blazor] Use settings from appsettings.json to configure folders and log levels. 2025-07-27 18:19:01 +01:00
b62f495824 [Blazor] Fix missing using. 2025-07-27 17:51:41 +01:00
7e7660bf6c [Dependencies] Update Roslynator analyzers to version 4.14.0 2025-07-27 17:50:46 +01:00
cb4d1706f8 [Dependencies] Update .NET to 9.0.7 2025-07-27 17:50:11 +01:00
2928ac3e1f Use ReadExactly instead EnsureRead extension. 2025-07-27 17:49:50 +01:00
8c116535c5 General refactor and cleanup. 2025-07-27 17:46:22 +01:00
a551e2474d [Blazor] Add localization. 2025-07-27 17:19:25 +01:00
396ee08030 [Blazor] Enable single file publish. 2025-07-27 16:15:55 +01:00
058b8e4b92 [Blazor] Implement importing ROMs. 2025-07-27 14:32:04 +01:00
a290996037 [Blazor] Move importing DAT to single thread.
Does not take so much longer, but UX is much improved.
2025-07-27 12:16:39 +01:00
e1ced26bc5 [Blazor] Update footer. 2025-07-27 04:04:52 +01:00
9c91d76561 [Blazor] Implement home page. 2025-07-27 04:02:17 +01:00
cf7186adbb [Blazor] Implement importing DAT folder. 2025-07-27 03:30:51 +01:00
122e397d0a [Blazor] Enable site wide server side rendering. 2025-07-27 03:30:32 +01:00
a2fc47cc5b [Blazor] On development show detailed circuit error information. 2025-07-27 03:29:55 +01:00
80f1c0e28e [Blazor] Set up settings on startup. 2025-07-27 03:29:21 +01:00
f4b87f68ec [Blazor] Add menu of actions. 2025-07-26 22:22:01 +01:00
dc4646512a [Blazor] Add database. 2025-07-26 21:16:23 +01:00
00d005ba98 [Blazor] Ensure folders exist. 2025-07-26 19:04:34 +01:00
9877f3886e Add debug logs. 2025-07-26 18:44:39 +01:00
5bb39929b4 [Blazor] Always use dark mode. 2025-07-26 18:11:05 +01:00
72b2eed55c Disable HTTPS redirection. 2025-07-26 18:00:30 +01:00
cca4af8d6d [Blazor] Add Serilog. 2025-07-26 17:59:18 +01:00
8df9bb7211 Add blazor skeleton. 2025-07-26 17:55:02 +01:00
52946eca2e Apply standard styles to all views. 2025-07-26 17:21:54 +01:00
de9ad80eef Remove spurious debug message. 2025-07-26 17:02:45 +01:00
a7985955b4 Add debug message of how long it took to import ROMs from folder. 2025-07-26 17:02:02 +01:00
054b9ffd0d When using internal decompressor, bypass extracting files to temporary folder.
Not needed and is slower.
2025-07-26 17:01:30 +01:00
a8921f640d Fix settings dialog not showing internal decompressor option correctly. 2025-07-26 17:00:21 +01:00
3e731115f2 Allow to read archives if no unar, but internal decompressor is allowed. 2025-07-26 17:00:02 +01:00
dc630f3e78 Fix reading internal decompressor settings in macOS. 2025-07-26 16:59:25 +01:00
5937e0d83e Add support for internal decompressor. 2025-07-26 04:17:26 +01:00
8f789484ca Add support for uncompressed repository. 2025-07-25 22:55:30 +01:00
0bda03afee Add support to compress repository with zstd. 2025-07-25 17:49:36 +01:00
bf19439e49 Use Serilog in EntityFramework Core logging. 2025-07-24 21:23:53 +01:00
1435aa10ee Adjust visibility of items in import ROM view. 2025-07-24 21:13:45 +01:00
87 changed files with 3731 additions and 975 deletions

View File

@@ -15,23 +15,26 @@
<PackageVersion Include="ErrorProne.NET.Structs" Version="0.1.2"/>
<PackageVersion Include="InclusivenessAnalyzer" Version="1.3.0"/>
<PackageVersion Include="MessageBox.Avalonia" Version="3.2.0"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.6"/>
<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.EntityFrameworkCore.Design" Version="9.0.7"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.7"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.7"/>
<PackageVersion Include="Microsoft.Extensions.Localization" Version="9.0.7"/>
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.7"/>
<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"/>
<PackageVersion Include="Philips.CodeAnalysis.MaintainabilityAnalyzers" Version="1.6.3"/>
<PackageVersion Include="plist-cil" Version="2.2.0"/>
<PackageVersion Include="Roslynator.Analyzers" Version="4.13.1"/>
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.13.1"/>
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.13.1"/>
<PackageVersion Include="Roslynator.Analyzers" Version="4.14.0"/>
<PackageVersion Include="Roslynator.CodeAnalysis.Analyzers" Version="4.14.0"/>
<PackageVersion Include="Roslynator.Formatting.Analyzers" Version="4.14.0"/>
<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="Serilog.Sinks.File" Version="7.0.0"/>
<PackageVersion Include="SharpCompress" Version="0.39.0"/>
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31"/>
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0"/>
@@ -45,6 +48,9 @@
<PackageVersion Include="System.IO.Compression" Version="4.3.0"/>
<PackageVersion Include="SharpCompress" Version="0.38.0"/>
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0"/>
<PackageVersion Include="ZstdSharp.Port" Version="0.8.1"/>
<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"/>
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.116.1" Condition="$(RuntimeIdentifier.StartsWith('linux'))" />
</ItemGroup>
</Project>

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
<div style="text-align: center;">
<img src="logo.png" alt="logo"/>
</div>
# ROM Repository Manager
ROM Repository Manager is a versatile tool designed to manage your cold storage of ROM sets effortlessly. Whether you're a retro gaming enthusiast or a dedicated collector, ROM Repository Manager streamlines the process of organizing, compressing, and deduplicating your ROM sets.
## Features
* **Organize ROMs**: Automatically sort and categorize ROM files based on the metadata found in widely available DAT files.
* **Deduplicate**: Optimize your storage by ensuring each ROM is stored only once, even if it appears in multiple sets or duplicates within the same set.
* **Compress:** Reduce the size of your ROMs with advanced compression options. Choose LZIP for maximum space savings or ZSTD for faster performance, both surpassing the typical ZIP files used in ROM sets.
* **Cross-Platform**: Compatible with Windows, macOS, and Linux.
* **User-Friendly Interface**: Intuitive and easy-to-use interface for seamless management. Choose between the desktop application for direct access or deploy it on a server to use through your browser (e.g., in a NAS). The choice is yours!
* **Virtual filesystem**: A standout feature of our desktop application is its ability to provide direct access to the ROM set from our repository. This is achieved through a virtual filesystem, allowing you to view and interact with the ROM set in its original folder structure without the need for extraction.
## Applications
ROM Repository Manager contains two different applications:
* **Desktop Application**: Built using .NET, this application provides a robust and feature-rich interface for managing ROM repositories on your computer.
* **Blazor Application**: A web-based application built with .NET Blazor, offering seamless access to ROM management features through your browser.
Both applications are available as pre-compiled binaries in the releases section. Users do not need to compile anything themselves.
## Usage
1. Launch the application.
2. Import the DAT files to get started.
3. Import your ROM sets.
4. Let ROM Repository Manager store your ROMs deduplicated and compressed.
## How does it compare?
When comparing with the ROM set from MAME 0.278, which occupies 78.2Gb compressed, 169Gb uncompressed:
* Using LZIP compression, ROM Repository Manager reduces the size to **68.5Gb**.
* Using ZSTD compression, the size is reduced to **71.4Gb**.
### Let the filesystem do it
A filesystem can improve our deduplication, adding block-level optimization on top of our file-level strategy, and achieving slightly better results. For example:
* **ZFS**: Reduces the repository to **71.3Gb** with ZSTD compression.
* **btrfs**: Reduces the repository to **85.6Gb**.
While these options deliver marginal gains in space and speed, they come at the cost of significantly higher RAM usage. Consider them only if your system has the resources to support it.
## Blazor Application Configuration
To configure the Blazor application, you need to modify the `appsettings.json` file. Below are the valid fields you can change, along with their purposes:
- **LogLevel**: Adjusts the level of detail written to the logs. Valid values are`Information` and `Debug`
- **LogFile**: Specifies the location where the log is written.
- **Repository**: Defines the location where the repository resides.
- **ImportRoms**: Sets the location where the application will look for ROMs to import.
- **ImportDats**: Sets the location where the application will look for DATs to import.
- **ExportRoms**: Specifies the location where the application will export ROMs.
- **ExportDats**: Specifies the location where the application will export DATs.
- **Database**: Defines the location where the repository database will reside.
- **Temporary**: Specifies the location where the application will store temporary files.
- **CompressionType**: Determines the repository compression algorithm used. This setting only applies to newly added ROMs. Valid values are `Lzip`, `Zstd` and `None`
## Contributing
Contributions are welcome! If you have ideas for new features or improvements, feel free to open an issue or submit a pull request.
## License
This project is licensed under the GPL version 3. See the LICENSE file for details.
---
For more information, visit the [GitHub repository](https://github.com/claunia/RomRepoMgr).
© 2020-2025 [Nat Portillo](https://www.natportillo.es)
Happy ROM managing!

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,29 @@
@using Microsoft.Extensions.Localization
@using RomRepoMgr.Blazor.Resources
@implements IDialogContentComponent
@inject ILogger<ImportDats> Logger
@inject IStringLocalizer<Localization> Localizer
@inject IConfiguration Configuration
<FluentDialog Width="800px" Height="400px" Title="Import DATs" Modal="true" TrapFocus="true">
<FluentDialogBody>
<div>
<p hidden="@IsBusy">@string.Format(Localizer["DATs_will_be_imported_from"], path)</p>
<FluentLabel Color="@StatusColor">@StatusMessage</FluentLabel>
<FluentProgress Max="@ProgressMax" Min="@ProgressMin" Value="@ProgressValue" Visible="@ProgressVisible"/>
</div>
@if(Progress2Visible)
{
<div>
<p>@StatusMessage2</p>
<FluentProgress Max="@Progress2Max" Min="@Progress2Min" Value="@Progress2Value"/>
</div>
}
</FluentDialogBody>
<FluentDialogFooter>
<FluentStack Orientation="Orientation.Horizontal">
<FluentButton OnClick="@Start" Disabled="@IsBusy">@Localizer["Start"]</FluentButton>
<FluentButton OnClick="@CloseAsync" Disabled="@CannotClose">@Localizer["Close"]</FluentButton>
</FluentStack>
</FluentDialogFooter>
</FluentDialog>

View File

@@ -0,0 +1,181 @@
using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Workers;
using Serilog;
using Serilog.Extensions.Logging;
using ErrorEventArgs = RomRepoMgr.Core.EventArgs.ErrorEventArgs;
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 string StatusMessage2 { get; set; }
public bool Progress2Visible { get; set; }
public int? Progress2Max { get; set; }
public int? Progress2Min { get; set; }
public int? Progress2Value { get; set; }
public Color? StatusColor { get; set; }
/// <inheritdoc />
protected override void OnInitialized()
{
base.OnInitialized();
path = Configuration["DataFolders:ImportDats"] ?? "incoming-dats";
StatusMessage = "";
StatusMessage2 = "";
IsBusy = false;
CannotClose = false;
ProgressVisible = false;
Progress2Visible = false;
}
void Start()
{
IsBusy = true;
CannotClose = true;
ProgressVisible = true;
ProgressValue = null;
StatusMessage = Localizer["SearchingForFiles"];
_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(Localizer["FoundFiles"], _datFiles.Length);
ProgressMin = 0;
ProgressMax = _datFiles.Length;
ProgressValue = 0;
Progress2Visible = true;
_listPosition = 0;
_workers = 0;
StateHasChanged();
_stopwatch.Restart();
Logger.LogDebug("Starting to import DAT files...");
Import();
}
void Import()
{
if(_listPosition >= _datFiles.Length)
{
_ = InvokeAsync(() =>
{
ProgressVisible = false;
Progress2Visible = false;
StatusMessage = Localizer["Finished"];
CannotClose = false;
StateHasChanged();
});
_stopwatch.Stop();
Logger.LogDebug("Took {TotalSeconds} seconds to import {Length} DAT files",
_stopwatch.Elapsed.TotalSeconds,
_datFiles.Length);
return;
}
_ = InvokeAsync(() =>
{
StatusMessage = string.Format(Localizer["ImportingItem"], Path.GetFileName(_datFiles[_listPosition]));
ProgressValue = _listPosition;
StateHasChanged();
});
var worker = new DatImporter(_datFiles[_listPosition], null, new SerilogLoggerFactory(Log.Logger));
worker.ErrorOccurred += OnWorkerOnErrorOccurred;
worker.SetIndeterminateProgress += OnWorkerOnSetIndeterminateProgress;
worker.SetMessage += OnWorkerOnSetMessage;
worker.SetProgress += OnWorkerOnSetProgress;
worker.SetProgressBounds += OnWorkerOnSetProgressBounds;
worker.WorkFinished += OnWorkerOnWorkFinished;
_ = Task.Run(worker.Import);
}
void OnWorkerOnWorkFinished(object? sender, MessageEventArgs args)
{
_listPosition++;
Import();
}
void OnWorkerOnSetProgressBounds(object? sender, ProgressBoundsEventArgs args)
{
_ = InvokeAsync(() =>
{
Progress2Value = 0;
Progress2Max = (int?)args.Maximum;
Progress2Min = (int?)args.Minimum;
StateHasChanged();
});
}
void OnWorkerOnSetProgress(object? sender, ProgressEventArgs args)
{
_ = InvokeAsync(() =>
{
Progress2Value = (int?)args.Value;
StateHasChanged();
});
}
void OnWorkerOnSetMessage(object? sender, MessageEventArgs args)
{
_ = InvokeAsync(() =>
{
StatusMessage2 = args.Message;
StateHasChanged();
});
}
void OnWorkerOnSetIndeterminateProgress(object? sender, EventArgs args)
{
_ = InvokeAsync(() =>
{
Progress2Value = null;
StateHasChanged();
});
}
void OnWorkerOnErrorOccurred(object? sender, ErrorEventArgs args)
{
_ = InvokeAsync(() =>
{
_listPosition++;
Import();
});
}
Task CloseAsync() => Dialog.CloseAsync();
}

View File

@@ -0,0 +1,43 @@
@using Microsoft.Extensions.Localization
@using RomRepoMgr.Blazor.Resources
@using RomRepoMgr.Database
@implements IDialogContentComponent
@inject ILogger<ImportRoms> Logger
@inject Context _ctx
@inject IStringLocalizer<Localization> Localizer
@inject IConfiguration Configuration
<FluentDialog Width="1600px" Height="800px" Title="Import DATs" Modal="true" TrapFocus="true">
<FluentDialogBody>
<p hidden="@IsBusy">@string.Format(Localizer["ROMs_will_be_imported_from"], FolderPath)</p>
@if(NotYetStarted)
{
<FluentCheckbox @bind-Value="@RemoveFilesChecked" Label="@Localizer["RemoveFilesLabel"]"/>
<FluentCheckbox @bind-Value="@KnownOnlyChecked" Label="@Localizer["KnownOnlyLabel"]"/>
<FluentCheckbox @bind-Value="@RecurseArchivesChecked"
Label=@Localizer["RecurseArchivesLabel"]/>
}
@if(Importing)
{
<div>
<FluentLabel Color="@StatusMessageColor">@StatusMessage</FluentLabel>
<FluentProgress Max="@ProgressMax" Min="@ProgressMin" Value="@ProgressValue"/>
</div>
}
@if(Progress2Visible)
{
<div>
<FluentLabel>@StatusMessage2</FluentLabel>
<FluentProgress Max="@Progress2Max" Min="@Progress2Min" Value="@Progress2Value"/>
</div>
}
</FluentDialogBody>
<FluentDialogFooter>
<FluentStack Orientation="Orientation.Horizontal">
<FluentButton OnClick="@Start" Disabled="@IsBusy">@Localizer["Start"]</FluentButton>
<FluentButton OnClick="@CloseAsync" Disabled="@CannotClose">@Localizer["Close"]</FluentButton>
</FluentStack>
</FluentDialogFooter>
</FluentDialog>

View File

@@ -0,0 +1,355 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using Microsoft.AspNetCore.Components;
using Microsoft.FluentUI.AspNetCore.Components;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Workers;
using RomRepoMgr.Database.Models;
using Serilog;
using SharpCompress.Readers;
namespace RomRepoMgr.Blazor.Components.Dialogs;
public partial class ImportRoms : ComponentBase
{
readonly Stopwatch _mainStopwatch = new();
readonly ConcurrentBag<DbDisk> _newDisks = [];
readonly ConcurrentBag<DbFile> _newFiles = [];
readonly ConcurrentBag<DbMedia> _newMedias = [];
readonly Stopwatch _stopwatch = new();
int _listPosition;
FileImporter _rootImporter;
PaginationState? pagination;
[CascadingParameter]
public FluentDialog Dialog { get; set; }
public bool IsBusy { get; set; }
public bool NotYetStarted { get; set; }
public bool RemoveFilesChecked { get; set; }
public bool KnownOnlyChecked { get; set; }
public bool RecurseArchivesChecked { get; set; }
public Color? StatusMessageColor { get; set; }
public string? StatusMessage { get; set; }
public int? ProgressMax { get; set; }
public int? ProgressMin { get; set; }
public int? ProgressValue { get; set; }
public string? StatusMessage2 { get; set; }
public int? Progress2Max { get; set; }
public int? Progress2Min { get; set; }
public int? Progress2Value { get; set; }
public string? FolderPath { get; set; }
public bool Importing { get; set; }
public bool Progress2Visible { get; set; }
public bool DataGridVisible { get; set; }
public bool CannotClose { get; set; }
protected override void OnInitialized()
{
base.OnInitialized();
FolderPath = Configuration["DataFolders:ImportRoms"] ?? "incoming";
IsBusy = false;
NotYetStarted = true;
RemoveFilesChecked = false;
KnownOnlyChecked = true;
RecurseArchivesChecked = true;
StatusMessage = "";
Importing = false;
Progress2Visible = false;
DataGridVisible = false;
Logger.LogDebug("ImportRoms dialog initialized with Path: {Path}", FolderPath);
}
void Start()
{
_rootImporter = new FileImporter(_ctx, _newFiles, _newDisks, _newMedias, KnownOnlyChecked, RemoveFilesChecked);
_rootImporter.SetMessage += SetMessage;
_rootImporter.SetIndeterminateProgress += SetIndeterminateProgress;
_rootImporter.SetProgress += SetProgress;
_rootImporter.SetProgressBounds += SetProgressBounds;
_rootImporter.Finished += EnumeratingFilesFinished;
ProgressValue = null;
Importing = true;
CannotClose = true;
IsBusy = true;
NotYetStarted = false;
_mainStopwatch.Start();
_ = Task.Run(() => _rootImporter.FindFiles(FolderPath));
}
void SetProgressBounds(object? sender, ProgressBoundsEventArgs e)
{
_ = InvokeAsync(() =>
{
ProgressValue = 0;
ProgressMax = (int?)e.Maximum;
ProgressMin = (int?)e.Minimum;
});
}
void SetProgress(object? sender, ProgressEventArgs e)
{
_ = InvokeAsync(() =>
{
ProgressValue = (int?)e.Value;
StateHasChanged();
});
}
void SetIndeterminateProgress(object? sender, EventArgs e)
{
_ = InvokeAsync(() =>
{
ProgressValue = null;
StateHasChanged();
});
}
void SetMessage(object? sender, MessageEventArgs e)
{
_ = InvokeAsync(() =>
{
StatusMessage = e.Message;
StateHasChanged();
});
}
Task CloseAsync() => Dialog.CloseAsync();
void EnumeratingFilesFinished(object? sender, EventArgs e)
{
_rootImporter.Finished -= EnumeratingFilesFinished;
if(RecurseArchivesChecked)
{
_ = InvokeAsync(() =>
{
Progress2Visible = true;
StateHasChanged();
});
_rootImporter.SetMessage2 += SetMessage2;
_rootImporter.SetIndeterminateProgress2 += SetIndeterminateProgress2;
_rootImporter.SetProgress2 += SetProgress2;
_rootImporter.SetProgressBounds2 += SetProgress2Bounds;
_rootImporter.Finished += CheckArchivesFinished;
_ = Task.Run(() =>
{
_stopwatch.Restart();
_rootImporter.SeparateFilesAndArchivesManaged();
});
}
else
ProcessFiles();
}
void SetProgress2Bounds(object? sender, ProgressBoundsEventArgs e)
{
_ = InvokeAsync(() =>
{
Progress2Value = 0;
Progress2Max = (int?)e.Maximum;
Progress2Min = (int?)e.Minimum;
});
}
void SetProgress2(object? sender, ProgressEventArgs e)
{
_ = InvokeAsync(() =>
{
Progress2Value = (int?)e.Value;
StateHasChanged();
});
}
void SetIndeterminateProgress2(object? sender, EventArgs e)
{
_ = InvokeAsync(() =>
{
Progress2Value = null;
StateHasChanged();
});
}
void SetMessage2(object? sender, MessageEventArgs e)
{
_ = InvokeAsync(() =>
{
StatusMessage2 = e.Message;
StateHasChanged();
});
}
void ProcessFiles()
{
_ = InvokeAsync(() =>
{
ProgressMin = 0;
ProgressMax = _rootImporter.Files.Count;
ProgressValue = 0;
NotYetStarted = false;
CannotClose = true;
IsBusy = true;
Importing = true;
StateHasChanged();
});
_listPosition = 0;
_stopwatch.Restart();
Parallel.ForEach(_rootImporter.Files,
file =>
{
_ = InvokeAsync(() =>
{
StatusMessage = string.Format(Localizer["ImportingItem"], Path.GetFileName(file));
ProgressValue = _listPosition;
StateHasChanged();
});
var worker = new FileImporter(_ctx,
_newFiles,
_newDisks,
_newMedias,
KnownOnlyChecked,
RemoveFilesChecked);
worker.ImportFile(file);
Interlocked.Increment(ref _listPosition);
});
_stopwatch.Stop();
Log.Debug("Took {TotalSeconds} seconds to process files", _stopwatch.Elapsed.TotalSeconds);
_rootImporter.SaveChanges();
_rootImporter.UpdateRomStats();
_ = InvokeAsync(() =>
{
ProgressMin = 0;
ProgressMax = 1;
ProgressValue = 0;
CannotClose = false;
IsBusy = false;
Importing = false;
StatusMessage = Localizer["Finished"];
StateHasChanged();
});
_listPosition = 0;
_mainStopwatch.Stop();
Log.Debug("Took {TotalSeconds} seconds to import ROMs", _mainStopwatch.Elapsed.TotalSeconds);
}
void CheckArchivesFinished(object? sender, EventArgs e)
{
_stopwatch.Stop();
Log.Debug("Took {TotalSeconds} seconds to check archives", _stopwatch.Elapsed.TotalSeconds);
_ = InvokeAsync(() =>
{
Progress2Visible = false;
StateHasChanged();
});
_rootImporter.Finished -= CheckArchivesFinished;
ProcessArchivesManaged();
}
void ProcessArchivesManaged()
{
_ = InvokeAsync(() =>
{
ProgressMax = _rootImporter.Archives.Count;
ProgressMin = 0;
ProgressValue = 0;
Progress2Visible = false;
StateHasChanged();
});
_listPosition = 0;
_stopwatch.Restart();
// For each archive
Parallel.ForEach(_rootImporter.Archives,
archive =>
{
_ = InvokeAsync(() =>
{
StatusMessage =
string.Format(Localizer["ProcessingArchive"], Path.GetFileName(archive));
ProgressValue = _listPosition;
StateHasChanged();
});
// Create FileImporter
var archiveImporter = new FileImporter(_ctx,
_newFiles,
_newDisks,
_newMedias,
KnownOnlyChecked,
RemoveFilesChecked);
// Open archive
try
{
using var fs = new FileStream(archive, FileMode.Open, FileAccess.Read);
using IReader reader = ReaderFactory.Open(fs);
// Process files in archive
while(reader.MoveToNextEntry())
{
if(reader.Entry.IsDirectory) continue;
if(reader.Entry.Crc == 0 && KnownOnlyChecked) continue;
if(!archiveImporter.IsCrcInDb(reader.Entry.Crc) && KnownOnlyChecked) continue;
var worker = new FileImporter(_ctx,
_newFiles,
_newDisks,
_newMedias,
KnownOnlyChecked,
RemoveFilesChecked);
worker.ImportAndHashRom(reader.OpenEntryStream(),
reader.Entry.Key,
Path.Combine(Settings.Settings.Current.RepositoryPath,
Path.GetFileName(Path.GetTempFileName())),
reader.Entry.Size);
}
}
catch(InvalidOperationException) {}
finally
{
Interlocked.Increment(ref _listPosition);
}
});
_stopwatch.Stop();
Log.Debug("Took {TotalSeconds} seconds to process archives", _stopwatch.Elapsed.TotalSeconds);
_ = InvokeAsync(() =>
{
Progress2Visible = false;
StateHasChanged();
});
ProcessFiles();
}
}

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,37 @@
@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,40 @@
@page "/"
@using Microsoft.Extensions.Localization
@using RomRepoMgr.Blazor.Resources
@using RomRepoMgr.Database
@rendermode InteractiveServer
@inject IDialogService DialogService
@inject Context ctx
@inject IStringLocalizer<Localization> Localizer
<PageTitle>ROM Repository Manager</PageTitle>
<FluentToolbar>
<FluentButton OnClick="@ImportDatsAsync">@Localizer["ImportDats"]</FluentButton>
<FluentButton Disabled="true">@Localizer["ExportDat"]</FluentButton>
<FluentButton Disabled="true">@Localizer["RemoveDat"]</FluentButton>
<FluentButton OnClick="@ImportRomsAsync">@Localizer["ImportRoms"]</FluentButton>
<FluentButton Disabled="true">@Localizer["ExportRoms"]</FluentButton>
</FluentToolbar>
<FluentDataGrid @ref="romSetsGrid" Items="@RomSets" Style="width: 100%;" AutoFit="true" Pagination="@pagination"
AutoItemsPerPage="true" ResizableColumns="true">
<PropertyColumn Property="@(p => p.Name)" Title="@Localizer["RomSetNameLabel"]"/>
<PropertyColumn Property="@(p => p.Version)" Title="@Localizer["RomSetVersionLabel"]"/>
<PropertyColumn Property="@(p => p.Author)" Title="@Localizer["RomSetAuthorLabel"]"/>
<PropertyColumn Property="@(p => p.Category)" Title="@Localizer["RomSetCategoryLabel"]"/>
<PropertyColumn Property="@(p => p.Date)" Title="@Localizer["RomSetDateLabel"]"/>
<PropertyColumn Property="@(p => p.Description)" Title="@Localizer["RomSetDescriptionLabel"]"/>
<PropertyColumn Property="@(p => p.Comment)" Title="@Localizer["RomSetCommentLabel"]"/>
<PropertyColumn Property="@(p => p.Homepage)" Title="@Localizer["HomepageLabel"]"/>
<PropertyColumn Property="@(p => p.TotalMachines)" Title="@Localizer["RomSetTotalMachinesLabel"]"/>
<PropertyColumn Property="@(p => p.CompleteMachines)" Title="@Localizer["RomSetCompleteMachinesLabel"]"/>
<PropertyColumn Property="@(p => p.IncompleteMachines)" Title="@Localizer["RomSetIncompleteMachinesLabel"]"/>
<PropertyColumn Property="@(p => p.TotalRoms)" Title="@Localizer["RomSetTotalRomsLabel"]"/>
<PropertyColumn Property="@(p => p.HaveRoms)" Title="@Localizer["RomSetHaveRomsLabel"]"/>
<PropertyColumn Property="@(p => p.MissRoms)" Title="@Localizer["RomSetMissRomsLabel"]"/>
</FluentDataGrid>
<FluentPaginator State="@pagination"/>

View File

@@ -0,0 +1,64 @@
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());
}
async Task ImportRomsAsync()
{
IDialogReference dialog = await DialogService.ShowDialogAsync<ImportRoms>(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,9 @@
@using RomRepoMgr.Blazor.Components.Layout
<FluentDesignTheme StorageName="theme" Mode="DesignThemeModes.Dark">
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView DefaultLayout="typeof(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,172 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using Microsoft.FluentUI.AspNetCore.Components;
using RomRepoMgr.Blazor.Components;
using RomRepoMgr.Database;
using RomRepoMgr.Settings;
using Serilog;
using Serilog.Events;
// Start the application
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().Enrich.FromLogContext().CreateLogger();
Log.Information("Welcome to ROM Repository Manager!");
Log.Information("Copyright © 2020-2025 Natalia Portillo");
// Configuration and settings
// We need the builder now
Log.Debug("Creating the builder...");
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Access the full configuration
Log.Debug("Creating the configuration reader...");
ConfigurationManager config = builder.Configuration;
string logFile = config["LogFile"] ?? "logs/rom-repo-mgr.log";
string defaultLogLevel = config["Logging:LogLevel:Default"] ?? "Information";
// Parse to LogEventLevel
if(!Enum.TryParse(defaultLogLevel, true, out LogEventLevel level))
{
// Fallback if parsing fails
#if DEBUG
level = LogEventLevel.Debug;
#else
level = LogEventLevel.Information;
#endif
}
// Now create a logger with the specified log level and log file
Log.Logger = new LoggerConfiguration()
#if DEBUG
.MinimumLevel.Debug()
#else
.MinimumLevel.Information()
#endif
.WriteTo.Console()
.WriteTo.File(logFile, rollingInterval: RollingInterval.Day, fileSizeLimitBytes: 10 * 1048576)
.Enrich.FromLogContext()
.CreateLogger();
// Read the rest of the configuration and settings
Log.Debug("Reading configuration settings...");
string repoFolder = config["DataFolders:Repository"] ?? "repo";
string importRoms = config["DataFolders:ImportRoms"] ?? "incoming";
string importDats = config["DataFolders:ImportDats"] ?? "incoming-dats";
string exportRoms = config["DataFolders:ExportRoms"] ?? "export";
string exportDats = config["DataFolders:ExportDats"] ?? "export-dats";
string databaseFolder = config["DataFolders:Database"] ?? "db";
string temporaryFolder = config["DataFolders:Temporary"] ?? "tmp";
string compressionTypeString = config["CompressionType"] ?? "Zstd";
// Parse the compression type
if(!Enum.TryParse(compressionTypeString, true, out CompressionType compressionType))
{
// Fallback if parsing fails
compressionType = CompressionType.Zstd;
}
// Ensure the folders exist
Log.Information("Ensuring folders exist...");
string[] folders = [repoFolder, importRoms, importDats, databaseFolder, exportRoms, exportDats, temporaryFolder];
foreach(string folder in folders)
{
// Check File.Exists for symlinks or junctions
if(!Directory.Exists(folder) && !File.Exists(folder))
{
Log.Debug("Creating folder: {Folder}", folder);
Directory.CreateDirectory(folder);
}
else
Log.Debug("Folder already exists: {Folder}", folder);
}
// ✅ Plug Serilog into the host
builder.Host.UseSerilog();
// Add services to the container.
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddFluentUIComponents();
// Localization
builder.Services.AddLocalization();
Log.Debug("Creating database context...");
builder.Services.AddDbContextFactory<Context>(options =>
{
options.UseSqlite($"Data Source={databaseFolder}/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, databaseFolder, "database.db"),
TemporaryFolder = Path.Combine(Environment.CurrentDirectory, temporaryFolder),
RepositoryPath = Path.Combine(Environment.CurrentDirectory, repoFolder),
UseInternalDecompressor = true,
Compression = compressionType
};
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();
// Localization
string[] supportedCultures = ["en", "es"];
RequestLocalizationOptions localizationOptions = new RequestLocalizationOptions().SetDefaultCulture("en")
.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures);
app.UseRequestLocalization(localizationOptions);
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,228 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace RomRepoMgr.Blazor.Resources {
using System;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Localization {
private static System.Resources.ResourceManager resourceMan;
private static System.Globalization.CultureInfo resourceCulture;
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Localization() {
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
public static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("RomRepoMgr.Blazor.Resources.Localization", typeof(Localization).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
public static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
public static string ROMs_will_be_imported_from {
get {
return ResourceManager.GetString("ROMs_will_be_imported_from", resourceCulture);
}
}
public static string RemoveFilesLabel {
get {
return ResourceManager.GetString("RemoveFilesLabel", resourceCulture);
}
}
public static string KnownOnlyLabel {
get {
return ResourceManager.GetString("KnownOnlyLabel", resourceCulture);
}
}
public static string RecurseArchivesLabel {
get {
return ResourceManager.GetString("RecurseArchivesLabel", resourceCulture);
}
}
public static string Start {
get {
return ResourceManager.GetString("Start", resourceCulture);
}
}
public static string Close {
get {
return ResourceManager.GetString("Close", resourceCulture);
}
}
public static string ImportingItem {
get {
return ResourceManager.GetString("ImportingItem", resourceCulture);
}
}
public static string ProcessingArchive {
get {
return ResourceManager.GetString("ProcessingArchive", resourceCulture);
}
}
public static string Finished {
get {
return ResourceManager.GetString("Finished", resourceCulture);
}
}
public static string DATs_will_be_imported_from {
get {
return ResourceManager.GetString("DATs_will_be_imported_from", resourceCulture);
}
}
public static string SearchingForFiles {
get {
return ResourceManager.GetString("SearchingForFiles", resourceCulture);
}
}
public static string RomSetNameLabel {
get {
return ResourceManager.GetString("RomSetNameLabel", resourceCulture);
}
}
public static string RomSetVersionLabel {
get {
return ResourceManager.GetString("RomSetVersionLabel", resourceCulture);
}
}
public static string RomSetAuthorLabel {
get {
return ResourceManager.GetString("RomSetAuthorLabel", resourceCulture);
}
}
public static string RomSetCategoryLabel {
get {
return ResourceManager.GetString("RomSetCategoryLabel", resourceCulture);
}
}
public static string RomSetDateLabel {
get {
return ResourceManager.GetString("RomSetDateLabel", resourceCulture);
}
}
public static string RomSetDescriptionLabel {
get {
return ResourceManager.GetString("RomSetDescriptionLabel", resourceCulture);
}
}
public static string RomSetCommentLabel {
get {
return ResourceManager.GetString("RomSetCommentLabel", resourceCulture);
}
}
public static string HomepageLabel {
get {
return ResourceManager.GetString("HomepageLabel", resourceCulture);
}
}
public static string RomSetTotalMachinesLabel {
get {
return ResourceManager.GetString("RomSetTotalMachinesLabel", resourceCulture);
}
}
public static string RomSetCompleteMachinesLabel {
get {
return ResourceManager.GetString("RomSetCompleteMachinesLabel", resourceCulture);
}
}
public static string RomSetIncompleteMachinesLabel {
get {
return ResourceManager.GetString("RomSetIncompleteMachinesLabel", resourceCulture);
}
}
public static string RomSetTotalRomsLabel {
get {
return ResourceManager.GetString("RomSetTotalRomsLabel", resourceCulture);
}
}
public static string RomSetHaveRomsLabel {
get {
return ResourceManager.GetString("RomSetHaveRomsLabel", resourceCulture);
}
}
public static string RomSetMissRomsLabel {
get {
return ResourceManager.GetString("RomSetMissRomsLabel", resourceCulture);
}
}
public static string ImportDats {
get {
return ResourceManager.GetString("ImportDats", resourceCulture);
}
}
public static string ImportRoms {
get {
return ResourceManager.GetString("ImportRoms", resourceCulture);
}
}
public static string ExportDat {
get {
return ResourceManager.GetString("ExportDat", resourceCulture);
}
}
public static string RemoveDat {
get {
return ResourceManager.GetString("RemoveDat", resourceCulture);
}
}
public static string ExportRoms {
get {
return ResourceManager.GetString("ExportRoms", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,108 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="ROMs_will_be_imported_from" xml:space="preserve">
<value>Las ROMs se importarán de {0}.</value>
</data>
<data name="DATs_will_be_imported_from" xml:space="preserve">
<value>Los ficheros DAT se importarán de {0}.</value>
</data>
<data name="Close" xml:space="preserve">
<value>Cerrar</value>
</data>
<data name="ExportDat" xml:space="preserve">
<value>Exportar DAT</value>
</data>
<data name="ExportRoms" xml:space="preserve">
<value>Exportar ROMs</value>
</data>
<data name="Finished" xml:space="preserve">
<value>Terminado</value>
</data>
<data name="HomepageLabel" xml:space="preserve">
<value>Página web</value>
</data>
<data name="ImportingItem" xml:space="preserve">
<value>Importando {0}...</value>
</data>
<data name="ImportRoms" xml:space="preserve">
<value>Importar ROMs</value>
</data>
<data name="KnownOnlyLabel" xml:space="preserve">
<value>Importar solo archivos conocidos.</value>
</data>
<data name="ProcessingArchive" xml:space="preserve">
<value>Procesando archivo: {0}</value>
</data>
<data name="RecurseArchivesLabel" xml:space="preserve">
<value>Intentar detectar archivos comprimiedos e importar su contenido.</value>
</data>
<data name="RemoveDat" xml:space="preserve">
<value>Eliminar DAT</value>
</data>
<data name="RemoveFilesLabel" xml:space="preserve">
<value>Eliminar archivos después de importar satisfactoriamente.</value>
</data>
<data name="RomSetNameLabel" xml:space="preserve">
<value>Nombre</value>
</data>
<data name="RomSetMissRomsLabel" xml:space="preserve">
<value>Faltantes</value>
</data>
<data name="RomSetIncompleteMachinesLabel" xml:space="preserve">
<value>Incompletos</value>
</data>
<data name="RomSetHaveRomsLabel" xml:space="preserve">
<value>Presentes</value>
</data>
<data name="RomSetDescriptionLabel" xml:space="preserve">
<value>Descripción</value>
</data>
<data name="RomSetDateLabel" xml:space="preserve">
<value>Incompletos</value>
</data>
<data name="RomSetCompleteMachinesLabel" xml:space="preserve">
<value>Completos</value>
</data>
<data name="RomSetCommentLabel" xml:space="preserve">
<value>Comentario</value>
</data>
<data name="RomSetCategoryLabel" xml:space="preserve">
<value>Categoría</value>
</data>
<data name="RomSetAuthorLabel" xml:space="preserve">
<value>Categoría</value>
</data>
<data name="RomSetTotalMachinesLabel" xml:space="preserve">
<value>Juegos</value>
</data>
<data name="RomSetTotalRomsLabel" xml:space="preserve">
<value>ROMs</value>
</data>
<data name="RomSetVersionLabel" xml:space="preserve">
<value>Versión</value>
</data>
<data name="SearchingForFiles" xml:space="preserve">
<value>Buscando archivos...</value>
</data>
<data name="Start" xml:space="preserve">
<value>Comenzar</value>
</data>
<data name="ImportDats" xml:space="preserve">
<value>Importar DATs</value>
</data>
</root>

View File

@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
id="root"
xmlns="">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089
</value>
</resheader>
<data name="ROMs_will_be_imported_from" xml:space="preserve">
<value>ROM files will be imported from {0}.</value>
</data>
<data name="RemoveFilesLabel" xml:space="preserve">
<value>Remove files after import successful.</value>
</data>
<data name="KnownOnlyLabel" xml:space="preserve">
<value>Only import known files.</value>
</data>
<data name="RecurseArchivesLabel" xml:space="preserve">
<value>Try to detect archives and import their contents.</value>
</data>
<data name="Start" xml:space="preserve">
<value>Start</value>
</data>
<data name="Close" xml:space="preserve">
<value>Close</value>
</data>
<data name="ImportingItem" xml:space="preserve">
<value>Importing {0}...</value>
</data>
<data name="ProcessingArchive" xml:space="preserve">
<value>Processing archive: {0}</value>
</data>
<data name="Finished" xml:space="preserve">
<value>Finished</value>
</data>
<data name="DATs_will_be_imported_from" xml:space="preserve">
<value>DAT files will be imported from {0}.</value>
</data>
<data name="SearchingForFiles" xml:space="preserve">
<value>Searching for files...</value>
</data>
<data name="RomSetNameLabel" xml:space="preserve">
<value>Name</value>
</data>
<data name="RomSetVersionLabel" xml:space="preserve">
<value>Version</value>
</data>
<data name="RomSetAuthorLabel" xml:space="preserve">
<value>Author</value>
</data>
<data name="RomSetCategoryLabel" xml:space="preserve">
<value>Category</value>
</data>
<data name="RomSetDateLabel" xml:space="preserve">
<value>Date</value>
</data>
<data name="RomSetDescriptionLabel" xml:space="preserve">
<value>Description</value>
</data>
<data name="RomSetCommentLabel" xml:space="preserve">
<value>Comment</value>
</data>
<data name="HomepageLabel" xml:space="preserve">
<value>Homepage</value>
</data>
<data name="RomSetTotalMachinesLabel" xml:space="preserve">
<value>Games</value>
</data>
<data name="RomSetCompleteMachinesLabel" xml:space="preserve">
<value>Complete</value>
</data>
<data name="RomSetIncompleteMachinesLabel" xml:space="preserve">
<value>Incomplete</value>
</data>
<data name="RomSetTotalRomsLabel" xml:space="preserve">
<value>ROMs</value>
</data>
<data name="RomSetHaveRomsLabel" xml:space="preserve">
<value>Have</value>
</data>
<data name="RomSetMissRomsLabel" xml:space="preserve">
<value>Miss</value>
</data>
<data name="ImportDats" xml:space="preserve">
<value>Import DATs</value>
</data>
<data name="ImportRoms" xml:space="preserve">
<value>Import ROMs</value>
</data>
<data name="ExportDat" xml:space="preserve">
<value>Export DAT</value>
</data>
<data name="RemoveDat" xml:space="preserve">
<value>Remove DAT</value>
</data>
<data name="ExportRoms" xml:space="preserve">
<value>Export ROMs</value>
</data>
</root>

View File

@@ -0,0 +1,54 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
<PropertyGroup>
<Version>1.0.0-beta.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0-beta.1</InformationalVersion>
<Copyright>© $([System.DateTime]::Now.Year) Natalia Portillo</Copyright>
<ApplicationIcon>romrepomgr.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite"/>
<PackageReference Include="Microsoft.Extensions.Localization"/>
<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"/>
<PackageReference Include="Serilog.Sinks.File"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RomRepoMgr.Core\RomRepoMgr.Core.csproj"/>
<ProjectReference Include="..\RomRepoMgr.Database\RomRepoMgr.Database.csproj"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Localization.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Localization.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Localization.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

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

View File

@@ -0,0 +1,20 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"LogFile": "logs/romrepomgr.log",
"AllowedHosts": "*",
"DataFolders": {
"Repository": "repo",
"ImportRoms": "incoming",
"ImportDats": "incoming-dats",
"ExportRoms": "export",
"ExportDats": "export-dats",
"Database": "db",
"Temporary": "tmp"
},
"CompressionType": "Zstd"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

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

@@ -74,17 +74,17 @@ public static class FAT
byte bpbSignature;
byte fat32Signature;
ulong hugeSectors;
var fat32Id = new byte[8];
var msxId = new byte[6];
var dosOem = new byte[8];
var atariOem = new byte[6];
byte[] fat32Id = new byte[8];
byte[] msxId = new byte[6];
byte[] dosOem = new byte[8];
byte[] atariOem = new byte[6];
ushort bootable = 0;
var bpbSector = new byte[512];
var fatSector = new byte[512];
byte[] bpbSector = new byte[512];
byte[] fatSector = new byte[512];
imageStream.Position = 0;
imageStream.EnsureRead(bpbSector, 0, 512);
imageStream.EnsureRead(fatSector, 0, 512);
imageStream.ReadExactly(bpbSector, 0, 512);
imageStream.ReadExactly(fatSector, 0, 512);
Array.Copy(bpbSector, 0x02, atariOem, 0, 6);
Array.Copy(bpbSector, 0x03, dosOem, 0, 8);
@@ -111,13 +111,13 @@ public static class FAT
string oemString = Encoding.ASCII.GetString(dosOem);
var apricotBps = BitConverter.ToUInt16(bpbSector, 0x50);
byte apricotSpc = bpbSector[0x52];
var apricotReservedSecs = BitConverter.ToUInt16(bpbSector, 0x53);
byte apricotFatsNo = bpbSector[0x55];
var apricotRootEntries = BitConverter.ToUInt16(bpbSector, 0x56);
var apricotSectors = BitConverter.ToUInt16(bpbSector, 0x58);
var apricotFatSectors = BitConverter.ToUInt16(bpbSector, 0x5B);
ushort apricotBps = BitConverter.ToUInt16(bpbSector, 0x50);
byte apricotSpc = bpbSector[0x52];
ushort apricotReservedSecs = BitConverter.ToUInt16(bpbSector, 0x53);
byte apricotFatsNo = bpbSector[0x55];
ushort apricotRootEntries = BitConverter.ToUInt16(bpbSector, 0x56);
ushort apricotSectors = BitConverter.ToUInt16(bpbSector, 0x58);
ushort apricotFatSectors = BitConverter.ToUInt16(bpbSector, 0x5B);
bool apricotCorrectSpc = apricotSpc is 1 or 2 or 4 or 8 or 16 or 32 or 64;
@@ -197,20 +197,20 @@ public static class FAT
byte z80Di = bpbSector[0];
// First FAT1 sector resides at LBA 0x14
var fat1Sector0 = new byte[512];
byte[] fat1Sector0 = new byte[512];
imageStream.Position = 0x14 * 512;
imageStream.EnsureRead(fat1Sector0, 0, 512);
imageStream.ReadExactly(fat1Sector0, 0, 512);
// First FAT2 sector resides at LBA 0x1A
var fat2Sector0 = new byte[512];
byte[] fat2Sector0 = new byte[512];
imageStream.Position = 0x1A * 512;
imageStream.EnsureRead(fat2Sector0, 0, 512);
imageStream.ReadExactly(fat2Sector0, 0, 512);
bool equalFatIds = fat1Sector0[0] == fat2Sector0[0] && fat1Sector0[1] == fat2Sector0[1];
// Volume is software interleaved 2:1
var rootMs = new MemoryStream();
var tmp = new byte[512];
byte[] tmp = new byte[512];
foreach(long position in new long[]
{
@@ -218,17 +218,17 @@ public static class FAT
})
{
imageStream.Position = position * 512;
imageStream.EnsureRead(tmp, 0, 512);
imageStream.ReadExactly(tmp, 0, 512);
rootMs.Write(tmp, 0, tmp.Length);
}
byte[] rootDir = rootMs.ToArray();
var validRootDir = true;
bool validRootDir = true;
// Iterate all root directory
for(var e = 0; e < 96 * 32; e += 32)
for(int e = 0; e < 96 * 32; e += 32)
{
for(var c = 0; c < 11; c++)
for(int c = 0; c < 11; c++)
{
if((rootDir[c + e] >= 0x20 || rootDir[c + e] == 0x00 || rootDir[c + e] == 0x05) &&
rootDir[c + e] != 0xFF &&

View File

@@ -38,17 +38,17 @@ public static class Base32
var builder = new StringBuilder(bytes.Length * _inByteSize / _outByteSize);
// Position in the input buffer
var bytesPosition = 0;
int bytesPosition = 0;
// Offset inside a single byte that <bytesPosition> points to (from left to right)
// 0 - highest bit, 7 - lowest bit
var bytesSubPosition = 0;
int bytesSubPosition = 0;
// Byte to look up in the dictionary
byte outputBase32Byte = 0;
// The number of bits filled in the current output byte
var outputBase32BytePosition = 0;
int outputBase32BytePosition = 0;
// Iterate through input buffer until we reach past the end of it
while(bytesPosition < bytes.Length)
@@ -119,22 +119,22 @@ public static class Base32
string base32StringUpperCase = base32String.ToUpperInvariant();
// Prepare output byte array
var outputBytes = new byte[base32StringUpperCase.Length * _outByteSize / _inByteSize];
byte[] outputBytes = new byte[base32StringUpperCase.Length * _outByteSize / _inByteSize];
// Check the size
if(outputBytes.Length == 0) throw new ArgumentException(Localization.Base32_Not_enought_data);
// Position in the string
var base32Position = 0;
int base32Position = 0;
// Offset inside the character in the string
var base32SubPosition = 0;
int base32SubPosition = 0;
// Position within outputBytes array
var outputBytePosition = 0;
int outputBytePosition = 0;
// The number of bits filled in the current output byte
var outputByteSubPosition = 0;
int outputByteSubPosition = 0;
// Normally we would iterate on the input array but in this case we actually iterate on the output array
// We do it because output array doesn't have overflow bits, while input does and it will cause output array overflow if we don''t stop in time

View File

@@ -56,7 +56,7 @@ static class ArmSimd
{
uint c = crc;
var bufPos = 0;
int bufPos = 0;
while(len >= 64)
{
@@ -95,7 +95,7 @@ static class ArmSimd
{
uint c = crc;
var bufPos = 0;
int bufPos = 0;
while(len >= 32)
{

View File

@@ -110,9 +110,9 @@ static class Clmul
Vector128<uint> xmmCRC1 = Vector128<uint>.Zero;
Vector128<uint> xmmCRC2 = Vector128<uint>.Zero;
Vector128<uint> xmmCRC3 = Vector128<uint>.Zero;
var bufPos = 0;
int bufPos = 0;
var first = true;
bool first = true;
/* fold 512 to 32 step variable declarations for ISO-C90 compat. */
var xmmMask = Vector128.Create(0xFFFFFFFF, 0xFFFFFFFF, 0x00000000, 0x00000000);

View File

@@ -379,15 +379,15 @@ public sealed partial class Crc32Context : IChecksum
static uint[][] GenerateTable(uint polynomial)
{
var table = new uint[8][];
uint[][] table = new uint[8][];
for(var i = 0; i < 8; i++) table[i] = new uint[256];
for(int i = 0; i < 8; i++) table[i] = new uint[256];
for(var i = 0; i < 256; i++)
for(int i = 0; i < 256; i++)
{
var entry = (uint)i;
uint entry = (uint)i;
for(var j = 0; j < 8; j++)
for(int j = 0; j < 8; j++)
{
if((entry & 1) == 1)
entry = entry >> 1 ^ polynomial;
@@ -398,9 +398,9 @@ public sealed partial class Crc32Context : IChecksum
table[0][i] = entry;
}
for(var slice = 1; slice < 8; slice++)
for(int slice = 1; slice < 8; slice++)
{
for(var i = 0; i < 256; i++)
for(int i = 0; i < 256; i++)
table[slice][i] = table[slice - 1][i] >> 8 ^ table[0][table[slice - 1][i] & 0xFF];
}
@@ -417,7 +417,7 @@ public sealed partial class Crc32Context : IChecksum
return;
}
var currentPos = 0;
int currentPos = 0;
if(useIso)
{
@@ -467,7 +467,7 @@ public sealed partial class Crc32Context : IChecksum
{
uint one = BitConverter.ToUInt32(data, currentPos) ^ crc;
currentPos += 4;
var two = BitConverter.ToUInt32(data, currentPos);
uint two = BitConverter.ToUInt32(data, currentPos);
currentPos += 4;
crc = table[0][two >> 24 & 0xFF] ^
@@ -528,8 +528,8 @@ public sealed partial class Crc32Context : IChecksum
uint[][] localTable = GenerateTable(polynomial);
var buffer = new byte[65536];
int read = fileStream.EnsureRead(buffer, 0, 65536);
byte[] buffer = new byte[65536];
int read = fileStream.EnsureRead(buffer, 0, 65536);
while(read > 0)
{
@@ -661,7 +661,7 @@ public sealed partial class Crc32Context : IChecksum
crc32_free(_nativeContext);
}
for(var i = 0; i < BigEndianBitConverter.GetBytes(crc).Length; i++)
for(int i = 0; i < BigEndianBitConverter.GetBytes(crc).Length; i++)
crc32Output.Append(BigEndianBitConverter.GetBytes(crc)[i].ToString("x2"));
return crc32Output.ToString();

View File

@@ -72,7 +72,7 @@ static class Clmul
internal static ulong Step(ulong crc, byte[] data, uint length)
{
var bufPos = 16;
int bufPos = 16;
const ulong k1 = 0xe05dd497ca393ae4;
const ulong k2 = 0xdabe95afc7875f40;
const ulong mu = 0x9c3e466c172963d5;

View File

@@ -324,15 +324,15 @@ public sealed partial class Crc64Context : IChecksum
static ulong[][] GenerateTable(ulong polynomial)
{
var table = new ulong[8][];
ulong[][] table = new ulong[8][];
for(var i = 0; i < 8; i++) table[i] = new ulong[256];
for(int i = 0; i < 8; i++) table[i] = new ulong[256];
for(var i = 0; i < 256; i++)
for(int i = 0; i < 256; i++)
{
var entry = (ulong)i;
ulong entry = (ulong)i;
for(var j = 0; j < 8; j++)
for(int j = 0; j < 8; j++)
{
if((entry & 1) == 1)
entry = entry >> 1 ^ polynomial;
@@ -343,9 +343,9 @@ public sealed partial class Crc64Context : IChecksum
table[0][i] = entry;
}
for(var slice = 1; slice < 4; slice++)
for(int slice = 1; slice < 4; slice++)
{
for(var i = 0; i < 256; i++)
for(int i = 0; i < 256; i++)
table[slice][i] = table[slice - 1][i] >> 8 ^ table[0][table[slice - 1][i] & 0xFF];
}
@@ -362,7 +362,7 @@ public sealed partial class Crc64Context : IChecksum
return;
}
var dataOff = 0;
int dataOff = 0;
if(useEcma && Pclmulqdq.IsSupported && Sse41.IsSupported && Ssse3.IsSupported && Sse2.IsSupported)
{
@@ -393,7 +393,7 @@ public sealed partial class Crc64Context : IChecksum
while(dataOff < limit)
{
var tmp = (uint)(crc ^ BitConverter.ToUInt32(data, dataOff));
uint tmp = (uint)(crc ^ BitConverter.ToUInt32(data, dataOff));
dataOff += 4;
crc = table[3][tmp & 0xFF] ^
@@ -449,8 +449,8 @@ public sealed partial class Crc64Context : IChecksum
ulong[][] localTable = GenerateTable(polynomial);
var buffer = new byte[65536];
int read = fileStream.EnsureRead(buffer, 0, 65536);
byte[] buffer = new byte[65536];
int read = fileStream.EnsureRead(buffer, 0, 65536);
while(read > 0)
{
@@ -582,7 +582,7 @@ public sealed partial class Crc64Context : IChecksum
crc64_free(_nativeContext);
}
for(var i = 0; i < BigEndianBitConverter.GetBytes(crc).Length; i++)
for(int i = 0; i < BigEndianBitConverter.GetBytes(crc).Length; i++)
crc64Output.Append(BigEndianBitConverter.GetBytes(crc)[i].ToString("x2"));
return crc64Output.ToString();

View File

@@ -75,7 +75,7 @@ public sealed class SpamSumContext : IChecksum
Bh = new BlockhashContext[NUM_BLOCKHASHES]
};
for(var i = 0; i < NUM_BLOCKHASHES; i++) _self.Bh[i].Digest = new byte[SPAMSUM_LENGTH];
for(int i = 0; i < NUM_BLOCKHASHES; i++) _self.Bh[i].Digest = new byte[SPAMSUM_LENGTH];
_self.Bhstart = 0;
_self.Bhend = 1;
@@ -240,7 +240,7 @@ public sealed class SpamSumContext : IChecksum
var sb = new StringBuilder();
uint bi = _self.Bhstart;
uint h = roll_sum();
var remain = (int)(FUZZY_MAX_RESULT - 1); /* Exclude terminating '\0'. */
int remain = (int)(FUZZY_MAX_RESULT - 1); /* Exclude terminating '\0'. */
result = new byte[FUZZY_MAX_RESULT];
/* Verify that our elimination was not overeager. */
@@ -423,7 +423,7 @@ public sealed class SpamSumContext : IChecksum
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static string CToString(byte[] cString)
{
var count = 0;
int count = 0;
// ReSharper disable once LoopCanBeConvertedToQuery
// LINQ is six times slower
@@ -506,7 +506,7 @@ public sealed class SpamSumContext : IChecksum
{
_self.TotalSize += len;
for(var i = 0; i < len; i++) fuzzy_engine_step(data[i]);
for(int i = 0; i < len; i++) fuzzy_engine_step(data[i]);
}
/// <inheritdoc />

View File

@@ -58,7 +58,7 @@ public static class Extensions
/// </returns>
public static int EnsureRead(this Stream s, byte[] buffer, int offset, int count)
{
var pos = 0;
int pos = 0;
int read;
do

View File

@@ -509,7 +509,7 @@ public sealed class Fuse : FileSystem
{
xattr = new byte[hash.Length / 2];
for(var i = 0; i < xattr.Length; i++)
for(int i = 0; i < xattr.Length; i++)
{
if(hash[i * 2] >= 0x30 && hash[i * 2] <= 0x39)
xattr[i] = (byte)((hash[i * 2] - 0x30) * 0x10);
@@ -823,8 +823,8 @@ public sealed class Fuse : FileSystem
public void Umount()
{
var rnd = new Random();
var token = new byte[64];
var rnd = new Random();
byte[] token = new byte[64];
rnd.NextBytes(token);
_umountToken = Base32.ToBase32String(token);
setxattr(Path.Combine(MountPoint, ".fuse_umount"), _umountToken, IntPtr.Zero, 0, 0);

View File

@@ -7,10 +7,12 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using SharpCompress.Compressors;
using SharpCompress.Compressors.LZMA;
using ZstdSharp;
namespace RomRepoMgr.Core.Filesystem;
@@ -18,7 +20,7 @@ namespace RomRepoMgr.Core.Filesystem;
// TODO: Invalidate caches
// TODO: Mount options
// TODO: Do not show machines or romsets with no ROMs in repo
public class Vfs : IDisposable
public class Vfs(ILoggerFactory loggerFactory) : IDisposable
{
readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, CachedDisk>> _machineDisksCache = [];
readonly ConcurrentDictionary<ulong, ConcurrentDictionary<string, CachedFile>> _machineFilesCache = [];
@@ -98,7 +100,7 @@ public class Vfs : IDisposable
internal void GetInfo(out ulong files, out ulong totalSize)
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
totalSize = (ulong)(ctx.Files.Where(f => f.IsInRepo).Sum(f => (double)f.Size) +
ctx.Disks.Where(f => f.IsInRepo).Sum(f => (double)f.Size) +
@@ -114,7 +116,7 @@ public class Vfs : IDisposable
void FillRootDirectoryCache()
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
var rootCache = new ConcurrentDictionary<string, long>();
@@ -180,7 +182,7 @@ public class Vfs : IDisposable
{
if(_romSetsCache.TryGetValue(id, out RomSet romSet)) return romSet;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
romSet = ctx.RomSets.Find(id);
@@ -199,7 +201,7 @@ public class Vfs : IDisposable
cachedMachines = [];
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
foreach(Machine mach in ctx.Machines.Where(m => m.RomSet.Id == id))
{
@@ -231,7 +233,7 @@ public class Vfs : IDisposable
if(cachedMachineFiles != null) return cachedMachineFiles;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
cachedMachineFiles = [];
@@ -267,7 +269,7 @@ public class Vfs : IDisposable
if(cachedMachineDisks != null) return cachedMachineDisks;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
cachedMachineDisks = [];
@@ -301,7 +303,7 @@ public class Vfs : IDisposable
if(cachedMachineMedias != null) return cachedMachineMedias;
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
cachedMachineMedias = [];
@@ -364,9 +366,9 @@ public class Vfs : IDisposable
internal long Open(string sha384, long fileSize)
{
var sha384Bytes = new byte[48];
byte[] sha384Bytes = new byte[48];
for(var i = 0; i < 48; i++)
for(int i = 0; i < 48; i++)
{
if(sha384[i * 2] >= 0x30 && sha384[i * 2] <= 0x39)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x30) * 0x10);
@@ -385,19 +387,66 @@ public class Vfs : IDisposable
string sha384B32 = Base32.ToBase32String(sha384Bytes);
string repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32 + ".lz");
if(!File.Exists(repoPath)) return -1;
string repoPath;
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32 + ".lz");
long handle;
if(!File.Exists(repoPath))
{
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32 + ".zst");
if(!File.Exists(repoPath))
{
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32);
if(!File.Exists(repoPath)) return -1;
_lastHandle++;
handle = _lastHandle;
_streamsCache[handle] = Stream.Synchronized(new FileStream(repoPath, FileMode.Open, FileAccess.Read));
return handle;
}
_lastHandle++;
handle = _lastHandle;
_streamsCache[handle] =
Stream.Synchronized(new ForcedSeekStream<DecompressionStream>(fileSize,
new FileStream(repoPath,
FileMode.Open,
FileAccess.Read)));
return handle;
}
_lastHandle++;
long handle = _lastHandle;
handle = _lastHandle;
_streamsCache[handle] =
Stream.Synchronized(new ForcedSeekStream<LZipStream>(fileSize,
@@ -456,9 +505,9 @@ public class Vfs : IDisposable
if(sha1 != null)
{
var sha1Bytes = new byte[20];
byte[] sha1Bytes = new byte[20];
for(var i = 0; i < 20; i++)
for(int i = 0; i < 20; i++)
{
if(sha1[i * 2] >= 0x30 && sha1[i * 2] <= 0x39)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x30) * 0x10);
@@ -489,9 +538,9 @@ public class Vfs : IDisposable
if(md5 != null)
{
var md5Bytes = new byte[16];
byte[] md5Bytes = new byte[16];
for(var i = 0; i < 16; i++)
for(int i = 0; i < 16; i++)
{
if(md5[i * 2] >= 0x30 && md5[i * 2] <= 0x39)
md5Bytes[i] = (byte)((md5[i * 2] - 0x30) * 0x10);
@@ -544,9 +593,9 @@ public class Vfs : IDisposable
if(sha256 != null)
{
var sha256Bytes = new byte[32];
byte[] sha256Bytes = new byte[32];
for(var i = 0; i < 32; i++)
for(int i = 0; i < 32; i++)
{
if(sha256[i * 2] >= 0x30 && sha256[i * 2] <= 0x39)
sha256Bytes[i] = (byte)((sha256[i * 2] - 0x30) * 0x10);
@@ -578,9 +627,9 @@ public class Vfs : IDisposable
if(sha1 != null)
{
var sha1Bytes = new byte[20];
byte[] sha1Bytes = new byte[20];
for(var i = 0; i < 20; i++)
for(int i = 0; i < 20; i++)
{
if(sha1[i * 2] >= 0x30 && sha1[i * 2] <= 0x39)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x30) * 0x10);
@@ -611,9 +660,9 @@ public class Vfs : IDisposable
if(md5 != null)
{
var md5Bytes = new byte[16];
byte[] md5Bytes = new byte[16];
for(var i = 0; i < 16; i++)
for(int i = 0; i < 16; i++)
{
if(md5[i * 2] >= 0x30 && md5[i * 2] <= 0x39)
md5Bytes[i] = (byte)((md5[i * 2] - 0x30) * 0x10);

View File

@@ -369,7 +369,7 @@ public class Winfsp(Vfs vfs) : FileSystemBase
if(fileNode is not FileNode { Handle: > 0 } node) return STATUS_INVALID_HANDLE;
var buf = new byte[length];
byte[] buf = new byte[length];
int ret = vfs.Read(node.Handle, buf, (long)offset);
@@ -575,10 +575,10 @@ public class Winfsp(Vfs vfs) : FileSystemBase
if(securityDescriptor == null) return STATUS_SUCCESS;
var rootSddl = "O:BAG:BAD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;WD)";
string rootSddl = "O:BAG:BAD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;WD)";
var rootSecurityDescriptor = new RawSecurityDescriptor(rootSddl);
var fileSecurity = new byte[rootSecurityDescriptor.BinaryLength];
var rootSecurityDescriptor = new RawSecurityDescriptor(rootSddl);
byte[] fileSecurity = new byte[rootSecurityDescriptor.BinaryLength];
rootSecurityDescriptor.GetBinaryForm(fileSecurity, 0);
securityDescriptor = fileSecurity;

View File

@@ -89,7 +89,7 @@ internal sealed class ForcedSeekStream<T> : Stream where T : Stream
do
{
var buffer = new byte[BUFFER_LEN];
byte[] buffer = new byte[BUFFER_LEN];
read = _baseStream.Read(buffer, 0, BUFFER_LEN);
_backStream.Write(buffer, 0, read);
} while(read == BUFFER_LEN);
@@ -111,19 +111,19 @@ internal sealed class ForcedSeekStream<T> : Stream where T : Stream
_backStream.Position = _backStream.Length;
long toPosition = position - _backStream.Position;
var fullBufferReads = (int)(toPosition / BUFFER_LEN);
var restToRead = (int)(toPosition % BUFFER_LEN);
int fullBufferReads = (int)(toPosition / BUFFER_LEN);
int restToRead = (int)(toPosition % BUFFER_LEN);
byte[] buffer;
for(var i = 0; i < fullBufferReads; i++)
for(int i = 0; i < fullBufferReads; i++)
{
buffer = new byte[BUFFER_LEN];
_baseStream.EnsureRead(buffer, 0, BUFFER_LEN);
_baseStream.ReadExactly(buffer, 0, BUFFER_LEN);
_backStream.Write(buffer, 0, BUFFER_LEN);
}
buffer = new byte[restToRead];
_baseStream.EnsureRead(buffer, 0, restToRead);
_baseStream.ReadExactly(buffer, 0, restToRead);
_backStream.Write(buffer, 0, restToRead);
}

View File

@@ -392,5 +392,11 @@ namespace RomRepoMgr.Core.Resources {
return ResourceManager.GetString("DatImportSuccess", resourceCulture);
}
}
internal static string ImportingFile {
get {
return ResourceManager.GetString("ImportingFile", resourceCulture);
}
}
}
}

View File

@@ -186,4 +186,7 @@
<data name="AddingMedias" xml:space="preserve">
<value>Añadiendo medios...</value>
</data>
<data name="ImportingFile" xml:space="preserve">
<value>Importando fichero...</value>
</data>
</root>

View File

@@ -196,4 +196,7 @@
<data name="DatImportSuccess" xml:space="preserve">
<value>Imported {0} machines with {1} ROMs.</value>
</data>
<data name="ImportingFile" xml:space="preserve">
<value>Importing file...</value>
</data>
</root>

View File

@@ -1,7 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0-beta.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0-beta.1</InformationalVersion>
<Copyright>© $([System.DateTime]::Now.Year) Natalia Portillo</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aaru.Checksums.Native" />
<PackageReference Include="Aaru.Checksums.Native"/>
<PackageReference Include="DotNetZip"/>
<PackageReference Include="EFCore.BulkExtensions"/>
<PackageReference Include="Mono.Fuse.NETStandard"/>
@@ -19,6 +27,7 @@
<PackageReference Include="Roslynator.Formatting.Analyzers"/>
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer"/>
<PackageReference Include="Text.Analyzers"/>
<PackageReference Include="ZstdSharp.Port"/>
</ItemGroup>
<ItemGroup>

View File

@@ -28,8 +28,11 @@ using System.Diagnostics;
using System.IO;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Resources;
using RomRepoMgr.Settings;
using SharpCompress.Compressors;
using SharpCompress.Compressors.LZMA;
using ZstdSharp;
using ZstdSharp.Unsafe;
using ErrorEventArgs = RomRepoMgr.Core.EventArgs.ErrorEventArgs;
namespace RomRepoMgr.Core.Workers;
@@ -45,11 +48,32 @@ public sealed class Compression
public void CompressFile(string source, string destination)
{
var inFs = new FileStream(source, FileMode.Open, FileAccess.Read);
var outFs = new FileStream(destination, FileMode.CreateNew, FileAccess.Write);
Stream zStream = new LZipStream(outFs, CompressionMode.Compress);
var inFs = new FileStream(source, FileMode.Open, FileAccess.Read);
var outFs = new FileStream(destination, FileMode.CreateNew, FileAccess.Write);
var buffer = new byte[BUFFER_SIZE];
Stream zStream;
switch(Settings.Settings.Current.Compression)
{
case CompressionType.Zstd:
{
var zstdStream = new CompressionStream(outFs, 15);
zstdStream.SetParameter(ZSTD_cParameter.ZSTD_c_nbWorkers, Environment.ProcessorCount);
zStream = zstdStream;
break;
}
case CompressionType.None:
zStream = outFs;
break;
default:
zStream = new LZipStream(outFs, CompressionMode.Compress);
break;
}
byte[] buffer = new byte[BUFFER_SIZE];
SetProgressBounds?.Invoke(this,
new ProgressBoundsEventArgs
@@ -66,7 +90,7 @@ public sealed class Compression
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
zStream.Write(buffer, 0, buffer.Length);
}
@@ -79,7 +103,7 @@ public sealed class Compression
Maximum = inFs.Length
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
zStream.Write(buffer, 0, buffer.Length);
inFs.Close();
@@ -89,9 +113,19 @@ public sealed class Compression
public void DecompressFile(string source, string destination)
{
var inFs = new FileStream(source, FileMode.Open, FileAccess.Read);
var outFs = new FileStream(destination, FileMode.Create, FileAccess.Write);
Stream zStream = new LZipStream(inFs, CompressionMode.Decompress);
var inFs = new FileStream(source, FileMode.Open, FileAccess.Read);
var outFs = new FileStream(destination, FileMode.Create, FileAccess.Write);
Stream zStream;
if(Path.GetExtension(source) == ".zst")
zStream = new DecompressionStream(inFs);
else if(Path.GetExtension(source) == ".lz")
zStream = new LZipStream(inFs, CompressionMode.Decompress);
else if(string.IsNullOrWhiteSpace(Path.GetExtension(source)))
zStream = inFs;
else
throw new ArgumentException($"Invalid compression extension {Path.GetExtension(source)}");
zStream.CopyTo(outFs);

View File

@@ -33,6 +33,7 @@ using System.Linq;
using System.Threading;
using EFCore.BulkExtensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using RomRepoMgr.Core.Checksums;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Models;
@@ -54,16 +55,18 @@ namespace RomRepoMgr.Core.Workers;
public sealed class DatImporter
{
static readonly Lock DbLock = new();
readonly string _category;
readonly string _datFilesPath;
readonly string _datPath;
bool _aborted;
static readonly Lock DbLock = new();
readonly string _category;
readonly string _datFilesPath;
readonly string _datPath;
readonly ILoggerFactory _loggerFactory;
bool _aborted;
public DatImporter(string datPath, string category)
public DatImporter(string datPath, string category, ILoggerFactory loggerFactory)
{
_datPath = datPath;
_datFilesPath = Path.Combine(Settings.Settings.Current.RepositoryPath, "datfiles");
_datPath = datPath;
_datFilesPath = Path.Combine(Settings.Settings.Current.RepositoryPath, "datfiles");
_loggerFactory = loggerFactory;
if(!string.IsNullOrWhiteSpace(category)) _category = category;
}
@@ -72,7 +75,7 @@ public sealed class DatImporter
{
try
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, _loggerFactory);
SetIndeterminateProgress?.Invoke(this, System.EventArgs.Empty);
@@ -172,7 +175,7 @@ public sealed class DatImporter
Maximum = machineNames.Count
});
var position = 0;
int position = 0;
var machines = new Dictionary<string, Machine>();
foreach(string name in machineNames)
@@ -220,29 +223,29 @@ public sealed class DatImporter
var disks = new List<Disk>();
var medias = new List<Media>();
var tmpRomCrc32Table = Guid.NewGuid().ToString();
var tmpRomMd5Table = Guid.NewGuid().ToString();
var tmpRomSha1Table = Guid.NewGuid().ToString();
var tmpRomSha256Table = Guid.NewGuid().ToString();
var tmpRomSha384Table = Guid.NewGuid().ToString();
var tmpRomSha512Table = Guid.NewGuid().ToString();
var tmpDiskMd5Table = Guid.NewGuid().ToString();
var tmpDiskSha1Table = Guid.NewGuid().ToString();
var tmpMediaMd5Table = Guid.NewGuid().ToString();
var tmpMediaSha1Table = Guid.NewGuid().ToString();
var tmpMediaSha256Table = Guid.NewGuid().ToString();
string tmpRomCrc32Table = Guid.NewGuid().ToString();
string tmpRomMd5Table = Guid.NewGuid().ToString();
string tmpRomSha1Table = Guid.NewGuid().ToString();
string tmpRomSha256Table = Guid.NewGuid().ToString();
string tmpRomSha384Table = Guid.NewGuid().ToString();
string tmpRomSha512Table = Guid.NewGuid().ToString();
string tmpDiskMd5Table = Guid.NewGuid().ToString();
string tmpDiskSha1Table = Guid.NewGuid().ToString();
string tmpMediaMd5Table = Guid.NewGuid().ToString();
string tmpMediaSha1Table = Guid.NewGuid().ToString();
string tmpMediaSha256Table = Guid.NewGuid().ToString();
var romsHaveCrc = false;
var romsHaveMd5 = false;
var romsHaveSha1 = false;
var romsHaveSha256 = false;
var romsHaveSha384 = false;
var romsHaveSha512 = false;
var disksHaveMd5 = false;
var disksHaveSha1 = false;
var mediasHaveMd5 = false;
var mediasHaveSha1 = false;
var mediasHaveSha256 = false;
bool romsHaveCrc = false;
bool romsHaveMd5 = false;
bool romsHaveSha1 = false;
bool romsHaveSha256 = false;
bool romsHaveSha384 = false;
bool romsHaveSha512 = false;
bool disksHaveMd5 = false;
bool disksHaveSha1 = false;
bool mediasHaveMd5 = false;
bool mediasHaveSha1 = false;
bool mediasHaveSha256 = false;
DbConnection dbConnection = ctx.Database.GetDbConnection();
dbConnection.Open();
@@ -623,7 +626,7 @@ public sealed class DatImporter
foreach(Rom rom in roms)
{
var hashCollision = false;
bool hashCollision = false;
SetProgress?.Invoke(this,
new ProgressEventArgs
@@ -645,7 +648,7 @@ public sealed class DatImporter
return;
}
var uSize = (ulong)rom.GetInt64FieldValue(SabreTools.Models.Metadata.Rom.SizeKey);
ulong uSize = (ulong)rom.GetInt64FieldValue(SabreTools.Models.Metadata.Rom.SizeKey);
DbFile file = null;

View File

@@ -6,16 +6,18 @@ using System.Text;
using System.Threading.Tasks;
using Ionic.Zip;
using Ionic.Zlib;
using Microsoft.Extensions.Logging;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Resources;
using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using SharpCompress.Compressors.LZMA;
using ZstdSharp;
using CompressionMode = SharpCompress.Compressors.CompressionMode;
namespace RomRepoMgr.Core.Workers;
public class FileExporter(long romSetId, string outPath)
public class FileExporter(long romSetId, string outPath, ILoggerFactory loggerFactory)
{
const long BUFFER_SIZE = 131072;
long _filePosition;
@@ -43,7 +45,7 @@ public class FileExporter(long romSetId, string outPath)
Message = Localization.RetrievingRomSetFromDatabase
});
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
RomSet romSet = ctx.RomSets.Find(romSetId);
@@ -108,7 +110,7 @@ public class FileExporter(long romSetId, string outPath)
Message = machine.Name
});
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath, loggerFactory);
string machineName = machine.Name;
@@ -154,10 +156,10 @@ public class FileExporter(long romSetId, string outPath)
if(media.Sha256 != null)
{
var sha256Bytes = new byte[32];
byte[] sha256Bytes = new byte[32];
string sha256 = media.Sha256;
for(var i = 0; i < 32; i++)
for(int i = 0; i < 32; i++)
{
if(sha256[i * 2] >= 0x30 && sha256[i * 2] <= 0x39)
sha256Bytes[i] = (byte)((sha256[i * 2] - 0x30) * 0x10);
@@ -189,10 +191,10 @@ public class FileExporter(long romSetId, string outPath)
if(media.Sha1 != null)
{
var sha1Bytes = new byte[20];
byte[] sha1Bytes = new byte[20];
string sha1 = media.Sha1;
for(var i = 0; i < 20; i++)
for(int i = 0; i < 20; i++)
{
if(sha1[i * 2] >= 0x30 && sha1[i * 2] <= 0x39)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x30) * 0x10);
@@ -224,10 +226,10 @@ public class FileExporter(long romSetId, string outPath)
if(media.Md5 != null)
{
var md5Bytes = new byte[16];
byte[] md5Bytes = new byte[16];
string md5 = media.Md5;
for(var i = 0; i < 16; i++)
for(int i = 0; i < 16; i++)
{
if(md5[i * 2] >= 0x30 && md5[i * 2] <= 0x39)
md5Bytes[i] = (byte)((md5[i * 2] - 0x30) * 0x10);
@@ -285,7 +287,7 @@ public class FileExporter(long romSetId, string outPath)
Maximum = inFs.Length
});
var buffer = new byte[BUFFER_SIZE];
byte[] buffer = new byte[BUFFER_SIZE];
while(inFs.Position + BUFFER_SIZE <= inFs.Length)
{
@@ -295,7 +297,7 @@ public class FileExporter(long romSetId, string outPath)
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
}
@@ -307,7 +309,7 @@ public class FileExporter(long romSetId, string outPath)
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
inFs.Close();
@@ -358,10 +360,10 @@ public class FileExporter(long romSetId, string outPath)
if(disk.Sha1 != null)
{
var sha1Bytes = new byte[20];
byte[] sha1Bytes = new byte[20];
string sha1 = disk.Sha1;
for(var i = 0; i < 20; i++)
for(int i = 0; i < 20; i++)
{
if(sha1[i * 2] >= 0x30 && sha1[i * 2] <= 0x39)
sha1Bytes[i] = (byte)((sha1[i * 2] - 0x30) * 0x10);
@@ -393,10 +395,10 @@ public class FileExporter(long romSetId, string outPath)
if(disk.Md5 != null)
{
var md5Bytes = new byte[16];
byte[] md5Bytes = new byte[16];
string md5 = disk.Md5;
for(var i = 0; i < 16; i++)
for(int i = 0; i < 16; i++)
{
if(md5[i * 2] >= 0x30 && md5[i * 2] <= 0x39)
md5Bytes[i] = (byte)((md5[i * 2] - 0x30) * 0x10);
@@ -452,7 +454,7 @@ public class FileExporter(long romSetId, string outPath)
Maximum = inFs.Length
});
var buffer = new byte[BUFFER_SIZE];
byte[] buffer = new byte[BUFFER_SIZE];
while(inFs.Position + BUFFER_SIZE <= inFs.Length)
{
@@ -462,7 +464,7 @@ public class FileExporter(long romSetId, string outPath)
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
}
@@ -474,7 +476,7 @@ public class FileExporter(long romSetId, string outPath)
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
inFs.Close();
@@ -557,10 +559,10 @@ public class FileExporter(long romSetId, string outPath)
// Special case for empty file, as it seems to crash when SharpCompress tries to unLZMA it.
if(file.Size == 0) return new MemoryStream();
var sha384Bytes = new byte[48];
byte[] sha384Bytes = new byte[48];
string sha384 = file.Sha384;
for(var i = 0; i < 48; i++)
for(int i = 0; i < 48; i++)
{
if(sha384[i * 2] >= 0x30 && sha384[i * 2] <= 0x39)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x30) * 0x10);
@@ -588,10 +590,43 @@ public class FileExporter(long romSetId, string outPath)
sha384B32[4].ToString(),
sha384B32 + ".lz");
if(!File.Exists(repoPath))
throw new ArgumentException(string.Format(Localization.CannotFindHashInRepository, file.Sha256));
FileStream inFs;
var inFs = new FileStream(repoPath, FileMode.Open, FileAccess.Read);
// Try ZSTD
if(!File.Exists(repoPath))
{
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32 + ".zst");
if(!File.Exists(repoPath))
{
repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString(),
sha384B32);
return !File.Exists(repoPath)
? throw new ArgumentException(string.Format(Localization.CannotFindHashInRepository,
file.Sha256))
: new FileStream(repoPath, FileMode.Open, FileAccess.Read);
}
inFs = new FileStream(repoPath, FileMode.Open, FileAccess.Read);
return new StreamWithLength(new DecompressionStream(inFs), (long)file.Size);
}
inFs = new FileStream(repoPath, FileMode.Open, FileAccess.Read);
return new StreamWithLength(new LZipStream(inFs, CompressionMode.Decompress), (long)file.Size);
}
@@ -611,11 +646,9 @@ public class FileExporter(long romSetId, string outPath)
Value = _filePosition
});
if(!_filesByMachine.TryGetValue(e.CurrentEntry.FileName, out FileByMachine fileByMachine))
{
if(!_filesByMachine.TryGetValue(e.CurrentEntry.FileName.Replace('/', '\\'), out fileByMachine))
throw new ArgumentException(Localization.CannotFindZipEntryInDictionary);
}
if(!_filesByMachine.TryGetValue(e.CurrentEntry.FileName, out FileByMachine fileByMachine) &&
!_filesByMachine.TryGetValue(e.CurrentEntry.FileName.Replace('/', '\\'), out fileByMachine))
throw new ArgumentException(Localization.CannotFindZipEntryInDictionary);
DbFile currentFile = fileByMachine.File;

View File

@@ -17,8 +17,13 @@ using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using SabreTools.FileTypes.Aaru;
using SabreTools.FileTypes.CHD;
using SharpCompress.Common;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Readers;
using ZstdSharp;
using ZstdSharp.Unsafe;
using CompressionMode = SharpCompress.Compressors.CompressionMode;
using CompressionType = RomRepoMgr.Settings.CompressionType;
namespace RomRepoMgr.Core.Workers;
@@ -81,6 +86,77 @@ public sealed class FileImporter
Finished?.Invoke(this, System.EventArgs.Empty);
}
public void SeparateFilesAndArchivesManaged()
{
SetProgressBounds?.Invoke(this,
new ProgressBoundsEventArgs
{
Minimum = 0,
Maximum = Files.Count
});
ConcurrentBag<string> files = [];
ConcurrentBag<string> archives = [];
Parallel.ForEach(Files,
file =>
{
SetProgress?.Invoke(this,
new ProgressEventArgs
{
Value = _position
});
SetMessage?.Invoke(this,
new MessageEventArgs
{
Message = "Checking archives. Found " +
archives.Count +
" archives and " +
files.Count +
" files."
});
SetMessage2?.Invoke(this,
new MessageEventArgs
{
Message =
$"Checking if file {Path.GetFileName(file)} is an archive..."
});
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
try
{
using var fs = new FileStream(file, FileMode.Open, FileAccess.Read);
using IReader reader = ReaderFactory.Open(fs);
archives.Add(file);
}
catch(InvalidOperationException)
{
files.Add(file);
}
Interlocked.Increment(ref _position);
});
Files = files.Order().ToList();
Archives = archives.Order().ToList();
SetMessage?.Invoke(this,
new MessageEventArgs
{
Message = "Finished checking archives. Found " +
Archives.Count +
" archives and " +
Files.Count +
" files."
});
Finished?.Invoke(this, System.EventArgs.Empty);
}
public void SeparateFilesAndArchives()
{
SetProgressBounds?.Invoke(this,
@@ -275,11 +351,8 @@ public sealed class FileImporter
}
}
public bool ExtractArchive(string archive)
public bool ExtractArchiveManaged(string archive)
{
string archiveFormat = null;
long archiveFiles = 0;
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
SetMessage2?.Invoke(this,
@@ -288,7 +361,60 @@ public sealed class FileImporter
Message = Localization.CheckingIfFIleIsAnArchive
});
archiveFormat = GetArchiveFormat(archive, out archiveFiles);
try
{
using var fs = new FileStream(archive, FileMode.Open, FileAccess.Read);
using IReader reader = ReaderFactory.Open(fs);
if(!Directory.Exists(Settings.Settings.Current.TemporaryFolder))
Directory.CreateDirectory(Settings.Settings.Current.TemporaryFolder);
_archiveFolder = Path.Combine(Settings.Settings.Current.TemporaryFolder, Path.GetRandomFileName());
Directory.CreateDirectory(_archiveFolder);
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
SetMessage2?.Invoke(this,
new MessageEventArgs
{
Message = Localization.ExtractingArchive
});
reader.WriteAllToDirectory(_archiveFolder,
new ExtractionOptions
{
ExtractFullPath = true,
Overwrite = true
});
Files = Directory.GetFiles(_archiveFolder, "*", SearchOption.AllDirectories).Order().ToList();
SetMessage2?.Invoke(this,
new MessageEventArgs
{
Message = "Finished extracting files. Extracted " + Files.Count + " files."
});
return true;
}
catch(InvalidOperationException)
{
return false;
}
}
public bool ExtractArchive(string archive)
{
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
SetMessage2?.Invoke(this,
new MessageEventArgs
{
Message = Localization.CheckingIfFIleIsAnArchive
});
string archiveFormat = GetArchiveFormat(archive, out long archiveFiles);
// If a floppy contains only the archive, unar will recognize it, on its skipping of SFXs.
if(archiveFormat != null && FAT.Identify(archive)) archiveFormat = null;
@@ -414,6 +540,295 @@ public sealed class FileImporter
}
}
public bool IsCrcInDb(long crc32)
{
lock(DbLock)
{
return _ctx.Files.Any(f => f.Crc32 == crc32.ToString("x8"));
}
}
public void ImportAndHashRom(Stream stream, string filename, string tempPath, long size)
{
try
{
var outFs = new FileStream(tempPath, FileMode.Create, FileAccess.Write);
SetMessage2?.Invoke(this,
new MessageEventArgs
{
Message = Localization.ImportingFile
});
byte[] buffer;
var checksumWorker = new Checksum();
Stream zStream;
switch(Settings.Settings.Current.Compression)
{
case CompressionType.Zstd:
{
var zstdStream = new CompressionStream(outFs, 15);
zstdStream.SetParameter(ZSTD_cParameter.ZSTD_c_nbWorkers, Environment.ProcessorCount);
zStream = zstdStream;
break;
}
case CompressionType.None:
zStream = outFs;
break;
default:
zStream = new LZipStream(outFs, CompressionMode.Compress);
break;
}
if(size > BUFFER_SIZE)
{
SetProgressBounds2?.Invoke(this,
new ProgressBoundsEventArgs
{
Minimum = 0,
Maximum = size
});
long offset;
long remainder = size % BUFFER_SIZE;
for(offset = 0; offset < size - remainder; offset += (int)BUFFER_SIZE)
{
SetProgress2?.Invoke(this,
new ProgressEventArgs
{
Value = offset
});
buffer = new byte[BUFFER_SIZE];
stream.ReadExactly(buffer, 0, (int)BUFFER_SIZE);
checksumWorker.Update(buffer);
zStream.Write(buffer, 0, buffer.Length);
}
SetProgress2?.Invoke(this,
new ProgressEventArgs
{
Value = offset
});
buffer = new byte[remainder];
stream.ReadExactly(buffer, 0, (int)remainder);
}
else
{
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
buffer = new byte[size];
stream.ReadExactly(buffer, 0, (int)size);
}
checksumWorker.Update(buffer);
zStream.Write(buffer, 0, buffer.Length);
Dictionary<ChecksumType, string> checksums = checksumWorker.End();
ulong uSize = (ulong)size;
bool fileInDb = true;
bool knownFile = _pendingFiles.TryGetValue(checksums[ChecksumType.Sha512], out DbFile dbFile);
lock(DbLock)
{
dbFile ??= _ctx.Files.FirstOrDefault(f => (f.Sha512 == checksums[ChecksumType.Sha512] ||
f.Sha384 == checksums[ChecksumType.Sha384] ||
f.Sha256 == checksums[ChecksumType.Sha256] ||
f.Sha1 == checksums[ChecksumType.Sha1] ||
f.Md5 == checksums[ChecksumType.Md5] ||
f.Crc32 == checksums[ChecksumType.Crc32]) &&
f.Size == uSize);
}
if(dbFile == null)
{
if(onlyKnown)
{
ImportedRom?.Invoke(this,
new ImportedRomItemEventArgs
{
Item = new ImportRomItem
{
Filename = filename,
Status = Localization.UnknownFile
}
});
return;
}
dbFile = new DbFile
{
Crc32 = checksums[ChecksumType.Crc32],
Md5 = checksums[ChecksumType.Md5],
Sha1 = checksums[ChecksumType.Sha1],
Sha256 = checksums[ChecksumType.Sha256],
Sha384 = checksums[ChecksumType.Sha384],
Sha512 = checksums[ChecksumType.Sha512],
Size = uSize,
CreatedOn = DateTime.UtcNow,
UpdatedOn = DateTime.UtcNow,
OriginalFileName = Path.GetFileName(filename)
};
fileInDb = false;
}
if(!knownFile) _pendingFiles[checksums[ChecksumType.Sha512]] = dbFile;
byte[] sha384Bytes = new byte[48];
string sha384 = checksums[ChecksumType.Sha384];
for(int i = 0; i < 48; i++)
{
if(sha384[i * 2] >= 0x30 && sha384[i * 2] <= 0x39)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x30) * 0x10);
else if(sha384[i * 2] >= 0x41 && sha384[i * 2] <= 0x46)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x37) * 0x10);
else if(sha384[i * 2] >= 0x61 && sha384[i * 2] <= 0x66)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x57) * 0x10);
if(sha384[i * 2 + 1] >= 0x30 && sha384[i * 2 + 1] <= 0x39)
sha384Bytes[i] += (byte)(sha384[i * 2 + 1] - 0x30);
else if(sha384[i * 2 + 1] >= 0x41 && sha384[i * 2 + 1] <= 0x46)
sha384Bytes[i] += (byte)(sha384[i * 2 + 1] - 0x37);
else if(sha384[i * 2 + 1] >= 0x61 && sha384[i * 2 + 1] <= 0x66)
sha384Bytes[i] += (byte)(sha384[i * 2 + 1] - 0x57);
}
string sha384B32 = Base32.ToBase32String(sha384Bytes);
string repoPath = Path.Combine(Settings.Settings.Current.RepositoryPath,
"files",
sha384B32[0].ToString(),
sha384B32[1].ToString(),
sha384B32[2].ToString(),
sha384B32[3].ToString(),
sha384B32[4].ToString());
if(!Directory.Exists(repoPath)) Directory.CreateDirectory(repoPath);
repoPath = Settings.Settings.Current.Compression switch
{
CompressionType.Zstd => Path.Combine(repoPath, sha384B32 + ".zst"),
CompressionType.None => Path.Combine(repoPath, sha384B32),
_ => Path.Combine(repoPath, sha384B32 + ".lz")
};
if(dbFile.Crc32 == null)
{
dbFile.Crc32 = checksums[ChecksumType.Crc32];
dbFile.UpdatedOn = DateTime.UtcNow;
}
if(dbFile.Md5 == null)
{
dbFile.Md5 = checksums[ChecksumType.Md5];
dbFile.UpdatedOn = DateTime.UtcNow;
}
if(dbFile.Sha1 == null)
{
dbFile.Sha1 = checksums[ChecksumType.Sha1];
dbFile.UpdatedOn = DateTime.UtcNow;
}
if(dbFile.Sha256 == null)
{
dbFile.Sha256 = checksums[ChecksumType.Sha256];
dbFile.UpdatedOn = DateTime.UtcNow;
}
if(dbFile.Sha384 == null)
{
dbFile.Sha384 = checksums[ChecksumType.Sha384];
dbFile.UpdatedOn = DateTime.UtcNow;
}
if(dbFile.Sha512 == null)
{
dbFile.Sha512 = checksums[ChecksumType.Sha512];
dbFile.UpdatedOn = DateTime.UtcNow;
}
if(File.Exists(repoPath))
{
dbFile.IsInRepo = true;
dbFile.UpdatedOn = DateTime.UtcNow;
if(!fileInDb) _newFiles.Add(dbFile);
zStream.Close();
outFs.Dispose();
File.Delete(tempPath);
ImportedRom?.Invoke(this,
new ImportedRomItemEventArgs
{
Item = new ImportRomItem
{
Filename = filename,
Status = Localization.OK
}
});
return;
}
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
SetMessage2?.Invoke(this,
new MessageEventArgs
{
Message = Localization.Finishing
});
zStream.Close();
outFs.Dispose();
File.Move(tempPath, repoPath, true);
dbFile.IsInRepo = true;
dbFile.UpdatedOn = DateTime.UtcNow;
if(!fileInDb) _newFiles.Add(dbFile);
ImportedRom?.Invoke(this,
new ImportedRomItemEventArgs
{
Item = new ImportRomItem
{
Filename = filename,
Status = Localization.OK
}
});
}
catch(Exception)
{
ImportedRom?.Invoke(this,
new ImportedRomItemEventArgs
{
Item = new ImportRomItem
{
Filename = filename,
Status = Localization.UnhandledExceptionWhenImporting
}
});
#pragma warning disable ERP022
}
#pragma warning restore ERP022
}
bool ImportRom(string path)
{
try
@@ -451,7 +866,7 @@ public sealed class FileImporter
});
buffer = new byte[BUFFER_SIZE];
inFs.EnsureRead(buffer, 0, (int)BUFFER_SIZE);
inFs.ReadExactly(buffer, 0, (int)BUFFER_SIZE);
checksumWorker.Update(buffer);
}
@@ -462,13 +877,13 @@ public sealed class FileImporter
});
buffer = new byte[remainder];
inFs.EnsureRead(buffer, 0, (int)remainder);
inFs.ReadExactly(buffer, 0, (int)remainder);
}
else
{
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
buffer = new byte[inFs.Length];
inFs.EnsureRead(buffer, 0, (int)inFs.Length);
inFs.ReadExactly(buffer, 0, (int)inFs.Length);
}
checksumWorker.Update(buffer);
@@ -551,7 +966,12 @@ public sealed class FileImporter
if(!Directory.Exists(repoPath)) Directory.CreateDirectory(repoPath);
repoPath = Path.Combine(repoPath, sha384B32 + ".lz");
repoPath = Settings.Settings.Current.Compression switch
{
CompressionType.Zstd => Path.Combine(repoPath, sha384B32 + ".zst"),
CompressionType.None => Path.Combine(repoPath, sha384B32),
_ => Path.Combine(repoPath, sha384B32 + ".lz")
};
if(dbFile.Crc32 == null)
{
@@ -605,8 +1025,29 @@ public sealed class FileImporter
inFs.Position = 0;
var outFs = new FileStream(repoPath, FileMode.CreateNew, FileAccess.Write);
Stream zStream = new LZipStream(outFs, CompressionMode.Compress);
var outFs = new FileStream(repoPath, FileMode.CreateNew, FileAccess.Write);
Stream zStream;
switch(Settings.Settings.Current.Compression)
{
case CompressionType.Zstd:
{
var zstdStream = new CompressionStream(outFs, 15);
zstdStream.SetParameter(ZSTD_cParameter.ZSTD_c_nbWorkers, Environment.ProcessorCount);
zStream = zstdStream;
break;
}
case CompressionType.None:
zStream = outFs;
break;
default:
zStream = new LZipStream(outFs, CompressionMode.Compress);
break;
}
SetProgressBounds2?.Invoke(this,
new ProgressBoundsEventArgs
@@ -631,7 +1072,7 @@ public sealed class FileImporter
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
zStream.Write(buffer, 0, buffer.Length);
}
@@ -643,7 +1084,7 @@ public sealed class FileImporter
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
zStream.Write(buffer, 0, buffer.Length);
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
@@ -667,7 +1108,7 @@ public sealed class FileImporter
return true;
}
catch(Exception ex)
catch(Exception)
{
_lastMessage = Localization.UnhandledExceptionWhenImporting;
@@ -911,7 +1352,7 @@ public sealed class FileImporter
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
}
@@ -923,7 +1364,7 @@ public sealed class FileImporter
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);
@@ -1253,7 +1694,7 @@ public sealed class FileImporter
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
}
@@ -1265,7 +1706,7 @@ public sealed class FileImporter
Value = inFs.Position
});
inFs.EnsureRead(buffer, 0, buffer.Length);
inFs.ReadExactly(buffer, 0, buffer.Length);
outFs.Write(buffer, 0, buffer.Length);
SetIndeterminateProgress2?.Invoke(this, System.EventArgs.Empty);

View File

@@ -41,13 +41,13 @@ public sealed class Context(DbContextOptions options) : DbContext(options)
public DbSet<MediaByMachine> MediasByMachines { get; set; }
public DbSet<RomSetStat> RomSetStats { get; set; }
public static Context Create(string dbPath)
public static Context Create(string dbPath, ILoggerFactory loggerFactory)
{
var optionsBuilder = new DbContextOptionsBuilder();
optionsBuilder.UseLazyLoadingProxies()
#if DEBUG
.UseLoggerFactory(LoggerFactory.Create(builder => builder.AddConsole()))
.UseLoggerFactory(loggerFactory)
#endif
.UseSqlite($"Data Source={dbPath}");

View File

@@ -24,10 +24,12 @@
*******************************************************************************/
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Logging;
namespace RomRepoMgr.Database;
public class ContextFactory : IDesignTimeDbContextFactory<Context>
{
public Context CreateDbContext(string[] args) => Context.Create("romrepo.db");
public Context CreateDbContext(string[] args) =>
Context.Create("romrepo.db", LoggerFactory.Create(builder => builder.AddConsole()));
}

View File

@@ -1,10 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0-beta.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0-beta.1</InformationalVersion>
<Copyright>© $([System.DateTime]::Now.Year) Natalia Portillo</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design"/>
<PackageReference Include="Microsoft.Extensions.Logging.Console"/>
<PackageReference Include="AsyncFixer"/>
<PackageReference Include="ErrorProne.NET.CoreAnalyzers"/>

View File

@@ -232,7 +232,7 @@ public static class DetectOS
/// <returns>Current operating system version</returns>
public static string GetVersion()
{
var environ = Environment.OSVersion.Version.ToString();
string environ = Environment.OSVersion.Version.ToString();
switch(GetRealPlatformID())
{

View File

@@ -1,4 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.0.0-beta.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0-beta.1</InformationalVersion>
<Copyright>© $([System.DateTime]::Now.Year) Natalia Portillo</Copyright>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Win32.Registry"/>

View File

@@ -33,12 +33,21 @@ using PlatformID = Aaru.CommonTypes.Interop.PlatformID;
namespace RomRepoMgr.Settings;
public enum CompressionType
{
Lzip = 0,
Zstd,
None
}
public sealed class SetSettings
{
public string DatabasePath { get; set; }
public string RepositoryPath { get; set; }
public string TemporaryFolder { get; set; }
public string UnArchiverPath { get; set; }
public string DatabasePath { get; set; }
public string RepositoryPath { get; set; }
public string TemporaryFolder { get; set; }
public string UnArchiverPath { get; set; }
public CompressionType Compression { get; set; }
public bool UseInternalDecompressor { get; set; }
}
/// <summary>Manages statistics</summary>
@@ -48,7 +57,7 @@ public static class Settings
const string XDG_CONFIG_HOME_RESOLVED = ".config";
/// <summary>Current statistics</summary>
public static SetSettings Current;
public static bool UnArUsable { get; set; }
public static bool CanDecompress { get; set; }
/// <summary>Loads settings</summary>
public static void LoadSettings()
@@ -105,6 +114,15 @@ public static class Settings
? ((NSString)obj).ToString()
: null;
Current.Compression = parsedPreferences.TryGetValue("Compression", out obj)
? (CompressionType)Enum.Parse(typeof(CompressionType),
((NSNumber)obj).ToString())
: CompressionType.Lzip;
Current.UseInternalDecompressor =
parsedPreferences.TryGetValue("UseInternalDecompressor", out obj) &&
((NSNumber)obj).ToBool();
prefsFs.Close();
}
else
@@ -146,10 +164,16 @@ public static class Settings
return;
}
Current.DatabasePath = key.GetValue("DatabasePath") as string;
Current.RepositoryPath = key.GetValue("RepositoryPath") as string;
Current.TemporaryFolder = key.GetValue("TemporaryFolder") as string;
Current.UnArchiverPath = key.GetValue("UnArchiverPath") as string;
Current.DatabasePath = key.GetValue("DatabasePath") as string;
Current.RepositoryPath = key.GetValue("RepositoryPath") as string;
Current.TemporaryFolder = key.GetValue("TemporaryFolder") as string;
Current.UnArchiverPath = key.GetValue("UnArchiverPath") as string;
Current.UseInternalDecompressor = key.GetValue("UseInternalDecompressor") as int? > 0;
if(key.GetValue("Compression") is int compression)
Current.Compression = (CompressionType)compression;
else
Current.Compression = CompressionType.Lzip;
}
break;
@@ -222,6 +246,12 @@ public static class Settings
},
{
"UnArchiverPath", Current.UnArchiverPath
},
{
"Compression", (NSNumber)(int)Current.Compression
},
{
"UseInternalDecompressor", new NSNumber(Current.UseInternalDecompressor ? 1 : 0)
}
};
@@ -252,10 +282,12 @@ public static class Settings
RegistryKey key = parentKey?.CreateSubKey("RomRepoMgr");
key?.SetValue("DatabasePath", Current.DatabasePath);
key?.SetValue("RepositoryPath", Current.RepositoryPath);
key?.SetValue("TemporaryFolder", Current.TemporaryFolder);
key?.SetValue("UnArchiverPath", Current.UnArchiverPath);
key?.SetValue("DatabasePath", Current.DatabasePath);
key?.SetValue("RepositoryPath", Current.RepositoryPath);
key?.SetValue("TemporaryFolder", Current.TemporaryFolder);
key?.SetValue("UnArchiverPath", Current.UnArchiverPath);
key?.SetValue("Compression", (int)Current.Compression);
key?.SetValue("UseInternalDecompressor", Current.UseInternalDecompressor ? 1 : 0);
}
break;
@@ -309,7 +341,8 @@ public static class Settings
{
DatabasePath = Path.Combine(dataPath, "romrepo.db"),
RepositoryPath = Path.Combine(dataPath, "repo"),
TemporaryFolder = Path.GetTempPath()
TemporaryFolder = Path.GetTempPath(),
Compression = CompressionType.Lzip
};
}
}

View File

@@ -26,12 +26,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
Directory.Packages.props = Directory.Packages.props
global.json = global.json
README.md = README.md
logo.png = logo.png
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SabreTools.DatTools", "SabreTools\SabreTools.DatTools\SabreTools.DatTools.csproj", "{20CC9ED6-9F56-49B5-A265-DC246C424F27}"
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 +83,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

View File

@@ -1,18 +1,22 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xml:space="preserve">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Aaru/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Claunia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=datfiles/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=dlclose/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=HKLM/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=libdl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lsar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=romrepo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=romrepombgrfs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=romrepomgr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=setxattr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Umounted/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Winfsp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xattr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xattrs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=_0020_007B_000A_0020_0020_0020_0020/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Claunia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=datfiles/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=dlclose/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=HKLM/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=libdl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lsar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=romrepo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=romrepombgrfs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=romrepomgr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=setxattr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Umounted/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Winfsp/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xattr/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=xattrs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean
x:Key="/Default/UserDictionary/Words/=_0020_007B_000A_0020_0020_0020_0020/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using RomRepoMgr.Core.EventArgs;
@@ -24,7 +25,7 @@ public partial class DatImporter : ObservableObject
public Task Task { get; set; }
public bool Running { get; private set; } = true;
internal void OnErrorOccurred(object sender, ErrorEventArgs e)
internal void OnErrorOccurred(object sender, ErrorEventArgs e) => Dispatcher.UIThread.Post(() =>
{
StatusMessage = e.Message;
StatusColor = Colors.Red;
@@ -33,31 +34,25 @@ public partial class DatImporter : ObservableObject
Indeterminate = false;
Progress = 0;
}
});
internal void OnSetIndeterminateProgress(object sender, EventArgs e)
{
Indeterminate = true;
}
internal void OnSetIndeterminateProgress(object sender, EventArgs e) =>
Dispatcher.UIThread.Post(() => Indeterminate = true);
internal void OnSetMessage(object sender, MessageEventArgs e)
{
StatusMessage = e.Message;
}
internal void OnSetMessage(object sender, MessageEventArgs e) =>
Dispatcher.UIThread.Post(() => StatusMessage = e.Message);
internal void OnSetProgress(object sender, ProgressEventArgs e)
{
Progress = e.Value;
}
internal void OnSetProgress(object sender, ProgressEventArgs e) =>
Dispatcher.UIThread.Post(() => Progress = e.Value);
internal void OnSetProgressBounds(object sender, ProgressBoundsEventArgs e)
internal void OnSetProgressBounds(object sender, ProgressBoundsEventArgs e) => Dispatcher.UIThread.Post(() =>
{
Indeterminate = false;
Maximum = e.Maximum;
Minimum = e.Minimum;
}
});
internal void OnWorkFinished(object sender, MessageEventArgs e)
internal void OnWorkFinished(object sender, MessageEventArgs e) => Dispatcher.UIThread.Post(() =>
{
Indeterminate = false;
Maximum = 1;
@@ -65,5 +60,5 @@ public partial class DatImporter : ObservableObject
Progress = 1;
StatusMessage = e.Message;
Running = false;
}
});
}

View File

@@ -49,7 +49,6 @@ internal static class Program
try
{
Log.Information("Starting up");
Log.Debug("Testing debug logging");
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}

View File

@@ -770,5 +770,23 @@ namespace RomRepoMgr.Resources {
return ResourceManager.GetString("ProgressLabel", resourceCulture);
}
}
public static string CompressionType {
get {
return ResourceManager.GetString("CompressionType", resourceCulture);
}
}
public static string UseInternalDecompressorLabel {
get {
return ResourceManager.GetString("UseInternalDecompressorLabel", resourceCulture);
}
}
public static string ProcessingArchive {
get {
return ResourceManager.GetString("ProcessingArchive", resourceCulture);
}
}
}
}

View File

@@ -381,4 +381,13 @@ Tardará mucho tiempo...</value>
<data name="ProgressLabel" xml:space="preserve">
<value>Progreso</value>
</data>
<data name="CompressionType" xml:space="preserve">
<value>Compresión</value>
</data>
<data name="UseInternalDecompressorLabel" xml:space="preserve">
<value>Usar decompresor interno (soporta menos formatos)</value>
</data>
<data name="ProcessingArchive" xml:space="preserve">
<value>Procesando archivo: {0}</value>
</data>
</root>

View File

@@ -389,4 +389,13 @@ This will take a long time...</value>
<data name="ProgressLabel" xml:space="preserve">
<value>Progress</value>
</data>
<data name="CompressionType" xml:space="preserve">
<value>Compression</value>
</data>
<data name="UseInternalDecompressorLabel" xml:space="preserve">
<value>Use internal decompressor (supports less formats)</value>
</data>
<data name="ProcessingArchive" xml:space="preserve">
<value>Processing archive: {0}</value>
</data>
</root>

View File

@@ -1,6 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
</PropertyGroup>
<PropertyGroup>
<Version>1.0.0-beta.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<InformationalVersion>1.0.0-beta.1</InformationalVersion>
<Copyright>© $([System.DateTime]::Now.Year) Natalia Portillo</Copyright>
<ApplicationIcon>Assets/romrepomgr.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
@@ -36,6 +48,7 @@
<PackageReference Include="Serilog.Sinks.Console"/>
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer"/>
<PackageReference Include="Text.Analyzers"/>
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Condition="$(RuntimeIdentifier.StartsWith('linux'))"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RomRepoMgr.Database\RomRepoMgr.Database.csproj"/>
@@ -48,4 +61,39 @@
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Target Name="CreateMacAppBundle" AfterTargets="Publish" Condition="'$(RuntimeIdentifier)' == 'osx-x64' Or '$(RuntimeIdentifier)' == 'osx-arm64'">
<!-- Create Directory Structure -->
<MakeDir Directories="
$(PublishDir)MacPackage/RomRepoMgr.app/Contents/MacOS;
$(PublishDir)MacPackage/RomRepoMgr.app/Contents/Resources" />
<!-- Copy Executable -->
<Copy SourceFiles="$(PublishDir)RomRepoMgr" DestinationFiles="$(PublishDir)MacPackage/RomRepoMgr.app/Contents/MacOS/RomRepoMgr" />
<!-- Copy .dylib files -->
<ItemGroup>
<DylibFiles Include="$(PublishDir)**\*.dylib" />
</ItemGroup>
<Copy SourceFiles="@(DylibFiles)" DestinationFolder="$(PublishDir)MacPackage/RomRepoMgr.app/Contents/MacOS\%(RecursiveDir)" />
<!-- Copy Icon (optional) -->
<Copy SourceFiles="Assets/RomRepoMgr.icns" DestinationFiles="$(PublishDir)MacPackage/RomRepoMgr.app/Contents/Resources/RomRepoMgr.icns" />
<!-- Generate Info.plist -->
<WriteLinesToFile File="$(PublishDir)MacPackage/RomRepoMgr.app/Contents/Info.plist" Lines="
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;!DOCTYPE plist PUBLIC &quot;-//Apple//DTD PLIST 1.0//EN&quot; &quot;http://www.apple.com/DTDs/PropertyList-1.0.dtd&quot;&gt;
&lt;plist version=&quot;1.0&quot;&gt;
&lt;dict&gt;
&lt;key&gt;CFBundleName&lt;/key&gt;&lt;string&gt;RomRepoMgr&lt;/string&gt;
&lt;key&gt;CFBundleIdentifier&lt;/key&gt;&lt;string&gt;es.natportillo.romrepomgr&lt;/string&gt;
&lt;key&gt;CFBundleVersion&lt;/key&gt;&lt;string&gt;1.0&lt;/string&gt;
&lt;key&gt;CFBundleExecutable&lt;/key&gt;&lt;string&gt;RomRepoMgr&lt;/string&gt;
&lt;key&gt;CFBundleIconFile&lt;/key&gt;&lt;string&gt;RomRepoMgr.icns&lt;/string&gt;
&lt;key&gt;NSHumanReadableCopyright&lt;/key&gt;&lt;string&gt;© 2025 Nat Portillo.&lt;/string&gt;
&lt;/dict&gt;
&lt;/plist&gt;" Overwrite="true" />
</Target>
</Project>

View File

@@ -14,23 +14,27 @@ public class SerilogSink(LogEventLevel minimumLevel, IList<string>? areas = null
public void Log(LogEventLevel level, string area, object? source, string messageTemplate)
{
if(IsEnabled(level, area))
{
Serilog.Log.Write(LogLevelToSerilogLevel(level),
"[{Area} {Source}] {MessageTemplate}",
area,
source,
messageTemplate);
}
}
public void Log(LogEventLevel level, string area, object? source, string messageTemplate,
params object?[] propertyValues)
{
if(IsEnabled(level, area))
{
Serilog.Log.Write(LogLevelToSerilogLevel(level),
"[{Area} {Source}] {MessageTemplate}",
propertyValues,
area,
source,
messageTemplate);
}
}
private static Serilog.Events.LogEventLevel LogLevelToSerilogLevel(LogEventLevel level)

View File

@@ -33,6 +33,8 @@ using RomRepoMgr.Core.Models;
using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -207,7 +209,8 @@ public partial class EditDatViewModel : ViewModelBase
async Task ExecuteSaveCommandAsync()
{
await using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
await using var ctx =
Context.Create(Settings.Settings.Current.DatabasePath, new SerilogLoggerFactory(Log.Logger));
RomSet romSetDb = await ctx.RomSets.FindAsync(_romSet.Id);

View File

@@ -95,9 +95,9 @@ public sealed partial class ExportDatViewModel : ViewModelBase
ProgressVisible = true;
StatusMessage = Localization.DecompressingDat;
var sha384Bytes = new byte[48];
byte[] sha384Bytes = new byte[48];
for(var i = 0; i < 48; i++)
for(int i = 0; i < 48; i++)
{
if(_datHash[i * 2] >= 0x30 && _datHash[i * 2] <= 0x39)
sha384Bytes[i] = (byte)((_datHash[i * 2] - 0x30) * 0x10);

View File

@@ -32,6 +32,8 @@ using CommunityToolkit.Mvvm.Input;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Workers;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -154,7 +156,7 @@ public sealed partial class ExportRomsViewModel : ViewModelBase
public void OnOpened()
{
var worker = new FileExporter(_romSetId, FolderPath);
var worker = new FileExporter(_romSetId, FolderPath, new SerilogLoggerFactory(Log.Logger));
worker.SetMessage += OnWorkerOnSetMessage;
worker.SetProgress += OnWorkerOnSetProgress;
worker.SetProgressBounds += OnWorkerOnSetProgressBounds;

View File

@@ -14,6 +14,8 @@ using CommunityToolkit.Mvvm.Input;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Models;
using RomRepoMgr.Resources;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -134,7 +136,9 @@ public sealed partial class ImportDatFolderViewModel : ViewModelBase
Indeterminate = false
};
var worker = new Core.Workers.DatImporter(_datFiles[_listPosition], Category);
var worker =
new Core.Workers.DatImporter(_datFiles[_listPosition], Category, new SerilogLoggerFactory(Log.Logger));
worker.ErrorOccurred += model.OnErrorOccurred;
worker.SetIndeterminateProgress += model.OnSetIndeterminateProgress;
worker.SetMessage += model.OnSetMessage;
@@ -150,6 +154,13 @@ public sealed partial class ImportDatFolderViewModel : ViewModelBase
if(_workers < Environment.ProcessorCount) Import();
};
worker.ErrorOccurred += (_, _) =>
{
_workers--;
if(_workers < Environment.ProcessorCount) Import();
};
Importers.Add(model);
model.Task = Task.Run(worker.Import);

View File

@@ -32,6 +32,8 @@ using CommunityToolkit.Mvvm.Input;
using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Workers;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -68,7 +70,7 @@ public sealed partial class ImportDatViewModel : ViewModelBase
IndeterminateProgress = true;
ProgressVisible = false;
ErrorVisible = false;
_worker = new DatImporter(datPath, null);
_worker = new DatImporter(datPath, null, new SerilogLoggerFactory(Log.Logger));
_worker.ErrorOccurred += OnWorkerOnErrorOccurred;
_worker.SetIndeterminateProgress += OnWorkerOnSetIndeterminateProgress;
_worker.SetMessage += OnWorkerOnSetMessage;

View File

@@ -19,16 +19,22 @@ using RomRepoMgr.Database.Models;
using RomRepoMgr.Models;
using RomRepoMgr.Resources;
using Serilog;
using Serilog.Extensions.Logging;
using SharpCompress.Readers;
namespace RomRepoMgr.ViewModels;
public sealed partial class ImportRomFolderViewModel : ViewModelBase
{
readonly Context _ctx = Context.Create(Settings.Settings.Current.DatabasePath);
readonly ConcurrentBag<DbDisk> _newDisks = [];
readonly ConcurrentBag<DbFile> _newFiles = [];
readonly ConcurrentBag<DbMedia> _newMedias = [];
readonly Stopwatch _stopwatch = new();
readonly Context _ctx =
Context.Create(Settings.Settings.Current.DatabasePath, new SerilogLoggerFactory(Log.Logger));
readonly Stopwatch _mainStopwatch = new();
readonly ConcurrentBag<DbDisk> _newDisks = [];
readonly ConcurrentBag<DbFile> _newFiles = [];
readonly ConcurrentBag<DbMedia> _newMedias = [];
readonly Stopwatch _stopwatch = new();
[ObservableProperty]
bool _canChoose;
[ObservableProperty]
bool _canClose;
[ObservableProperty]
@@ -75,7 +81,6 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
[ObservableProperty]
bool _statusMessage2Visible;
public ImportRomFolderViewModel()
{
SelectFolderCommand = new AsyncRelayCommand(SelectFolderAsync);
@@ -84,8 +89,9 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
CanClose = true;
RemoveFilesChecked = false;
KnownOnlyChecked = true;
RecurseArchivesChecked = Settings.Settings.UnArUsable;
RecurseArchivesChecked = Settings.Settings.CanDecompress;
RemoveFilesEnabled = false;
CanChoose = true;
}
public ICommand SelectFolderCommand { get; }
@@ -93,7 +99,7 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
public ICommand StartCommand { get; }
public Window View { get; init; }
public bool RecurseArchivesEnabled => Settings.Settings.UnArUsable;
public bool RecurseArchivesEnabled => Settings.Settings.CanDecompress;
public bool RecurseArchivesChecked
{
@@ -122,6 +128,9 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
CanClose = false;
CanStart = false;
IsImporting = true;
IsReady = false;
CanChoose = false;
_mainStopwatch.Start();
_ = Task.Run(() => _rootImporter.FindFiles(FolderPath));
}
@@ -178,7 +187,11 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
_ = Task.Run(() =>
{
_stopwatch.Restart();
_rootImporter.SeparateFilesAndArchives();
if(Settings.Settings.Current.UseInternalDecompressor)
_rootImporter.SeparateFilesAndArchivesManaged();
else
_rootImporter.SeparateFilesAndArchives();
});
}
else
@@ -253,6 +266,9 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
IsReady = false;
IsImporting = false;
StatusMessage = Localization.Finished;
_mainStopwatch.Stop();
Log.Debug("Took {TotalSeconds} seconds to import ROMs", _mainStopwatch.Elapsed.TotalSeconds);
}
void ProcessArchives()
@@ -285,7 +301,9 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
RemoveFilesChecked);
// Extract archive
bool ret = archiveImporter.ExtractArchive(archive);
bool ret = Settings.Settings.Current.UseInternalDecompressor
? archiveImporter.ExtractArchiveManaged(archive)
: archiveImporter.ExtractArchive(archive);
if(!ret) return;
@@ -334,6 +352,97 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
ProcessFiles();
}
void ProcessArchivesManaged()
{
// For each archive
ProgressMaximum = _rootImporter.Archives.Count;
ProgressMinimum = 0;
ProgressValue = 0;
ProgressIsIndeterminate = false;
Progress2Visible = false;
StatusMessage2Visible = false;
_listPosition = 0;
_stopwatch.Restart();
Parallel.ForEach(_rootImporter.Archives,
archive =>
{
Dispatcher.UIThread.Post(() =>
{
StatusMessage =
string.Format(Localization.ProcessingArchive, Path.GetFileName(archive));
ProgressValue = _listPosition;
});
// Create FileImporter
var archiveImporter = new FileImporter(_ctx,
_newFiles,
_newDisks,
_newMedias,
KnownOnlyChecked,
RemoveFilesChecked);
// Open archive
try
{
using var fs = new FileStream(archive, FileMode.Open, FileAccess.Read);
using IReader reader = ReaderFactory.Open(fs);
// Process files in archive
while(reader.MoveToNextEntry())
{
if(reader.Entry.IsDirectory) continue;
if(reader.Entry.Crc == 0 && KnownOnlyChecked) continue;
if(!archiveImporter.IsCrcInDb(reader.Entry.Crc) && KnownOnlyChecked) continue;
var model = new RomImporter
{
Filename = Path.GetFileName(reader.Entry.Key),
Indeterminate = true
};
var worker = new FileImporter(_ctx,
_newFiles,
_newDisks,
_newMedias,
KnownOnlyChecked,
RemoveFilesChecked);
worker.SetIndeterminateProgress2 += model.OnSetIndeterminateProgress;
worker.SetMessage2 += model.OnSetMessage;
worker.SetProgress2 += model.OnSetProgress;
worker.SetProgressBounds2 += model.OnSetProgressBounds;
worker.ImportedRom += model.OnImportedRom;
worker.WorkFinished += model.OnWorkFinished;
Dispatcher.UIThread.Post(() => Importers.Add(model));
worker.ImportAndHashRom(reader.OpenEntryStream(),
reader.Entry.Key,
Path.Combine(Settings.Settings.Current.RepositoryPath,
Path.GetFileName(Path.GetTempFileName())),
reader.Entry.Size);
}
}
catch(InvalidOperationException) {}
finally
{
Interlocked.Increment(ref _listPosition);
}
});
_stopwatch.Stop();
Log.Debug("Took {TotalSeconds} seconds to process archives", _stopwatch.Elapsed.TotalSeconds);
Progress2Visible = false;
StatusMessage2Visible = false;
ProcessFiles();
}
void CheckArchivesFinished(object sender, EventArgs e)
{
_stopwatch.Stop();
@@ -344,7 +453,10 @@ public sealed partial class ImportRomFolderViewModel : ViewModelBase
_rootImporter.Finished -= CheckArchivesFinished;
ProcessArchives();
if(Settings.Settings.Current.UseInternalDecompressor)
ProcessArchivesManaged();
else
ProcessArchives();
}
void SetMessage(object sender, MessageEventArgs e)

View File

@@ -45,6 +45,7 @@ using RomRepoMgr.Core.Models;
using RomRepoMgr.Resources;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -267,7 +268,7 @@ public sealed partial class MainWindowViewModel : ViewModelBase
try
{
Vfs = new Vfs();
Vfs = new Vfs(new SerilogLoggerFactory(Log.Logger));
Vfs.Umounted += VfsOnUmounted;
Vfs.MountTo(result[0].Path.LocalPath);
}

View File

@@ -32,6 +32,8 @@ using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using RomRepoMgr.Resources;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -55,7 +57,8 @@ public sealed partial class RemoveDatViewModel : ViewModelBase
{
_ = Task.Run(() =>
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath,
new SerilogLoggerFactory(Log.Logger));
Dispatcher.UIThread.Post(() => StatusMessage = Localization.RetrievingRomSetFromDatabase);
@@ -73,10 +76,10 @@ public sealed partial class RemoveDatViewModel : ViewModelBase
Dispatcher.UIThread.Post(() => StatusMessage = Localization.RemovingDatFileFromRepo);
var sha384Bytes = new byte[48];
byte[] sha384Bytes = new byte[48];
string sha384 = romSet.Sha384;
for(var i = 0; i < 48; i++)
for(int i = 0; i < 48; i++)
{
if(sha384[i * 2] >= 0x30 && sha384[i * 2] <= 0x39)
sha384Bytes[i] = (byte)((sha384[i * 2] - 0x30) * 0x10);

View File

@@ -26,6 +26,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Platform.Storage;
@@ -39,7 +40,10 @@ using RomRepoMgr.Core.EventArgs;
using RomRepoMgr.Core.Workers;
using RomRepoMgr.Database;
using RomRepoMgr.Resources;
using RomRepoMgr.Settings;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
using ErrorEventArgs = RomRepoMgr.Core.EventArgs.ErrorEventArgs;
namespace RomRepoMgr.ViewModels;
@@ -47,17 +51,22 @@ namespace RomRepoMgr.ViewModels;
public sealed partial class SettingsViewModel : ViewModelBase
{
readonly SettingsDialog _view;
bool _databaseChanged;
string _databasePath;
bool _repositoryChanged;
string _repositoryPath;
bool _temporaryChanged;
string _temporaryPath;
bool _unArChanged;
CompressionType _compression;
bool _compressionChanged;
bool _databaseChanged;
string _databasePath;
bool _repositoryChanged;
string _repositoryPath;
bool _temporaryChanged;
string _temporaryPath;
bool _unArChanged;
[ObservableProperty]
string _unArPath;
[ObservableProperty]
string _unArVersion;
bool _useInternalDecompressor;
bool _useInternalDecompressorChanged;
// Mock
public SettingsViewModel() {}
@@ -77,14 +86,19 @@ public sealed partial class SettingsViewModel : ViewModelBase
DatabaseCommand = new AsyncRelayCommand(ExecuteDatabaseCommandAsync);
SaveCommand = new RelayCommand(ExecuteSaveCommand);
DatabasePath = Settings.Settings.Current.DatabasePath;
RepositoryPath = Settings.Settings.Current.RepositoryPath;
TemporaryPath = Settings.Settings.Current.TemporaryFolder;
UnArPath = Settings.Settings.Current.UnArchiverPath;
DatabasePath = Settings.Settings.Current.DatabasePath;
RepositoryPath = Settings.Settings.Current.RepositoryPath;
TemporaryPath = Settings.Settings.Current.TemporaryFolder;
UnArPath = Settings.Settings.Current.UnArchiverPath;
Compression = Settings.Settings.Current.Compression;
UseInternalDecompressor = Settings.Settings.Current.UseInternalDecompressor;
if(!string.IsNullOrWhiteSpace(UnArPath)) CheckUnAr();
}
public List<CompressionType> CompressionTypes { get; } =
Enum.GetValues(typeof(CompressionType)).Cast<CompressionType>().ToList();
public ICommand UnArCommand { get; }
public ICommand TemporaryCommand { get; }
public ICommand RepositoryCommand { get; }
@@ -124,6 +138,26 @@ public sealed partial class SettingsViewModel : ViewModelBase
}
}
public CompressionType Compression
{
get => _compression;
set
{
SetProperty(ref _compression, value);
_compressionChanged = true;
}
}
public bool UseInternalDecompressor
{
get => _useInternalDecompressor;
set
{
SetProperty(ref _useInternalDecompressor, value);
_useInternalDecompressorChanged = true;
}
}
void CheckUnAr()
{
var worker = new Compression();
@@ -227,7 +261,7 @@ public sealed partial class SettingsViewModel : ViewModelBase
{
try
{
var ctx = Context.Create(result);
var ctx = Context.Create(result, new SerilogLoggerFactory(Log.Logger));
await ctx.Database.MigrateAsync();
}
catch
@@ -295,7 +329,7 @@ public sealed partial class SettingsViewModel : ViewModelBase
try
{
var ctx = Context.Create(result);
var ctx = Context.Create(result, new SerilogLoggerFactory(Log.Logger));
await ctx.Database.MigrateAsync();
}
catch
@@ -326,10 +360,19 @@ public sealed partial class SettingsViewModel : ViewModelBase
if(_unArChanged)
{
Settings.Settings.Current.UnArchiverPath = UnArPath;
Settings.Settings.UnArUsable = true;
Settings.Settings.CanDecompress = true;
}
if(_databaseChanged || _repositoryChanged || _temporaryChanged || _unArChanged)
if(_compressionChanged) Settings.Settings.Current.Compression = Compression;
if(_useInternalDecompressorChanged) Settings.Settings.Current.UseInternalDecompressor = UseInternalDecompressor;
if(_databaseChanged ||
_repositoryChanged ||
_temporaryChanged ||
_unArChanged ||
_compressionChanged ||
_useInternalDecompressorChanged)
Settings.Settings.SaveSettings();
_view.Close();

View File

@@ -39,6 +39,7 @@ using RomRepoMgr.Core.Models;
using RomRepoMgr.Core.Workers;
using RomRepoMgr.Database;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -143,7 +144,9 @@ public sealed partial class SplashWindowViewModel : ViewModelBase
try
{
var worker = new Compression();
Settings.Settings.UnArUsable = worker.CheckUnAr(Settings.Settings.Current.UnArchiverPath);
Settings.Settings.CanDecompress = worker.CheckUnAr(Settings.Settings.Current.UnArchiverPath) ||
Settings.Settings.Current.UseInternalDecompressor;
Dispatcher.UIThread.Post(LoadDatabase);
}
@@ -175,7 +178,8 @@ public sealed partial class SplashWindowViewModel : ViewModelBase
if(!Directory.Exists(dbPathFolder)) Directory.CreateDirectory(dbPathFolder);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath,
new SerilogLoggerFactory(Log.Logger));
Dispatcher.UIThread.Post(MigrateDatabase);
}
@@ -203,7 +207,8 @@ public sealed partial class SplashWindowViewModel : ViewModelBase
{
try
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath,
new SerilogLoggerFactory(Log.Logger));
ctx.Database.Migrate();
@@ -233,7 +238,8 @@ public sealed partial class SplashWindowViewModel : ViewModelBase
{
try
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath,
new SerilogLoggerFactory(Log.Logger));
GotRomSets?.Invoke(this,
new RomSetsEventArgs

View File

@@ -36,6 +36,8 @@ using RomRepoMgr.Database;
using RomRepoMgr.Database.Models;
using RomRepoMgr.Resources;
using RomRepoMgr.Views;
using Serilog;
using Serilog.Extensions.Logging;
namespace RomRepoMgr.ViewModels;
@@ -79,7 +81,8 @@ public sealed partial class UpdateStatsViewModel : ViewModelBase
{
_ = Task.Run(() =>
{
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath);
using var ctx = Context.Create(Settings.Settings.Current.DatabasePath,
new SerilogLoggerFactory(Log.Logger));
Dispatcher.UIThread.Post(() =>
{

View File

@@ -40,106 +40,106 @@
<Design.DataContext>
<vm:AboutViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,*,Auto">
<Grid Grid.Row="0"
ColumnDefinitions="Auto,*">
<Border Grid.Column="0"
BorderThickness="5">
<Image Source="/Assets/avalonia-logo.ico"
Width="48"
Height="48" />
</Border>
<Grid Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0"
Text="{Binding SoftwareName, Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="16"
FontWeight="Bold" />
<TextBlock Grid.Row="1"
Text="{Binding VersionText, Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
</Grid>
<Grid RowDefinitions="Auto,*,Auto"
Margin="16"
RowSpacing="8">
<Grid Grid.Row="0"
ColumnSpacing="8"
ColumnDefinitions="Auto,*">
<Image Source="/Assets/avalonia-logo.ico"
Width="48"
Height="48" />
<Grid Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
RowDefinitions="Auto,Auto"
RowSpacing="8">
<TextBlock Grid.Row="0"
Text="{Binding SoftwareName, Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontSize="16"
FontWeight="Bold" />
<TextBlock Grid.Row="1"
Text="{Binding VersionText, Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
</Grid>
<TabControl Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TabItem>
<TabItem.Header>
<TextBlock Text="{x:Static resources:Localization.AboutLabel}" />
</TabItem.Header>
<Grid RowDefinitions="Auto,12,Auto,12,Auto,Auto,*">
<TextBlock Grid.Row="0"
Text="{Binding SuiteName, Mode=OneWay}" />
<TextBlock Grid.Row="2"
Text="{Binding Copyright, Mode=OneWay}" />
<Button Grid.Row="4"
BorderThickness="0"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Padding="0"
Command="{Binding WebsiteCommand, Mode=OneWay}">
<!-- TODO: TextDecorations="Underline" in next Avalonia UI version -->
<TextBlock Text="{Binding Website, Mode=OneWay}"
Foreground="Blue" />
</Button>
<Button Grid.Row="5"
BorderThickness="0"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Padding="0"
Command="{Binding LicenseCommand, Mode=OneWay}">
<!-- TODO: TextDecorations="Underline" in next Avalonia UI version -->
<TextBlock Text="{x:Static resources:Localization.LicenseLabel}"
Foreground="Blue" />
</Button>
</Grid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Text="{x:Static resources:Localization.LibrariesLabel}" />
</TabItem.Header>
<DataGrid ItemsSource="{Binding Assemblies, Mode=OneWay}"
HorizontalScrollBarVisibility="Visible">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.AssembliesLibraryText}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Version, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.AssembliesVersionText}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Text="{x:Static resources:Localization.AuthorsLabel}" />
</TabItem.Header>
<TextBox IsReadOnly="True"
Text="{x:Static resources:Localization.AuthorsText}" />
</TabItem>
</TabControl>
<Button Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Border>
<TabControl Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<TabItem>
<TabItem.Header>
<TextBlock Text="{x:Static resources:Localization.AboutLabel}" />
</TabItem.Header>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,*"
RowSpacing="8">
<TextBlock Grid.Row="0"
Text="{Binding SuiteName, Mode=OneWay}" />
<TextBlock Grid.Row="1"
Text="{Binding Copyright, Mode=OneWay}" />
<Button Grid.Row="2"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Padding="0"
BorderThickness="0"
Command="{Binding WebsiteCommand, Mode=OneWay}">
<TextBlock Text="{Binding Website, Mode=OneWay}"
Foreground="Blue"
TextDecorations="Underline" />
</Button>
<Button Grid.Row="3"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Padding="0"
BorderThickness="0"
Command="{Binding LicenseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.LicenseLabel}"
Foreground="Blue"
TextDecorations="Underline" />
</Button>
</Grid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Text="{x:Static resources:Localization.LibrariesLabel}" />
</TabItem.Header>
<DataGrid ItemsSource="{Binding Assemblies, Mode=OneWay}"
HorizontalScrollBarVisibility="Visible">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.AssembliesLibraryText}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Version, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.AssembliesVersionText}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
</TabItem>
<TabItem>
<TabItem.Header>
<TextBlock Text="{x:Static resources:Localization.AuthorsLabel}" />
</TabItem.Header>
<TextBox IsReadOnly="True"
Text="{x:Static resources:Localization.AuthorsText}" />
</TabItem>
</TabControl>
<Button Grid.Row="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Window>

View File

@@ -41,226 +41,212 @@
<Design.DataContext>
<vm:EditDatViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto">
<Grid Grid.Row="0"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetNameLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Name, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetVersionLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Version, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetAuthorLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Author, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="3"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetCategoryLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Category, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="4"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetCommentLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Comment, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="5"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetDateLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Date, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="6"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetDescriptionLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Description, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="7"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.HomepageLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Homepage, Mode=TwoWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.TotalMachinesLabel}"
FontWeight="Bold"
Padding="5" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TotalMachines, Mode=OneWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="9"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.CompleteMachinesLabel}"
FontWeight="Bold"
Padding="5" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding CompleteMachines, Mode=OneWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="10"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.IncompleteMachinesLabel}"
FontWeight="Bold"
Padding="5" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding IncompleteMachines, Mode=OneWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="11"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.TotalRomsLabel}"
FontWeight="Bold"
Padding="5" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TotalRoms, Mode=OneWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="12"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.HaveRomsLabel}"
FontWeight="Bold"
Padding="5" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding HaveRoms, Mode=OneWay}"
Padding="5" />
</Grid>
<Grid Grid.Row="13"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.MissRomsLabel}"
FontWeight="Bold"
Padding="5" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding MissRoms, Mode=OneWay}"
Padding="5" />
</Grid>
<StackPanel Grid.Row="14"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding SaveCommand, Mode=OneWay}"
IsVisible="{Binding Modified, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.SaveLabel}" />
</Button>
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CancelCommand, Mode=OneWay}"
IsVisible="{Binding Modified, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CancelLabel}" />
</Button>
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CloseCommand, Mode=OneWay}"
IsVisible="{Binding !Modified, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</StackPanel>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
Margin="16"
RowSpacing="8">
<Grid Grid.Row="0"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetNameLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Name, Mode=TwoWay}" />
</Grid>
</Border>
<Grid Grid.Row="1"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetVersionLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Version, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="2"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetAuthorLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Author, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="3"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetCategoryLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Category, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="4"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetCommentLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Comment, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="5"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetDateLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Date, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="6"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RomSetDescriptionLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Description, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="7"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.HomepageLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding Homepage, Mode=TwoWay}" />
</Grid>
<Grid Grid.Row="8"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.TotalMachinesLabel}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TotalMachines, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="9"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.CompleteMachinesLabel}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding CompleteMachines, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="10"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.IncompleteMachinesLabel}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding IncompleteMachines, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="11"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.TotalRomsLabel}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TotalRoms, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="12"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.HaveRomsLabel}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding HaveRoms, Mode=OneWay}" />
</Grid>
<Grid Grid.Row="13"
ColumnSpacing="8"
ColumnDefinitions="140,*">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.MissRomsLabel}"
FontWeight="Bold" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding MissRoms, Mode=OneWay}" />
</Grid>
<StackPanel Grid.Row="14"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding SaveCommand, Mode=OneWay}"
IsVisible="{Binding Modified, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.SaveLabel}" />
</Button>
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CancelCommand, Mode=OneWay}"
IsVisible="{Binding Modified, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CancelLabel}" />
</Button>
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CloseCommand, Mode=OneWay}"
IsVisible="{Binding !Modified, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</StackPanel>
</Grid>
</Window>

View File

@@ -41,27 +41,27 @@
<Design.DataContext>
<vm:ExportDatViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,auto,Auto,Auto">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="True"
HorizontalAlignment="Stretch"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<TextBlock Grid.Row="2"
Text="{Binding ErrorMessage, Mode=OneWay}"
HorizontalAlignment="Center"
Foreground="Red"
IsVisible="{Binding ErrorVisible, Mode=OneWay}" />
<Button Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Border>
<Grid RowDefinitions="Auto,auto,Auto,Auto"
Margin="16"
RowSpacing="8">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="True"
HorizontalAlignment="Stretch"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<TextBlock Grid.Row="2"
Text="{Binding ErrorMessage, Mode=OneWay}"
HorizontalAlignment="Center"
Foreground="Red"
IsVisible="{Binding ErrorVisible, Mode=OneWay}" />
<Button Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Window>

View File

@@ -41,48 +41,51 @@
<Design.DataContext>
<vm:ExportRomsViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,*,Auto">
<StackPanel Grid.Row="0"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<TextBlock Text="{x:Static resources:Localization.PathLabel}"
FontWeight="Bold" />
<TextBlock Text="{Binding FolderPath, Mode=OneWay}" />
</StackPanel>
<TextBlock Grid.Row="1"
Text="{Binding StatusMessage, Mode=OneWay}"
FontWeight="Bold"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="2"
Minimum="{Binding ProgressMinimum, Mode=OneWay}"
Maximum="{Binding ProgressMaximum, Mode=OneWay}"
Value="{Binding ProgressValue, Mode=OneWay}"
IsIndeterminate="{Binding ProgressIsIndeterminate, Mode=OneWay}"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<StackPanel Grid.Row="3"
IsVisible="{Binding Progress2Visible, Mode=OneWay}">
<TextBlock Text="{Binding Status2Message, Mode=OneWay}" />
<ProgressBar Minimum="{Binding Progress2Minimum, Mode=OneWay}"
Maximum="{Binding Progress2Maximum, Mode=OneWay}"
Value="{Binding Progress2Value, Mode=OneWay}"
IsIndeterminate="{Binding Progress2IsIndeterminate, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Row="4"
IsVisible="{Binding Progress3Visible, Mode=OneWay}">
<TextBlock Text="{Binding Status3Message, Mode=OneWay}" />
<ProgressBar Minimum="{Binding Progress3Minimum, Mode=OneWay}"
Maximum="{Binding Progress3Maximum, Mode=OneWay}"
Value="{Binding Progress3Value, Mode=OneWay}"
IsIndeterminate="{Binding Progress3IsIndeterminate, Mode=OneWay}" />
</StackPanel>
<Button Grid.Row="5"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Border>
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,*,Auto"
Margin="16"
RowSpacing="8">
<StackPanel Grid.Row="0"
Spacing="8"
Orientation="Horizontal"
HorizontalAlignment="Stretch">
<TextBlock Text="{x:Static resources:Localization.PathLabel}"
FontWeight="Bold" />
<TextBlock Text="{Binding FolderPath, Mode=OneWay}" />
</StackPanel>
<TextBlock Grid.Row="1"
Text="{Binding StatusMessage, Mode=OneWay}"
FontWeight="Bold"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="2"
Minimum="{Binding ProgressMinimum, Mode=OneWay}"
Maximum="{Binding ProgressMaximum, Mode=OneWay}"
Value="{Binding ProgressValue, Mode=OneWay}"
IsIndeterminate="{Binding ProgressIsIndeterminate, Mode=OneWay}"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<StackPanel Grid.Row="3"
Spacing="8"
IsVisible="{Binding Progress2Visible, Mode=OneWay}">
<TextBlock Text="{Binding Status2Message, Mode=OneWay}" />
<ProgressBar Minimum="{Binding Progress2Minimum, Mode=OneWay}"
Maximum="{Binding Progress2Maximum, Mode=OneWay}"
Value="{Binding Progress2Value, Mode=OneWay}"
IsIndeterminate="{Binding Progress2IsIndeterminate, Mode=OneWay}" />
</StackPanel>
<StackPanel Grid.Row="4"
Spacing="8"
IsVisible="{Binding Progress3Visible, Mode=OneWay}">
<TextBlock Text="{Binding Status3Message, Mode=OneWay}" />
<ProgressBar Minimum="{Binding Progress3Minimum, Mode=OneWay}"
Maximum="{Binding Progress3Maximum, Mode=OneWay}"
Value="{Binding Progress3Value, Mode=OneWay}"
IsIndeterminate="{Binding Progress3IsIndeterminate, Mode=OneWay}" />
</StackPanel>
<Button Grid.Row="5"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Window>

View File

@@ -41,30 +41,30 @@
<Design.DataContext>
<vm:ImportDatViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,auto,Auto,Auto">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="{Binding IndeterminateProgress, Mode=OneWay}"
Maximum="{Binding MaximumValue, Mode=OneWay}"
Minimum="{Binding MinimumValue, Mode=OneWay}"
Value="{Binding CurrentValue, Mode=OneWay}"
HorizontalAlignment="Stretch"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<TextBlock Grid.Row="2"
Text="{Binding ErrorMessage, Mode=OneWay}"
HorizontalAlignment="Center"
Foreground="Red"
IsVisible="{Binding ErrorVisible, Mode=OneWay}" />
<Button Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Border>
<Grid RowDefinitions="Auto,auto,Auto,Auto"
Margin="16"
RowSpacing="8">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="{Binding IndeterminateProgress, Mode=OneWay}"
Maximum="{Binding MaximumValue, Mode=OneWay}"
Minimum="{Binding MinimumValue, Mode=OneWay}"
Value="{Binding CurrentValue, Mode=OneWay}"
HorizontalAlignment="Stretch"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<TextBlock Grid.Row="2"
Text="{Binding ErrorMessage, Mode=OneWay}"
HorizontalAlignment="Center"
Foreground="Red"
IsVisible="{Binding ErrorVisible, Mode=OneWay}" />
<Button Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Window>

View File

@@ -10,8 +10,7 @@
Height="600"
x:Class="RomRepoMgr.Views.ImportDatFolder"
Title="{x:Static resources:Localization.ImportDatFolderTitle}"
Icon="/Assets/avalonia-logo.ico"
CanResize="False">
Icon="/Assets/avalonia-logo.ico">
<Design.DataContext>
<vm:ImportDatFolderViewModel />
</Design.DataContext>

View File

@@ -10,7 +10,6 @@
Height="768"
x:Class="RomRepoMgr.Views.ImportRomFolder"
Icon="/Assets/avalonia-logo.ico"
CanResize="False"
Title="{x:Static resources:Localization.ImportRomFolderTitle}"
WindowStartupLocation="CenterOwner">
<Design.DataContext>
@@ -26,7 +25,8 @@
<Button Content="{x:Static resources:Localization.ChooseLabel}"
Command="{Binding SelectFolderCommand, Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
VerticalAlignment="Center"
IsVisible="{Binding CanChoose}" />
<TextBlock Text="{x:Static resources:Localization.PathLabel}"
FontWeight="Bold"
VerticalAlignment="Center" />

View File

@@ -41,14 +41,14 @@
<Design.DataContext>
<vm:RemoveDatViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,auto">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="True"
HorizontalAlignment="Stretch" />
</Grid>
</Border>
<Grid RowDefinitions="Auto,auto"
Margin="16"
RowSpacing="8">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="True"
HorizontalAlignment="Stretch" />
</Grid>
</Window>

View File

@@ -31,7 +31,7 @@
xmlns:vm="clr-namespace:RomRepoMgr.ViewModels;assembly=RomRepoMgr"
xmlns:resources="clr-namespace:RomRepoMgr.Resources"
mc:Ignorable="d"
Width="480"
Width="540"
Height="320"
x:Class="RomRepoMgr.Views.SettingsDialog"
Icon="/Assets/avalonia-logo.ico"
@@ -40,115 +40,129 @@
<Design.DataContext>
<vm:SettingsViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto">
<Grid Grid.Row="0"
ColumnDefinitions="*,250,Auto">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.DatabaseFileLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding DatabasePath, Mode=TwoWay}"
IsReadOnly="True"
Padding="5" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DatabaseCommand, Mode=OneWay}"
Padding="5">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<Grid Grid.Row="1"
ColumnDefinitions="*,250,Auto">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RepositoryFolderLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding RepositoryPath, Mode=TwoWay}"
IsReadOnly="True"
Padding="5" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding RepositoryCommand, Mode=OneWay}"
Padding="5">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="*,250,Auto">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.TemporaryFolderLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding TemporaryPath, Mode=TwoWay}"
IsReadOnly="True"
Padding="5" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding TemporaryCommand, Mode=OneWay}"
Padding="5">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<Grid Grid.Row="3"
ColumnDefinitions="*,250,Auto">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.UnArPathLabel}"
FontWeight="Bold"
Padding="5" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding UnArPath, Mode=TwoWay}"
IsReadOnly="True"
Padding="5" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding UnArCommand, Mode=OneWay}"
Padding="5">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<TextBlock Grid.Row="4"
HorizontalAlignment="Left"
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
Margin="16"
RowSpacing="8">
<Grid Grid.Row="0"
ColumnDefinitions="*,250,Auto"
ColumnSpacing="8">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{Binding UnArVersion, Mode=OneWay}"
Text="{x:Static resources:Localization.DatabaseFileLabel}"
FontWeight="Bold" />
<StackPanel Grid.Row="5"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding SaveCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.SaveLabel}" />
</Button>
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</StackPanel>
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding DatabasePath, Mode=TwoWay}"
IsReadOnly="True" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DatabaseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
</Border>
<Grid Grid.Row="1"
ColumnDefinitions="*,250,Auto"
ColumnSpacing="8">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.RepositoryFolderLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding RepositoryPath, Mode=TwoWay}"
IsReadOnly="True" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding RepositoryCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<Grid Grid.Row="2"
ColumnDefinitions="*,250,Auto"
ColumnSpacing="8">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.TemporaryFolderLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding TemporaryPath, Mode=TwoWay}"
IsReadOnly="True" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding TemporaryCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<CheckBox Grid.Row="3"
IsChecked="{Binding UseInternalDecompressor, Mode=TwoWay}">
<CheckBox.Content>
<TextBlock Text="{x:Static resources:Localization.UseInternalDecompressorLabel}" />
</CheckBox.Content>
</CheckBox>
<Grid Grid.Row="4"
ColumnDefinitions="*,250,Auto"
ColumnSpacing="8"
IsVisible="{Binding !UseInternalDecompressor}">
<TextBlock Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.UnArPathLabel}"
FontWeight="Bold" />
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Text="{Binding UnArPath, Mode=TwoWay}"
IsReadOnly="True" />
<Button Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding UnArCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.ChooseLabel}" />
</Button>
</Grid>
<TextBlock Grid.Row="5"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding UnArVersion, Mode=OneWay}"
FontWeight="Bold"
IsVisible="{Binding !UseInternalDecompressor}" />
<Grid Grid.Row="6"
ColumnDefinitions="Auto, *"
ColumnSpacing="8">
<TextBlock Grid.Column="0"
VerticalAlignment="Center"
Text="{x:Static resources:Localization.CompressionType}"
FontWeight="Bold" />
<ComboBox Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SelectedItem="{Binding Compression, Mode=TwoWay}"
ItemsSource="{Binding CompressionTypes, Mode=OneWay}" />
</Grid>
<StackPanel Grid.Row="7"
Spacing="8"
Orientation="Horizontal"
HorizontalAlignment="Right">
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding SaveCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.SaveLabel}" />
</Button>
<Button HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</StackPanel>
</Grid>
</Window>

View File

@@ -11,8 +11,8 @@
Title="ROM Repository Manager"
SystemDecorations="BorderOnly"
WindowStartupLocation="CenterScreen"
Width="250"
Height="175">
Width="320"
Height="240">
<Design.DataContext>
<vm:SplashWindowViewModel />
</Design.DataContext>
@@ -20,12 +20,14 @@
<StackPanel HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Orientation="Vertical"
Margin="5">
Margin="16"
Spacing="8">
<TextBlock Text="{Binding LoadingText, Mode=OneWay}"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Top" />
<StackPanel HorizontalAlignment="Left"
Spacing="8"
Orientation="Horizontal">
<Image MaxWidth="24"
MaxHeight="24"
@@ -52,6 +54,7 @@
VerticalAlignment="Center" />
</StackPanel>
<StackPanel HorizontalAlignment="Left"
Spacing="8"
Orientation="Horizontal">
<Image MaxWidth="24"
MaxHeight="24"
@@ -78,6 +81,7 @@
VerticalAlignment="Center" />
</StackPanel>
<StackPanel HorizontalAlignment="Left"
Spacing="8"
Orientation="Horizontal">
<Image MaxWidth="24"
MaxHeight="24"
@@ -104,6 +108,7 @@
VerticalAlignment="Center" />
</StackPanel>
<StackPanel HorizontalAlignment="Left"
Spacing="8"
Orientation="Horizontal">
<Image MaxWidth="24"
MaxHeight="24"
@@ -130,6 +135,7 @@
VerticalAlignment="Center" />
</StackPanel>
<StackPanel HorizontalAlignment="Left"
Spacing="8"
Orientation="Horizontal">
<Image MaxWidth="24"
MaxHeight="24"

View File

@@ -33,138 +33,137 @@
mc:Ignorable="d"
x:Class="RomRepoMgr.Views.UpdateStats"
Icon="/Assets/avalonia-logo.ico"
CanResize="False"
Title="{x:Static resources:Localization.UpdateStatsTitle}"
WindowStartupLocation="CenterOwner">
<Design.DataContext>
<vm:UpdateStatsViewModel />
</Design.DataContext>
<Border Padding="15">
<Grid RowDefinitions="Auto,Auto,*,Auto">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="{Binding IndeterminateProgress, Mode=OneWay}"
Maximum="{Binding MaximumValue, Mode=OneWay}"
Minimum="{Binding MinimumValue, Mode=OneWay}"
Value="{Binding CurrentValue, Mode=OneWay}"
HorizontalAlignment="Stretch"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<DataGrid Grid.Row="2"
ItemsSource="{Binding RomSets, Mode=OneWay}"
HorizontalScrollBarVisibility="Visible"
SelectedItem="{Binding SelectedRomSet, Mode=TwoWay}"
CanUserSortColumns="True"
CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetNameLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Version, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetVersionLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Author, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetAuthorLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Category, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetCategoryLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Date, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetDateLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Description, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetDescriptionLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Comment, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetCommentLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Homepage, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.HomepageLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TotalMachines, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetTotalMachinesLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding CompleteMachines, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetCompleteMachinesLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding IncompleteMachines, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetIncompleteMachinesLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TotalRoms, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetTotalRomsLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding HaveRoms, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetHaveRomsLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding MissRoms, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetMissRomsLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Border>
<Grid RowDefinitions="Auto,Auto,*,Auto"
Margin="16"
RowSpacing="8">
<TextBlock Grid.Row="0"
Text="{Binding StatusMessage, Mode=OneWay}"
HorizontalAlignment="Center" />
<ProgressBar Grid.Row="1"
IsIndeterminate="{Binding IndeterminateProgress, Mode=OneWay}"
Maximum="{Binding MaximumValue, Mode=OneWay}"
Minimum="{Binding MinimumValue, Mode=OneWay}"
Value="{Binding CurrentValue, Mode=OneWay}"
HorizontalAlignment="Stretch"
IsVisible="{Binding ProgressVisible, Mode=OneWay}" />
<DataGrid Grid.Row="2"
ItemsSource="{Binding RomSets, Mode=OneWay}"
HorizontalScrollBarVisibility="Visible"
SelectedItem="{Binding SelectedRomSet, Mode=TwoWay}"
CanUserSortColumns="True"
CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetNameLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Version, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetVersionLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Author, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetAuthorLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Category, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetCategoryLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Date, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetDateLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Description, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetDescriptionLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Comment, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetCommentLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Homepage, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.HomepageLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TotalMachines, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetTotalMachinesLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding CompleteMachines, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetCompleteMachinesLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding IncompleteMachines, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetIncompleteMachinesLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TotalRoms, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetTotalRomsLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding HaveRoms, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetHaveRomsLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding MissRoms, Mode=OneWay}"
Width="Auto"
IsReadOnly="True">
<DataGridTextColumn.Header>
<TextBlock Text="{x:Static resources:Localization.RomSetMissRomsLabel}" />
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<Button Grid.Row="3"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsEnabled="{Binding CanClose, Mode=OneWay}"
Command="{Binding CloseCommand, Mode=OneWay}">
<TextBlock Text="{x:Static resources:Localization.CloseLabel}" />
</Button>
</Grid>
</Window>

View File

@@ -1 +1 @@
{"projectId":"ad616503-97fa-41cc-8470-2fd4ac56d06f","projectName":"RomRepoMgr"}
{"projectId": "ad616503-97fa-41cc-8470-2fd4ac56d06f", "projectName": "RomRepoMgr"}

5
global.json Normal file
View File

@@ -0,0 +1,5 @@
{
"sdk": {
"version": "9.0.301"
}
}

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB