513 lines
19 KiB
Text
513 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" Title="Released">
|
|
@(context.Data.ReleasedOn != default ? context.Data.ReleasedOn.ToString("MM/dd/yyyy") : "—")
|
|
</PropertyColumn>
|
|
<PropertyColumn Property="g => g.Data.Platforms" Title="Platforms">
|
|
@string.Join(", ", context.Data.Platforms?.Select(p => p.Name) ?? [])
|
|
</PropertyColumn>
|
|
<PropertyColumn Property="g => g.Data.Developers" Title="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);
|
|
}
|
|
}
|