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.
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
@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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user