LANCommander/LANCommander.Server/UI/Pages/Games/Components/MediaEditor.razor
2026-06-22 17:05:47 -05:00

318 lines
11 KiB
Text

@using Microsoft.Extensions.Options
@using LANCommander.Server.Settings
@using LANCommander.Server.Services.Abstractions
@using LANCommander.Server.Services.Models
@using Microsoft.EntityFrameworkCore
@using System.Net.Mime
@inject MediaService MediaService
@inject GameService GameService
@inject IMediaGrabberService MediaGrabberService
@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 if (row.Type == MediaType.Video)
{
<video width="100" src="@originalPath" style="max-width: 100px; max-height: 60px;" controls />
}
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,.webp,.pdf,.ico,.mp4,.webm,.mkv"/>
<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)
&& row.Type != MediaType.Video
&& row.Type != MediaType.Screenshot)
{
<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.Grid,
MediaType.Logo,
MediaType.Manual,
MediaType.Screenshot,
MediaType.Video
];
string[] ValidMimeTypes =
[
MediaTypeNames.Image.Png,
MediaTypeNames.Image.Jpeg,
MediaTypeNames.Image.Gif,
MediaTypeNames.Image.Webp,
MediaTypeNames.Application.Pdf,
MediaTypeNames.Image.Icon,
"video/mp4",
"video/webm",
"video/x-matroska"
];
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, MediaGrabberDownloadResult>(modalOptions, grabberOptions);
modalRef.OnOk = async (downloadResult) =>
{
using var download = downloadResult.Download;
media.SourceUrl = downloadResult.Result.SourceUrl;
media.MimeType = download.MimeType;
if (media.Id == Guid.Empty)
{
media = await MediaService.WriteToFileAsync(media, download.Stream);
}
else
{
MediaService.DeleteLocalMediaFile(media);
media = await MediaService.WriteToFileAsync(media, download.Stream);
}
_media = (await MediaService.GetAsync(m => m.GameId == media.GameId)).ToList();
await InvokeAsync(() =>
{
ResultsTable?.ReloadData();
StateHasChanged();
});
};
}
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();
ResultsTable?.ReloadData();
}
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);
}
}