Settings for the server are now combined with the settings from the SDK. This introduces a large breaking change and a migration should be created. This refactor utilizes .NET Configuration and the Options pattern. This will allow for the overriding of any setting using envionment variables. It also means that Settings.yml can be used to override any .NET configuration. A SettingsProvider implementation was created, and any updating of settings was changed from SettingsService.SaveSettings() to SettingsProvider.Update(s => { ... })
303 lines
10 KiB
Text
303 lines
10 KiB
Text
@using Microsoft.Extensions.Options
|
|
@using LANCommander.Server.Settings
|
|
@using System.Net.Mime
|
|
@inject MediaService MediaService
|
|
@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="@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.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 {
|
|
[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;
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|