Files
deepdrft/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor
T
daniel-c-harvey 2f47efeb46 CMS Phase 9 Wave 3: Release Archive tab, medium selector, Session/Mix browsers
Renames Genre tab to Release Archive with switch-free medium card group
(Enum.GetValues-driven). Adds MediumFields single dispatch + CutFields/SessionFields/
MixFields per-medium sections embedded by all five upload/edit forms. BatchUpload
enforces single-track invariant for Session/Mix. Adds CmsSessionBrowser (hero-image
upload) and CmsMixBrowser (waveform status + per-row Generate trigger).
ICmsReleaseService/CmsReleaseService wraps api/release endpoints.
Note: medium selector is forward-compat only — API write path pending.
2026-06-12 23:07:15 -04:00

179 lines
7.0 KiB
Plaintext

@page "/tracks/sessions"
@using DeepDrftManager.Services
@using DeepDrftModels.DTOs
@using DeepDrftModels.Enums
@using Microsoft.AspNetCore.Components.Forms
@attribute [Authorize]
@inject ICmsReleaseService CmsReleaseService
@inject ISnackbar Snackbar
@inject ILogger<CmsSessionBrowser> Logger
@inject NavigationManager Navigation
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudButton Variant="Variant.Text"
StartIcon="@Icons.Material.Filled.ArrowBack"
Href="/tracks/archive"
Class="mb-4">
Back to Release Archive
</MudButton>
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
@if (_loading)
{
<MudProgressCircular Indeterminate="true" Class="mt-4" />
}
else if (_rows.Count == 0)
{
<MudText Typo="Typo.body1" Class="mt-4">No sessions found.</MudText>
}
else
{
<MudTable T="SessionRow" Items="_rows" Hover="true" Striped="true" Dense="true" Bordered="false" FixedHeader="true">
<HeaderContent>
<MudTh Style="width: 1%;">Cover</MudTh>
<MudTh Style="width: 1%;">Hero</MudTh>
<MudTh>Session</MudTh>
<MudTh>Artist</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Cover">
@if (!string.IsNullOrEmpty(context.Release.ImagePath))
{
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(context.Release.ImagePath)');"></div>
}
else
{
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
}
</MudTd>
<MudTd DataLabel="Hero">
@if (context.HeroImageEntryKey is { Length: > 0 } heroKey)
{
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
}
else
{
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
}
</MudTd>
<MudTd DataLabel="Session">@context.Release.Title</MudTd>
<MudTd DataLabel="Artist">@context.Release.Artist</MudTd>
<MudTd DataLabel="Actions">
<MudFileUpload T="IBrowserFile"
Accept="image/*"
FilesChanged="@(file => UploadHeroAsync(context, file))"
Disabled="@context.IsUploading">
<ActivatorContent>
<MudButton Variant="Variant.Outlined"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.Image"
Disabled="@context.IsUploading">
@if (context.IsUploading)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Uploading…</span>
}
else
{
<span>@(context.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
}
</MudButton>
</ActivatorContent>
</MudFileUpload>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudContainer>
@code {
private List<SessionRow> _rows = new();
private bool _loading = true;
protected override async Task OnInitializedAsync() => await LoadAsync();
private async Task LoadAsync()
{
_loading = true;
// Sessions are single-track releases; a single generous page covers the CMS catalogue (same
// small-catalogue assumption the album browser makes).
var result = await CmsReleaseService.GetPagedAsync(
ReleaseMedium.Session, page: 1, pageSize: 100,
sortColumn: "Title", sortDescending: false);
if (result.Success && result.Value is not null)
{
_rows = result.Value.Items
.Select(r => new SessionRow
{
Release = r,
HeroImageEntryKey = r.SessionMetadata?.HeroImageEntryKey
})
.ToList();
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Failed to load sessions: {error}", Severity.Error);
_rows = new List<SessionRow>();
}
_loading = false;
}
// Relative path — resolves against the Manager's own origin, proxied by ImageProxyController.
private static string ThumbUrl(string entryKey) =>
$"/api/image/{Uri.EscapeDataString(entryKey)}";
private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file)
{
if (file is null) return;
row.IsUploading = true;
StateHasChanged();
try
{
await using var stream = file.OpenReadStream(maxAllowedSize: 50_000_000);
var result = await CmsReleaseService.UploadSessionHeroImageAsync(
row.Release.Id, stream, file.Name, file.ContentType);
if (result.Success)
{
// The endpoint returns no payload; the entry key is server-generated. Re-fetch the
// release so the hero thumbnail reflects the new key without guessing it.
var refreshed = await CmsReleaseService.GetByIdAsync(row.Release.Id);
if (refreshed.Success && refreshed.Value is { } release)
{
row.Release = release;
row.HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey;
}
Snackbar.Add($"Hero image set for '{row.Release.Title}'.", Severity.Success);
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Hero image upload failed: {error}", Severity.Error);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Hero image upload failed for release {ReleaseId}", row.Release.Id);
Snackbar.Add("Hero image upload failed — please try again.", Severity.Error);
}
finally
{
row.IsUploading = false;
StateHasChanged();
}
}
private sealed class SessionRow
{
public required ReleaseDto Release { get; set; }
public string? HeroImageEntryKey { get; set; }
public bool IsUploading { get; set; }
}
}