Files
deepdrft/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.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

166 lines
6.4 KiB
Plaintext

@page "/tracks/mixes"
@using DeepDrftManager.Services
@using DeepDrftModels.DTOs
@using DeepDrftModels.Enums
@attribute [Authorize]
@inject ICmsReleaseService CmsReleaseService
@inject ISnackbar Snackbar
@inject ILogger<CmsMixBrowser> Logger
<PageTitle>Mixes — 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">Mixes</MudText>
@if (_loading)
{
<MudProgressCircular Indeterminate="true" Class="mt-4" />
}
else if (_rows.Count == 0)
{
<MudText Typo="Typo.body1" Class="mt-4">No mixes found.</MudText>
}
else
{
<MudTable T="MixRow" Items="_rows" Hover="true" Striped="true" Dense="true" Bordered="false" FixedHeader="true">
<HeaderContent>
<MudTh Style="width: 1%;">Cover</MudTh>
<MudTh>Mix</MudTh>
<MudTh>Artist</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">Waveform</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="Mix">@context.Release.Title</MudTd>
<MudTd DataLabel="Artist">@context.Release.Artist</MudTd>
<MudTd DataLabel="Waveform">
@if (context.HasWaveform)
{
<MudTooltip Text="Waveform generated">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
</MudTooltip>
}
else
{
<MudTooltip Text="No waveform — incomplete">
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Warning" Size="Size.Small" />
</MudTooltip>
}
</MudTd>
<MudTd DataLabel="Actions">
<MudButton Variant="Variant.Outlined"
Size="Size.Small"
StartIcon="@Icons.Material.Filled.GraphicEq"
Disabled="@context.IsGenerating"
OnClick="@(() => GenerateWaveformAsync(context))">
@if (context.IsGenerating)
{
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
<span>Generating…</span>
}
else
{
<span>@(context.HasWaveform ? "Regenerate" : "Generate")</span>
}
</MudButton>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudContainer>
@code {
private List<MixRow> _rows = new();
private bool _loading = true;
protected override async Task OnInitializedAsync() => await LoadAsync();
private async Task LoadAsync()
{
_loading = true;
// Mixes are single-track releases; a single generous page covers the CMS catalogue.
var result = await CmsReleaseService.GetPagedAsync(
ReleaseMedium.Mix, page: 1, pageSize: 100,
sortColumn: "Title", sortDescending: false);
if (result.Success && result.Value is not null)
{
_rows = result.Value.Items
.Select(r => new MixRow
{
Release = r,
HasWaveform = !string.IsNullOrEmpty(r.MixMetadata?.WaveformEntryKey)
})
.ToList();
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Failed to load mixes: {error}", Severity.Error);
_rows = new List<MixRow>();
}
_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 GenerateWaveformAsync(MixRow row)
{
row.IsGenerating = true;
StateHasChanged();
try
{
var result = await CmsReleaseService.GenerateMixWaveformAsync(row.Release.Id);
if (result.Success)
{
// Optimistic update: the trigger succeeded, so the waveform is stored. Unlike SessionBrowser's
// re-fetch (which retrieves the server-generated HeroImageEntryKey), there is nothing to reflect
// back here — HasWaveform is derived from WaveformEntryKey being non-null, which we know is now set.
row.HasWaveform = true;
Snackbar.Add($"Generated waveform for '{row.Release.Title}'.", Severity.Success);
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Waveform generation failed for '{row.Release.Title}': {error}", Severity.Error);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Waveform generation failed for release {ReleaseId}", row.Release.Id);
Snackbar.Add($"Waveform generation failed for '{row.Release.Title}' — please try again.", Severity.Error);
}
finally
{
row.IsGenerating = false;
StateHasChanged();
}
}
private sealed class MixRow
{
public required ReleaseDto Release { get; set; }
public bool HasWaveform { get; set; }
public bool IsGenerating { get; set; }
}
}