Adding a game now immediately provides a dialog to lookup a game through metadata providers. Selecting a result will create the game with the populated metadata. An additional option of automatically downloading art (icon, cover, background, logo) is also provided, though may be inaccurate and completely up to the results of the provider.
508 lines
19 KiB
Text
508 lines
19 KiB
Text
@using AntDesign.TableModels
|
|
@using LANCommander.Server.ImportExport.Factories
|
|
@using LANCommander.Server.Services.Providers.Metadata
|
|
@inject MetadataService MetadataService
|
|
@inject GameService GameService
|
|
@inject ImportContextFactory ImportContextFactory
|
|
@inject IMessageService MessageService
|
|
@inject ILogger<MetadataLookupPanel> Logger
|
|
|
|
<Spin Spinning="_importing" Tip="@_importStatus">
|
|
|
|
<Flex Gap="FlexGap.Small" Style="margin-bottom: 12px;">
|
|
<Input @bind-Value="Search" BindOnInput="true" OnkeyDown="SearchFieldOnKeyDown" AutoFocus="AutoFocus" />
|
|
<Button OnClick="() => SearchForGame(Search)" Type="ButtonType.Primary" Disabled="@(String.IsNullOrWhiteSpace(Search) || !_availableProviders.Any())">Search</Button>
|
|
</Flex>
|
|
|
|
@if (_stage == ModalStage.Search)
|
|
{
|
|
@if (_errorMessage != null)
|
|
{
|
|
<Result Status="ResultStatus.Error" Title="Search Failed" SubTitle="@_errorMessage">
|
|
<Extra>
|
|
<Button OnClick="DismissError">Dismiss</Button>
|
|
</Extra>
|
|
</Result>
|
|
}
|
|
else if (_hasSearched)
|
|
{
|
|
<Table
|
|
@ref="ResultsTable"
|
|
TItem="MetadataSearchResult<SDK.Models.Manifest.Game>"
|
|
DataSource="@(_results?.Results ?? [])"
|
|
HidePagination="true"
|
|
Loading="_loading"
|
|
OnRowClick="OnRowClicked"
|
|
@bind-SelectedRows="SelectedResults"
|
|
Responsive>
|
|
|
|
<Selection Key="@context.Id" Type="SelectionType.Radio"/>
|
|
<PropertyColumn Property="g => g.Data.Title" Title="Title"/>
|
|
<PropertyColumn Property="g => g.Data.ReleasedOn" Format="MM/dd/yyyy" Title="Released"/>
|
|
<PropertyColumn Property="g => g.Data.Developers">
|
|
@string.Join(", ", context.Data.Developers?.Select(d => d.Name) ?? [])
|
|
</PropertyColumn>
|
|
</Table>
|
|
|
|
@if (_results?.More ?? false)
|
|
{
|
|
<Button Type="ButtonType.Primary" OnClick="ShowMore" Block>Show More</Button>
|
|
}
|
|
}
|
|
|
|
<Flex Gap="FlexGap.Small" Justify="FlexJustify.End" Style="margin-top: 12px;">
|
|
<Select Value="_selectedProvider" ValueChanged="OnProviderChanged" DataSource="_availableProviders" TItem="string" TItemValue="string"/>
|
|
@if (_availableSubProviders?.Any() == true)
|
|
{
|
|
<Select Value="_selectedSubProvider" ValueChanged="OnSubProviderChanged" TItem="string" TItemValue="string">
|
|
@foreach (var sp in _availableSubProviders)
|
|
{
|
|
<SelectOption Value="@sp.Slug" Label="@sp.Name" TItemValue="string" TItem="string" />
|
|
}
|
|
</Select>
|
|
}
|
|
@if (ShowCancel)
|
|
{
|
|
<Button OnClick="HandleCancel" Style="margin-left: 0;">Cancel</Button>
|
|
}
|
|
</Flex>
|
|
|
|
@ChildContent
|
|
}
|
|
else if (_stage == ModalStage.Merge)
|
|
{
|
|
@if (_errorMessage != null)
|
|
{
|
|
<Result Status="ResultStatus.Error" Title="Import Failed" SubTitle="@_errorMessage">
|
|
<Extra>
|
|
<Button OnClick="DismissError">Dismiss</Button>
|
|
</Extra>
|
|
</Result>
|
|
}
|
|
else
|
|
{
|
|
<div class="ant-table">
|
|
<div class="ant-table-container">
|
|
<div class="ant-table-content">
|
|
<table class="metadata-merge-table">
|
|
<thead class="ant-table-thead">
|
|
<tr>
|
|
<th>Import</th>
|
|
<th>Field</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="ant-table-tbody">
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importTitle" /></td>
|
|
<td class="ant-table-cell">Title</td>
|
|
<td class="ant-table-cell">@_fetchedManifest?.Title</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importDescription" /></td>
|
|
<td class="ant-table-cell">Description</td>
|
|
<td class="ant-table-cell"><span class="metadata-merge-truncate">@_fetchedManifest?.Description</span></td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importReleasedOn" /></td>
|
|
<td class="ant-table-cell">Released On</td>
|
|
<td class="ant-table-cell">@(_fetchedManifest?.ReleasedOn.ToString("MM/dd/yyyy"))</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importSingleplayer" /></td>
|
|
<td class="ant-table-cell">Singleplayer</td>
|
|
<td class="ant-table-cell">@(_fetchedManifest?.Singleplayer == true ? "Yes" : "No")</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importEngine" /></td>
|
|
<td class="ant-table-cell">Engine</td>
|
|
<td class="ant-table-cell">@(_fetchedManifest?.Engine?.Name ?? "—")</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importDevelopers" /></td>
|
|
<td class="ant-table-cell">Developers</td>
|
|
<td class="ant-table-cell">
|
|
@foreach (var item in _fetchedManifest?.Developers ?? [])
|
|
{
|
|
<Tag>@item.Name</Tag>
|
|
}
|
|
</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importPublishers" /></td>
|
|
<td class="ant-table-cell">Publishers</td>
|
|
<td class="ant-table-cell">
|
|
@foreach (var item in _fetchedManifest?.Publishers ?? [])
|
|
{
|
|
<Tag>@item.Name</Tag>
|
|
}
|
|
</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importGenres" /></td>
|
|
<td class="ant-table-cell">Genres</td>
|
|
<td class="ant-table-cell">
|
|
@foreach (var item in _fetchedManifest?.Genres ?? [])
|
|
{
|
|
<Tag>@item.Name</Tag>
|
|
}
|
|
</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importTags" /></td>
|
|
<td class="ant-table-cell">Tags</td>
|
|
<td class="ant-table-cell">
|
|
@foreach (var item in _fetchedManifest?.Tags ?? [])
|
|
{
|
|
<Tag>@item.Name</Tag>
|
|
}
|
|
</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importMultiplayerModes" /></td>
|
|
<td class="ant-table-cell">Multiplayer Modes</td>
|
|
<td class="ant-table-cell">@(_fetchedManifest?.MultiplayerModes?.Count ?? 0) mode(s)</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importSavePaths" /></td>
|
|
<td class="ant-table-cell">Save Paths</td>
|
|
<td class="ant-table-cell">@(_fetchedManifest?.SavePaths?.Count ?? 0) path(s)</td>
|
|
</tr>
|
|
<tr class="ant-table-row">
|
|
<td class="ant-table-cell"><Checkbox @bind-Checked="_importExternalIds" /></td>
|
|
<td class="ant-table-cell">External IDs</td>
|
|
<td class="ant-table-cell">
|
|
@foreach (var item in _fetchedManifest?.ExternalIds ?? [])
|
|
{
|
|
<Tag>@item.Provider: @item.ExternalId</Tag>
|
|
}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<Flex Gap="FlexGap.Small" Justify="FlexJustify.End" Style="margin-top: 12px;">
|
|
<Button OnClick="ImportWithMerge" Loading="_loading" Type="ButtonType.Primary">Import</Button>
|
|
<Button OnClick="BackToSearch" Disabled="_loading">Back</Button>
|
|
@if (ShowCancel)
|
|
{
|
|
<Button OnClick="HandleCancel" Disabled="_loading">Cancel</Button>
|
|
}
|
|
</Flex>
|
|
}
|
|
|
|
</Spin>
|
|
|
|
@code {
|
|
private enum ModalStage { Search, Merge }
|
|
|
|
[Parameter] public Guid GameId { get; set; }
|
|
[Parameter] public EventCallback<Guid> OnImported { get; set; }
|
|
[Parameter] public EventCallback OnCancel { get; set; }
|
|
[Parameter] public bool AutoFocus { get; set; }
|
|
[Parameter] public bool ShowCancel { get; set; } = true;
|
|
[Parameter] public string Search { get; set; } = string.Empty;
|
|
[Parameter] public EventCallback<string> SearchChanged { get; set; }
|
|
[Parameter] public RenderFragment ChildContent { get; set; }
|
|
|
|
ITable? ResultsTable;
|
|
|
|
MetadataSearchResultsCollection<SDK.Models.Manifest.Game>? _results;
|
|
|
|
IEnumerable<MetadataSearchResult<SDK.Models.Manifest.Game>> SelectedResults { get; set; }
|
|
bool _loading = false;
|
|
bool _hasSearched = false;
|
|
int _limit = 10;
|
|
int _offset = 0;
|
|
|
|
string _selectedProvider = string.Empty;
|
|
IEnumerable<string> _availableProviders = [];
|
|
|
|
string? _selectedSubProvider;
|
|
IEnumerable<(string Slug, string Name)>? _availableSubProviders;
|
|
|
|
ModalStage _stage = ModalStage.Search;
|
|
string? _errorMessage = null;
|
|
LANCommander.SDK.Models.Manifest.Game? _fetchedManifest = null;
|
|
|
|
bool _importing = false;
|
|
string _importStatus = "Importing...";
|
|
|
|
bool _importTitle = true;
|
|
bool _importDescription = true;
|
|
bool _importReleasedOn = true;
|
|
bool _importSingleplayer = true;
|
|
bool _importEngine = true;
|
|
bool _importDevelopers = true;
|
|
bool _importPublishers = true;
|
|
bool _importGenres = true;
|
|
bool _importTags = true;
|
|
bool _importMultiplayerModes = true;
|
|
bool _importSavePaths = true;
|
|
bool _importExternalIds = true;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_availableProviders = MetadataService.GetProviderNames();
|
|
_selectedProvider = _availableProviders.FirstOrDefault() ?? string.Empty;
|
|
await UpdateSubProvidersAsync();
|
|
}
|
|
|
|
private async Task OnProviderChanged(string provider)
|
|
{
|
|
_selectedProvider = provider;
|
|
_errorMessage = null;
|
|
await UpdateSubProvidersAsync();
|
|
|
|
if (!string.IsNullOrWhiteSpace(Search))
|
|
await SearchForGame(Search);
|
|
}
|
|
|
|
private async Task OnSubProviderChanged(string subProvider)
|
|
{
|
|
_selectedSubProvider = subProvider;
|
|
_errorMessage = null;
|
|
|
|
if (!string.IsNullOrWhiteSpace(Search))
|
|
await SearchForGame(Search);
|
|
}
|
|
|
|
private async Task UpdateSubProvidersAsync()
|
|
{
|
|
var provider = MetadataService.GetProvider(_selectedProvider);
|
|
|
|
if (provider is not null)
|
|
{
|
|
_availableSubProviders = await provider.GetSubProvidersAsync();
|
|
_selectedSubProvider = _availableSubProviders?.FirstOrDefault().Slug;
|
|
}
|
|
else
|
|
{
|
|
_availableSubProviders = null;
|
|
_selectedSubProvider = null;
|
|
}
|
|
}
|
|
|
|
private async Task OnRowClicked(RowData<MetadataSearchResult<SDK.Models.Manifest.Game>> row)
|
|
{
|
|
ResultsTable?.SetSelection([row.Data.Id]);
|
|
await BeginMerge();
|
|
}
|
|
|
|
private async Task SearchFieldOnKeyDown(KeyboardEventArgs e)
|
|
{
|
|
if ((e.Code == "Enter" || e.Code == "NumpadEnter") && !String.IsNullOrWhiteSpace(Search))
|
|
await SearchForGame(Search);
|
|
}
|
|
|
|
private async Task SearchForGame(string title, bool newSearch = true)
|
|
{
|
|
var provider = MetadataService.GetProvider(_selectedProvider);
|
|
|
|
if (provider is null)
|
|
{
|
|
MessageService.Error($"Provider \"{_selectedProvider}\" is not available");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_loading = true;
|
|
_hasSearched = true;
|
|
_errorMessage = null;
|
|
|
|
if (newSearch)
|
|
{
|
|
_results = null;
|
|
_offset = 0;
|
|
}
|
|
|
|
_results = _availableSubProviders?.Any() == true
|
|
? await provider.SearchGamesAsync(title, _selectedSubProvider, _limit, _offset)
|
|
: await provider.SearchGamesAsync(title, _limit, _offset);
|
|
|
|
_loading = false;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "An error occurred searching {Provider}", _selectedProvider);
|
|
|
|
_loading = false;
|
|
_errorMessage = ex.Message;
|
|
}
|
|
}
|
|
|
|
private async Task BeginMerge()
|
|
{
|
|
var provider = MetadataService.GetProvider(_selectedProvider);
|
|
|
|
if (provider is null)
|
|
return;
|
|
|
|
_loading = true;
|
|
_errorMessage = null;
|
|
|
|
try
|
|
{
|
|
var selected = SelectedResults.First();
|
|
|
|
var manifest = await provider.GetGameAsync(selected.Id);
|
|
|
|
if (manifest is null)
|
|
{
|
|
_errorMessage = "Could not retrieve game metadata from the provider.";
|
|
_loading = false;
|
|
return;
|
|
}
|
|
|
|
_fetchedManifest = manifest;
|
|
|
|
_importTitle = true;
|
|
_importDescription = !string.IsNullOrWhiteSpace(manifest.Description);
|
|
_importReleasedOn = manifest.ReleasedOn != default;
|
|
_importSingleplayer = true;
|
|
_importEngine = manifest.Engine != null;
|
|
_importDevelopers = manifest.Developers?.Any() == true;
|
|
_importPublishers = manifest.Publishers?.Any() == true;
|
|
_importGenres = manifest.Genres?.Any() == true;
|
|
_importTags = manifest.Tags?.Any() == true;
|
|
_importMultiplayerModes = manifest.MultiplayerModes?.Any() == true;
|
|
_importSavePaths = manifest.SavePaths?.Any() == true;
|
|
_importExternalIds = manifest.ExternalIds?.Any() == true;
|
|
|
|
_stage = ModalStage.Merge;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "An error occurred fetching metadata from {Provider}", _selectedProvider);
|
|
_errorMessage = ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_loading = false;
|
|
}
|
|
}
|
|
|
|
private async Task ImportWithMerge()
|
|
{
|
|
if (_fetchedManifest is null)
|
|
return;
|
|
|
|
_loading = true;
|
|
_importing = true;
|
|
_importStatus = "Importing...";
|
|
_errorMessage = null;
|
|
|
|
try
|
|
{
|
|
LANCommander.Server.Data.Models.Game? existing = null;
|
|
|
|
if (GameId != Guid.Empty)
|
|
existing = await GameService.GetAsync(GameId);
|
|
|
|
var manifest = _fetchedManifest;
|
|
manifest.Id = GameId == Guid.Empty ? Guid.NewGuid() : GameId;
|
|
|
|
if (!_importTitle && existing != null)
|
|
manifest.Title = existing.Title;
|
|
|
|
if (!_importDescription && existing != null)
|
|
manifest.Description = existing.Description;
|
|
|
|
if (!_importReleasedOn && existing != null)
|
|
manifest.ReleasedOn = existing.ReleasedOn ?? default;
|
|
|
|
if (!_importSingleplayer && existing != null)
|
|
manifest.Singleplayer = existing.Singleplayer;
|
|
|
|
if (!_importEngine)
|
|
manifest.Engine = null;
|
|
|
|
if (!_importDevelopers)
|
|
manifest.Developers = new List<LANCommander.SDK.Models.Manifest.Company>();
|
|
|
|
if (!_importPublishers)
|
|
manifest.Publishers = new List<LANCommander.SDK.Models.Manifest.Company>();
|
|
|
|
if (!_importGenres)
|
|
manifest.Genres = new List<LANCommander.SDK.Models.Manifest.Genre>();
|
|
|
|
if (!_importTags)
|
|
manifest.Tags = new List<LANCommander.SDK.Models.Manifest.Tag>();
|
|
|
|
if (!_importMultiplayerModes)
|
|
manifest.MultiplayerModes = new List<LANCommander.SDK.Models.Manifest.MultiplayerMode>();
|
|
|
|
if (!_importSavePaths)
|
|
manifest.SavePaths = new List<LANCommander.SDK.Models.Manifest.SavePath>();
|
|
|
|
if (!_importExternalIds)
|
|
manifest.ExternalIds = new List<LANCommander.SDK.Models.Manifest.GameExternalId>();
|
|
|
|
using var context = ImportContextFactory.Create();
|
|
await context.InitializeMetadataUpdateAsync(manifest);
|
|
await context.ImportQueueAsync();
|
|
|
|
_loading = false;
|
|
|
|
await OnImported.InvokeAsync(manifest.Id);
|
|
|
|
_importing = false;
|
|
Reset();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "An error occurred importing metadata from {Provider}", _selectedProvider);
|
|
_errorMessage = ex.Message;
|
|
_loading = false;
|
|
_importing = false;
|
|
}
|
|
}
|
|
|
|
public async Task UpdateStatusAsync(string status)
|
|
{
|
|
_importStatus = status;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void BackToSearch()
|
|
{
|
|
_stage = ModalStage.Search;
|
|
_errorMessage = null;
|
|
_fetchedManifest = null;
|
|
SelectedResults = [];
|
|
}
|
|
|
|
private void DismissError()
|
|
{
|
|
_errorMessage = null;
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
_results = null;
|
|
_limit = 10;
|
|
_offset = 0;
|
|
_hasSearched = false;
|
|
_stage = ModalStage.Search;
|
|
_errorMessage = null;
|
|
_fetchedManifest = null;
|
|
}
|
|
|
|
private async Task HandleCancel()
|
|
{
|
|
Reset();
|
|
await OnCancel.InvokeAsync();
|
|
}
|
|
|
|
private async Task ShowMore()
|
|
{
|
|
_offset += _results?.Limit ?? _limit;
|
|
|
|
await SearchForGame(Search, false);
|
|
}
|
|
}
|