LANCommander/LANCommander.Server/UI/Pages/Games/Components/MediaEditor.razor
Pat Hartl a5dab5c094 Merge branch 'main' into refactor/optional-import-export
# Conflicts:
#	LANCommander.Server.Services/GameService.cs
#	LANCommander.Server.Services/ImportService.cs
#	LANCommander.Server.Services/Importers/GameImporter.cs
#	LANCommander.Server.Services/UserService.cs
#	LANCommander.Server/UI/Components/AvatarUploader.razor
#	LANCommander.Server/UI/Components/ImportUploadDialog.razor
2025-07-25 18:42:39 -05:00

300 lines
10 KiB
Text

@using System.Net.Mime
@inject MediaService MediaService
@inject ModalService ModalService
@inject IMessageService MessageService
@inject NavigationManager NavigationManager
@inject ILogger<MediaEditor> Logger
<Flex Vertical Gap="FlexGap.Large" Style="width: 100%" Class="media-editor">
<Table @ref="ResultsTable" TItem="Media" DataSource="@Values" 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.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 {
[Parameter] public ICollection<Media> Values { get; set; } = new List<Media>();
[Parameter] public EventCallback<ICollection<Media>> ValuesChanged { get; set; }
[Parameter] public Guid GameId { get; set; }
[Parameter] public string GameTitle { get; set; }
IEnumerable<Media> Selected = new List<Media>();
ITable? ResultsTable;
Settings Settings = SettingService.GetSettings();
MediaType[] AllowedMediaTypes = new MediaType[]
{
MediaType.Icon,
MediaType.Cover,
MediaType.Background,
MediaType.Logo,
MediaType.Manual
};
MediaType[] SearchableMediaTypes = new MediaType[]
{
MediaType.Icon,
MediaType.Cover,
MediaType.Background,
MediaType.Logo,
MediaType.Manual
};
string[] ValidMimeTypes = new string[]
{
MediaTypeNames.Image.Png,
MediaTypeNames.Image.Jpeg,
MediaTypeNames.Image.Gif,
MediaTypeNames.Application.Pdf,
MediaTypeNames.Image.Icon
};
private 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.");
}
}
private async Task AddMedia()
{
if (Values == null)
Values = new List<Media>();
try
{
var types = Enum.GetValues<MediaType>();
var nextType = types.FirstOrDefault(t => !Values.Any(v => v.Type == t) && AllowedMediaTypes.Contains(t));
var defaultStorageLocation = await MediaService.GetDefaultStorageLocationAsync();
var media = new Media
{
GameId = GameId,
Type = nextType == MediaType.Icon && Values.Any(v => v.Type == MediaType.Icon) ? MediaType.Manual : nextType,
StorageLocationId = defaultStorageLocation.Id,
Crc32 = String.Empty
};
media = await MediaService.AddAsync(media);
Values.Add(await MediaService.GetAsync(media.Id));
}
catch (Exception ex)
{
MessageService.Error("Could not add new media!");
Logger.LogError(ex, "Could not add new media!");
}
}
private 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 = GameTitle
};
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);
}
Values = await MediaService.GetAsync(m => m.GameId == media.GameId);
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
};
}
private 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;
}
if ((e.File.Size / 1024 / 1024) > Settings.Media.MaxSize)
{
MessageService.Error($"File size must be smaller than {Settings.Media.MaxSize}MB");
return;
}
media.SourceUrl = "";
media.MimeType = e.File.ContentType;
if (media.Id == Guid.Empty)
{
media = await MediaService.WriteToFileAsync(media, e.File.OpenReadStream(maxAllowedSize: Settings.Media.MaxSize * 1024 * 1024));
await MediaService.AddAsync(media);
}
else
{
MediaService.DeleteLocalMediaFile(media);
media = await MediaService.WriteToFileAsync(media, e.File.OpenReadStream(maxAllowedSize: Settings.Media.MaxSize * 1024 * 1024));
await MediaService.UpdateAsync(media);
}
Values = await MediaService.GetAsync(m => m.GameId == media.GameId);
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(Values);
}
private async Task DeleteAll()
{
if (!Selected.Any())
return;
Values = Values.Except(Selected).ToList();
await MediaService.DeleteRangeAsync(Selected);
Selected = [];
ResultsTable.ReloadData();
}
private async Task RemoveMedia(Media media)
{
Values.Remove(media);
if (media.Id != Guid.Empty)
await MediaService.DeleteAsync(media);
}
private 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);
}
private void NavigateToSettings()
{
NavigationManager.NavigateTo("/Settings/Media", true);
}
}