LANCommander/LANCommander.Server/UI/Pages/Games/Components/MediaEditor.razor
Pat Hartl 20731f355a Fix certain taxonomies (tag, genre, platform) being removed from game on save
Fixes the action, custom field, multiplayer mode, and save path editors from clearing out taxonomies when saving. Also passes the Game/Server/Redistributable ID as a cascading parameter.
2025-12-08 00:51:32 -06:00

297 lines
10 KiB
Text

@using Microsoft.Extensions.Options
@using LANCommander.Server.Settings
@using Microsoft.EntityFrameworkCore
@using System.Net.Mime
@inject MediaService MediaService
@inject GameService GameService
@inject ModalService ModalService
@inject IMessageService MessageService
@inject NavigationManager NavigationManager
@inject IOptions<Settings> Settings
@inject ILogger<MediaEditor> Logger
<Flex Direction="FlexDirection.Vertical" Gap="FlexGap.Large" Style="width: 100%" Class="media-editor">
<Table @ref="ResultsTable" TItem="Media" DataSource="@_media" RowKey="c => c.Id" @bind-SelectedRows="Selected" HidePagination="true" Responsive>
<TitleTemplate>
<Flex Justify="FlexJustify.End" Align="FlexAlign.Center" Gap="@("10")">
@if (Selected.Any())
{
<span>@Selected.Count() selected</span>
}
<Popconfirm Disabled="!Selected.Any()" OnConfirm="DeleteAll" Title="Are you sure you want to delete all selected media?">
<Button Disabled="!Selected.Any()" Danger>Delete</Button>
</Popconfirm>
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<Selection Type="SelectionType.Checkbox" />
<PropertyColumn Property="p => p.Id" Title="Preview" Width="100px" Align="ColumnAlign.Center">
@if (MediaService.FileExists(row))
{
string originalPath = $"/api/Media/{row.Id}/Download?fileId={row.FileId}";
string thumbnailPath = row.Thumbnail != null
? $"/api/Media/{row.Thumbnail?.Id}/Download?fileId={row.Thumbnail?.FileId}"
: $"/api/Media/{row.Id}/Thumbnail?fileId={row.FileId}";
if (row.Type == MediaType.Manual)
{
<Image Width="100px" Src="@thumbnailPath" PreviewVisible="false" OnClick="() => OpenManual(row)" />
}
else
{
<Image Width="100px" Src="@thumbnailPath" PreviewSrc="@originalPath" />
}
}
</PropertyColumn>
<PropertyColumn Property="p => p.Type">
<Select @key="row.Id" @bind-Value="row.Type" TItem="MediaType" TItemValue="MediaType" DataSource="AllowedMediaTypes" />
</PropertyColumn>
<PropertyColumn Property="p => p.Name">
@if (row.Type == MediaType.Manual)
{
<Input @bind-Value="row.Name" OnBlur="() => Update(row)" />
}
</PropertyColumn>
<ActionColumn>
<Flex Gap="FlexGap.Small" Justify="FlexJustify.End">
@{
var fileInputId = Guid.NewGuid();
}
<InputFile id="@fileInputId" OnChange="(e) => UploadMedia(e, row)" hidden accept=".jpg,.jpeg,.png,.gif,.pdf,.ico"/>
<label class="ant-btn ant-btn-text ant-btn-icon-only" for="@fileInputId">
<Icon Type="@IconType.Outline.Upload"/>
</label>
@if (String.IsNullOrWhiteSpace(Settings.Value.Server.Media.SteamGridDbApiKey))
{
<Popconfirm OnConfirm="() => NavigateToSettings()" Title="Invalid SteamGridDB credentials. Setup now?">
<Button Type="ButtonType.Text" Icon="@IconType.Outline.Search"/>
</Popconfirm>
}
else
{
<Button OnClick="() => SearchMedia(row)" Type="ButtonType.Text" Icon="@IconType.Outline.Search"/>
}
<Popconfirm OnConfirm="() => RemoveMedia(row)" Title="Are you sure you want to delete this media?">
<Button Type="ButtonType.Text" Danger Icon="@IconType.Outline.Close"/>
</Popconfirm>
</Flex>
</ActionColumn>
</ColumnDefinitions>
</Table>
<GridRow Justify="RowJustify.End">
<GridCol>
<Button OnClick="AddMedia" Type="@ButtonType.Primary">Add Media</Button>
</GridCol>
</GridRow>
</Flex>
@code {
[CascadingParameter(Name = "GameId")] public Guid GameId { get; set; }
IEnumerable<Media> Selected = new List<Media>();
ITable? ResultsTable;
MediaType[] AllowedMediaTypes =
[
MediaType.Icon,
MediaType.Cover,
MediaType.Background,
MediaType.Logo,
MediaType.Manual
];
string[] ValidMimeTypes =
[
MediaTypeNames.Image.Png,
MediaTypeNames.Image.Jpeg,
MediaTypeNames.Image.Gif,
MediaTypeNames.Application.Pdf,
MediaTypeNames.Image.Icon
];
List<Media> _media = new();
Game _game;
protected override async Task OnInitializedAsync()
{
_game = await GameService.Query(q =>
{
return q
.Include(g => g.Media)
.ThenInclude(m => m.StorageLocation);
})
.GetAsync(GameId);
_media = _game.Media.ToList();
}
async Task Update(Media media)
{
try
{
if (media.Id != Guid.Empty)
{
await MediaService.UpdateAsync(media);
MessageService.Success($"{media.Type} updated!");
}
}
catch (Exception ex)
{
MessageService.Error($"{media.Type} could not be updated.");
Logger.LogError(ex, $"{media.Type} could not be updated.");
}
}
async Task AddMedia()
{
try
{
var types = Enum.GetValues<MediaType>();
var nextType = types.FirstOrDefault(t => !_media.Any(v => v.Type == t) && AllowedMediaTypes.Contains(t));
var media = new Media
{
GameId = GameId,
Type = nextType == MediaType.Icon && _media.Any(v => v.Type == MediaType.Icon) ? MediaType.Manual : nextType,
StorageLocation = await MediaService.GetDefaultStorageLocationAsync(),
Crc32 = String.Empty
};
media = await MediaService.AddAsync(media);
_media.Add(media);
}
catch (Exception ex)
{
MessageService.Error("Could not add new media!");
Logger.LogError(ex, "Could not add new media!");
}
}
async Task SearchMedia(Media media)
{
var modalOptions = new ModalOptions
{
Title = $"Download {media.Type}",
Maximizable = false,
DefaultMaximized = true,
Closable = true,
OkText = "Select",
};
var grabberOptions = new MediaGrabberOptions()
{
Type = media.Type,
Search = _game.Title
};
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberResult>(modalOptions, grabberOptions);
modalRef.OnOk = async (result) =>
{
modalRef.Config.ConfirmLoading = true;
media.SourceUrl = result.SourceUrl;
media.MimeType = result.MimeType;
if (media.Id == Guid.Empty)
{
media = await MediaService.DownloadMediaAsync(result.SourceUrl, media);
await MediaService.AddAsync(media);
}
else
{
MediaService.DeleteLocalMediaFile(media);
media = await MediaService.DownloadMediaAsync(result.SourceUrl, media);
}
_media = (await MediaService.GetAsync(m => m.GameId == media.GameId)).ToList();
};
}
async Task UploadMedia(InputFileChangeEventArgs e, Media media)
{
if (!ValidMimeTypes.Contains(e.File.ContentType))
{
MessageService.Error("Unsupported file type");
Logger.LogError($"Unsupported file type {e.File.ContentType}");
return;
}
var config = Settings.Value.Server.Media.GetMediaTypeConfig(media.Type);
if (e.File.Size > config.MaxFileSize)
{
MessageService.Error($"File size must be smaller than {ByteSizeLib.ByteSize.FromBytes(config.MaxFileSize)}");
return;
}
media.SourceUrl = "";
media.MimeType = e.File.ContentType;
if (media.Id == Guid.Empty)
{
media = await MediaService.WriteToFileAsync(media, e.File.OpenReadStream(maxAllowedSize: config.MaxFileSize));
await MediaService.AddAsync(media);
}
else
{
MediaService.DeleteLocalMediaFile(media);
media = await MediaService.WriteToFileAsync(media, e.File.OpenReadStream(maxAllowedSize: config.MaxFileSize));
await MediaService.UpdateAsync(media);
}
_media = (await MediaService.GetAsync(m => m.GameId == media.GameId)).ToList();
}
async Task DeleteAll()
{
if (!Selected.Any())
return;
_media = _media.Except(Selected).ToList();
await MediaService.DeleteRangeAsync(Selected);
Selected = [];
ResultsTable.ReloadData();
}
async Task RemoveMedia(Media media)
{
_media.Remove(media);
if (media.Id != Guid.Empty)
await MediaService.DeleteAsync(media);
}
async Task OpenManual(Media media)
{
var modalOptions = new ModalOptions()
{
Title = media.Name,
Maximizable = true,
DefaultMaximized = true,
Closable = true,
Footer = null,
Draggable = true,
Resizable = true,
WrapClassName = "pdf-reader-dialog",
};
var modalRef = await ModalService.CreateModalAsync<PdfReaderDialog, Media>(modalOptions, media);
}
void NavigateToSettings()
{
NavigationManager.NavigateTo("/Settings/Media", true);
}
}