KoogleApp/src/Koogle.Web/Components/Pages/Admin/GifManagement.razor

604 lines
22 KiB
Plaintext

@page "/admin/gifs"
@attribute [Authorize(Policy = "ClubAdmin")]
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@using Koogle.Web.Components.Game
@using Koogle.Web.Store.GifState
@using Microsoft.AspNetCore.Authorization
@inject IClubGifService GifService
@inject ICurrentClubContext ClubContext
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject IClubTerminologyService Term
@inject IDispatcher Dispatcher
<PageTitle>GIF-Verwaltung</PageTitle>
<!-- GIF Player Overlay -->
<GifPlayer ShowRating="true" />
<MudText Typo="Typo.h4" Class="mb-2">GIF-Verwaltung</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
GIFs und Videos fuer Wurf-Ereignisse verwalten
</MudText>
@if (_error is not null)
{
<MudAlert Severity="Severity.Error" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="() => _error = null">
@_error
</MudAlert>
}
<MudTabs Elevation="2" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-4">
<MudTabPanel Text="GIFs" Icon="@Icons.Material.Filled.Gif">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudText Typo="Typo.h6">Aktive GIFs</MudText>
<MudStack Row="true" Spacing="2">
<MudButton Variant="Variant.Outlined"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Link"
OnClick="OpenImportDialog">
Von URL importieren
</MudButton>
<MudFileUpload T="IBrowserFile"
Accept=".gif,.mp4,.webm"
FilesChanged="OnFileSelected"
MaximumFileCount="1">
<ActivatorContent>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Upload">
Hochladen
</MudButton>
</ActivatorContent>
</MudFileUpload>
</MudStack>
</MudStack>
<MudTable Items="_gifs.Where(g => !g.IsPendingApproval)" Dense="true" Hover="true" Loading="_isLoading">
<HeaderContent>
<MudTh Style="width: 80px;">Vorschau</MudTh>
<MudTh>Name</MudTh>
<MudTh>Ereignisse</MudTh>
<MudTh>Bewertung</MudTh>
<MudTh>Status</MudTh>
<MudTh Style="width: 150px;">Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
@if (context.ContentType.StartsWith("image/"))
{
<img src="@context.Url" style="max-width: 60px; max-height: 40px;"/>
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Movie" Size="Size.Large"/>
}
</MudTd>
<MudTd DataLabel="Name">
<MudText Typo="Typo.body1"><strong>@context.Name</strong></MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">@FormatFileSize(context.FileSizeBytes)</MudText>
</MudTd>
<MudTd DataLabel="Ereignisse">
@foreach (var evt in GetAssignedEvents(context.AssignedEvents))
{
<MudChip T="string" Size="Size.Small" Color="GetEventColor(evt)" Variant="Variant.Outlined" Class="mr-1">
@GetEventName(evt)
</MudChip>
}
</MudTd>
<MudTd DataLabel="Bewertung">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.ThumbUp" Size="Size.Small" Color="Color.Success" />
<MudText>@context.RatingScore</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">(@context.RatingCount)</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="Status">
@if (context.IsEnabled)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">Aktiv</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Filled">Deaktiviert</MudChip>
}
</MudTd>
<MudTd>
<MudStack Row="true" Spacing="1">
<MudTooltip Text="Gif testen">
<MudIconButton Icon="@Icons.Material.Filled.RunCircle"
Size="Size.Small"
Color="Color.Success"
OnClick="@(() => TestGif(context))" />
</MudTooltip>
<MudTooltip Text="Bearbeiten">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
OnClick="@(() => OpenEditDialog(context))"/>
</MudTooltip>
<MudTooltip Text="@(context.IsEnabled ? "Deaktivieren" : "Aktivieren")">
<MudIconButton Icon="@(context.IsEnabled ? Icons.Material.Filled.VisibilityOff : Icons.Material.Filled.Visibility)"
Size="Size.Small"
Color="@(context.IsEnabled ? Color.Warning : Color.Success)"
OnClick="@(() => ToggleEnabled(context))"/>
</MudTooltip>
<MudTooltip Text="Loeschen">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => ConfirmDelete(context))"/>
</MudTooltip>
</MudStack>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>Keine GIFs vorhanden</MudText>
</NoRecordsContent>
</MudTable>
</MudTabPanel>
<MudTabPanel Text="Ausstehend" Icon="@Icons.Material.Filled.HourglassEmpty" BadgeData="@_pendingCount" BadgeColor="Color.Warning">
<MudText Typo="Typo.h6" Class="mb-4">Ausstehende Einreichungen</MudText>
<MudTable Items="_gifs.Where(g => g.IsPendingApproval)" Dense="true" Hover="true" Loading="_isLoading">
<HeaderContent>
<MudTh Style="width: 80px;">Vorschau</MudTh>
<MudTh>Name</MudTh>
<MudTh>Eingereicht am</MudTh>
<MudTh Style="width: 200px;">Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
@if (context.ContentType.StartsWith("image/"))
{
<img src="@context.Url" style="max-width: 60px; max-height: 40px;" />
}
else
{
<MudIcon Icon="@Icons.Material.Filled.Movie" Size="Size.Large" />
}
</MudTd>
<MudTd DataLabel="Name">
<MudText Typo="Typo.body1"><strong>@context.Name</strong></MudText>
</MudTd>
<MudTd DataLabel="Eingereicht">
@context.CreatedAt.ToString("dd.MM.yyyy HH:mm")
</MudTd>
<MudTd>
<MudStack Row="true" Spacing="1">
<MudButton Variant="Variant.Filled"
Color="Color.Success"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Check"
OnClick="@(() => ApproveGif(context))">
Annehmen
</MudButton>
<MudButton Variant="Variant.Outlined"
Color="Color.Error"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Close"
OnClick="@(() => RejectGif(context))">
Ablehnen
</MudButton>
</MudStack>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>Keine ausstehenden Einreichungen</MudText>
</NoRecordsContent>
</MudTable>
</MudTabPanel>
<MudTabPanel Text="Einreichungs-Tokens" Icon="@Icons.Material.Filled.QrCode">
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudText Typo="Typo.h6">Tokens fuer anonyme Einreichungen</MudText>
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenCreateTokenDialog">
Neues Token
</MudButton>
</MudStack>
<MudTable Items="_tokens" Dense="true" Hover="true" Loading="_isLoading">
<HeaderContent>
<MudTh>QR-Code</MudTh>
<MudTh>Gueltig bis</MudTh>
<MudTh>Nutzung</MudTh>
<MudTh>Status</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudButton Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.QrCode"
OnClick="@(() => ShowQrCode(context))">
QR anzeigen
</MudButton>
</MudTd>
<MudTd DataLabel="Gueltig bis">
@context.ExpiresAt.ToString("dd.MM.yyyy HH:mm")
</MudTd>
<MudTd DataLabel="Nutzung">
@context.SubmissionCount / @(context.MaxSubmissions?.ToString() ?? "∞")
</MudTd>
<MudTd DataLabel="Status">
@if (context.IsValid)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">Aktiv</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Error" Variant="Variant.Filled">Abgelaufen</MudChip>
}
</MudTd>
<MudTd>
<MudIconButton Icon="@Icons.Material.Filled.ContentCopy"
Size="Size.Small"
OnClick="@(() => CopyLink(context))"
Title="Link kopieren"/>
</MudTd>
</RowTemplate>
<NoRecordsContent>
<MudText>Keine Tokens vorhanden</MudText>
</NoRecordsContent>
</MudTable>
</MudTabPanel>
</MudTabs>
@code {
private List<ClubGifDto> _gifs = [];
private List<GifSubmissionTokenDto> _tokens = [];
private bool _isLoading = true;
private string? _error;
private int _pendingCount => _gifs.Count(g => g.IsPendingApproval);
private Dictionary<ThrowEventType, string> _eventNames = new();
protected override async Task OnInitializedAsync()
{
await LoadData();
}
private async Task LoadData()
{
_isLoading = true;
try
{
var clubId = ClubContext.ClubId;
_gifs = (await GifService.GetByClubAsync(clubId, includePending: true)).ToList();
_tokens = (await GifService.GetSubmissionTokensByClubAsync(clubId)).ToList();
await LoadEventNames();
}
catch (Exception ex)
{
_error = ex.Message;
}
finally
{
_isLoading = false;
StateHasChanged();
}
}
private async Task LoadEventNames()
{
var eventTypes = new[] { ThrowEventType.Strike, ThrowEventType.Circle, ThrowEventType.Bell,
ThrowEventType.Gutter, ThrowEventType.NoWood, ThrowEventType.Cleared };
foreach (var evt in eventTypes)
{
_eventNames[evt] = await Term.GetThrowEventName(evt);
}
}
private async Task OnFileSelected(IBrowserFile file)
{
try
{
var parameters = new DialogParameters
{
{ "FileName", file.Name }
};
var dialog = await DialogService.ShowAsync<GifUploadDialog>("GIF hochladen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is CreateClubGifDto dto)
{
_isLoading = true;
StateHasChanged();
using var stream = file.OpenReadStream(maxAllowedSize: 20 * 1024 * 1024);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms);
ms.Position = 0;
var formFile = new FormFileFromStream(ms, file.Name, file.ContentType);
await GifService.UploadAsync(ClubContext.ClubId, formFile, dto);
Snackbar.Add("GIF erfolgreich hochgeladen", Severity.Success);
await LoadData();
}
}
catch (Exception ex)
{
_error = ex.Message;
Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
}
finally
{
_isLoading = false;
}
}
private async Task OpenImportDialog()
{
var dialog = await DialogService.ShowAsync<GifImportDialog>("GIF von URL importieren");
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is ImportClubGifDto dto)
{
try
{
_isLoading = true;
StateHasChanged();
await GifService.ImportFromUrlAsync(ClubContext.ClubId, dto);
Snackbar.Add("GIF erfolgreich importiert", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
_error = ex.Message;
Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
}
finally
{
_isLoading = false;
}
}
}
private async Task OpenEditDialog(ClubGifDto gif)
{
var parameters = new DialogParameters
{
{ "Gif", gif }
};
var dialog = await DialogService.ShowAsync<GifEditDialog>("GIF bearbeiten", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is UpdateClubGifDto dto)
{
try
{
await GifService.UpdateAsync(dto);
Snackbar.Add("GIF aktualisiert", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
_error = ex.Message;
}
}
}
private async Task ToggleEnabled(ClubGifDto gif)
{
try
{
var dto = new UpdateClubGifDto
{
Id = gif.Id,
Name = gif.Name,
AssignedEvents = gif.AssignedEvents,
Description = gif.Description,
IsEnabled = !gif.IsEnabled
};
await GifService.UpdateAsync(dto);
Snackbar.Add(dto.IsEnabled ? "GIF aktiviert" : "GIF deaktiviert", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
_error = ex.Message;
}
}
private async Task ConfirmDelete(ClubGifDto gif)
{
var parameters = new DialogParameters
{
{ "ContentText", $"Soll das GIF \"{gif.Name}\" wirklich geloescht werden?" },
{ "ButtonText", "Loeschen" },
{ "Color", Color.Error }
};
var dialog = await DialogService.ShowAsync<Koogle.Web.Components.Shared.ConfirmDialog>("GIF loeschen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
try
{
await GifService.DeleteAsync(gif.Id);
Snackbar.Add("GIF geloescht", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
_error = ex.Message;
}
}
}
private async Task ApproveGif(ClubGifDto gif)
{
var parameters = new DialogParameters
{
{ "Gif", gif },
{ "IsApproval", true }
};
var dialog = await DialogService.ShowAsync<GifEditDialog>("GIF genehmigen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is UpdateClubGifDto dto)
{
try
{
await GifService.UpdateAsync(dto);
await GifService.ApproveAsync(gif.Id);
Snackbar.Add("GIF genehmigt", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
_error = ex.Message;
}
}
}
private async Task RejectGif(ClubGifDto gif)
{
var parameters = new DialogParameters
{
{ "ContentText", $"Soll das GIF \"{gif.Name}\" abgelehnt werden? Es wird geloescht." },
{ "ButtonText", "Ablehnen" },
{ "Color", Color.Error }
};
var dialog = await DialogService.ShowAsync<Koogle.Web.Components.Shared.ConfirmDialog>("GIF ablehnen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
try
{
await GifService.RejectAsync(gif.Id);
Snackbar.Add("GIF abgelehnt", Severity.Success);
await LoadData();
}
catch (Exception ex)
{
_error = ex.Message;
}
}
}
private async Task OpenCreateTokenDialog()
{
var dialog = await DialogService.ShowAsync<GifTokenDialog>("Neues Einreichungs-Token");
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is CreateGifSubmissionTokenDto dto)
{
try
{
dto = dto with { ClubId = ClubContext.ClubId };
var token = await GifService.CreateSubmissionTokenAsync(dto);
Snackbar.Add("Token erstellt", Severity.Success);
await LoadData();
await ShowQrCode(token);
}
catch (Exception ex)
{
_error = ex.Message;
}
}
}
private async Task ShowQrCode(GifSubmissionTokenDto token)
{
var parameters = new DialogParameters
{
{ "Token", token }
};
await DialogService.ShowAsync<GifQrCodeDialog>("QR-Code", parameters);
}
private async Task CopyLink(GifSubmissionTokenDto token)
{
// Note: Requires JS interop for clipboard
Snackbar.Add($"Link: {token.SubmissionUrl}", Severity.Info);
}
private static List<ThrowEventType> GetAssignedEvents(ThrowEventType events)
{
var result = new List<ThrowEventType>();
if ((events & ThrowEventType.Strike) != 0) result.Add(ThrowEventType.Strike);
if ((events & ThrowEventType.Circle) != 0) result.Add(ThrowEventType.Circle);
if ((events & ThrowEventType.Bell) != 0) result.Add(ThrowEventType.Bell);
if ((events & ThrowEventType.Gutter) != 0) result.Add(ThrowEventType.Gutter);
if ((events & ThrowEventType.NoWood) != 0) result.Add(ThrowEventType.NoWood);
if ((events & ThrowEventType.Cleared) != 0) result.Add(ThrowEventType.Cleared);
return result;
}
private string GetEventName(ThrowEventType type) =>
_eventNames.TryGetValue(type, out var name) ? name : type.ToString();
private static Color GetEventColor(ThrowEventType type) => type switch
{
ThrowEventType.Strike => Color.Success,
ThrowEventType.Circle => Color.Primary,
ThrowEventType.Bell => Color.Warning,
ThrowEventType.Gutter => Color.Error,
ThrowEventType.NoWood => Color.Tertiary,
ThrowEventType.Cleared => Color.Info,
_ => Color.Default
};
private static string FormatFileSize(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
return $"{bytes / (1024.0 * 1024):F1} MB";
}
// Helper class to convert IBrowserFile to IFormFile
private class FormFileFromStream : IFormFile
{
private readonly MemoryStream _stream;
public FormFileFromStream(MemoryStream stream, string fileName, string contentType)
{
_stream = stream;
Name = fileName;
FileName = fileName;
ContentType = contentType;
Length = stream.Length;
}
public string ContentType { get; }
public string ContentDisposition => "";
public IHeaderDictionary Headers => new HeaderDictionary();
public long Length { get; }
public string Name { get; }
public string FileName { get; }
public Stream OpenReadStream() => new MemoryStream(_stream.ToArray());
public void CopyTo(Stream target) => _stream.CopyTo(target);
public async Task CopyToAsync(Stream target, CancellationToken ct = default) => await _stream.CopyToAsync(target, ct);
}
private async Task TestGif(ClubGifDto gifdto)
{
var ev = gifdto.AssignedEvents;
var p = new GifPlaybackDto
{
Id = gifdto.Id,
Url = gifdto.Url,
ContentType = gifdto.ContentType,
Name = gifdto.Name,
EventType = ev
};
Dispatcher.Dispatch(new GifPlaybackStartedAction(p, ev));
}
}