Consolidate CMS /tracks into standalone /releases page

Retire the Tracks list view; promote the Releases view to /releases with
medium tabs (ALL/CUTS/SESSIONS/MIXES). Migrate bulk profile/high-res
backfill and per-track waveform columns into the releases grids. Point
catalogue cards at the three mediums. Remove dead BrowseMode/ViewModel.
This commit is contained in:
daniel-c-harvey
2026-06-17 13:35:25 -04:00
parent 0dce46bcab
commit 30999726e5
14 changed files with 463 additions and 911 deletions
@@ -114,10 +114,48 @@ else
<HeaderContent>
<MudTh Style="width: 1%; white-space: nowrap;">#</MudTh>
<MudTh>Track Name</MudTh>
@* Per-track waveform-datum status + generate (migrated from the retired
CmsTrackGrid). The expanded child row is the releases view's only
per-track surface, so the unique per-track Profile / High-res columns
live here. *@
<MudTh Style="width: 1%; white-space: nowrap;">Profile</MudTh>
<MudTh Style="width: 1%; white-space: nowrap;">High-res</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="#">@track.TrackNumber</MudTd>
<MudTd DataLabel="Track Name">@track.TrackName</MudTd>
<MudTd DataLabel="Profile">
@if (HasProfile(track.EntryKey))
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
}
else
{
<MudTooltip Text="Generate profile">
<MudIconButton Icon="@Icons.Material.Filled.GraphicEq"
Size="Size.Small"
Color="Color.Secondary"
Disabled="@_generating.Contains(track.EntryKey)"
OnClick="@(() => GenerateProfileAsync(track))" />
</MudTooltip>
}
</MudTd>
<MudTd DataLabel="High-res">
@if (HasHighRes(track.EntryKey))
{
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
}
else
{
<MudTooltip Text="Generate high-res datum">
<MudIconButton Icon="@Icons.Material.Filled.Waves"
Size="Size.Small"
Color="Color.Secondary"
Disabled="@_generatingHighRes.Contains(track.EntryKey)"
OnClick="@(() => GenerateHighResAsync(track))" />
</MudTooltip>
}
</MudTd>
</RowTemplate>
</MudTable>
}
@@ -181,6 +219,101 @@ else
[ReleaseMedium.Mix] = "DJ Mix",
};
// EntryKey → HasProfile / HasHighRes for the expanded-row per-track waveform columns (migrated from
// the retired CmsTrackGrid). Loaded once per grid instance on first row expand; a per-row generate
// flips a single entry to true. Null until first loaded.
private Dictionary<string, bool>? _profileStatus;
private Dictionary<string, bool>? _highResStatus;
private readonly HashSet<string> _generating = new();
private readonly HashSet<string> _generatingHighRes = new();
private bool HasProfile(string entryKey) =>
_profileStatus is not null && _profileStatus.TryGetValue(entryKey, out var has) && has;
private bool HasHighRes(string entryKey) =>
_highResStatus is not null && _highResStatus.TryGetValue(entryKey, out var has) && has;
// Fetch the catalogue-wide waveform status once and cache it. The admin catalogue is small (one unpaged
// call covers it), and per-track status only matters for rows the admin actually expands.
private async Task EnsureWaveformStatusAsync()
{
if (_profileStatus is not null) return;
var result = await CmsTrackService.GetWaveformStatusAsync();
if (result.Success && result.Value is not null)
{
_profileStatus = result.Value.ToDictionary(s => s.EntryKey, s => s.HasProfile);
_highResStatus = result.Value.ToDictionary(s => s.EntryKey, s => s.HasHighRes);
}
else
{
// Leave both empty (not null) so we do not re-fetch on every expand after a transient failure;
// the next OnReleasesChanged refresh path will rebuild the grid and retry.
_profileStatus = new Dictionary<string, bool>();
_highResStatus = new Dictionary<string, bool>();
}
}
private async Task GenerateProfileAsync(TrackDto track)
{
_generating.Add(track.EntryKey);
StateHasChanged();
try
{
var result = await CmsTrackService.GenerateWaveformProfileAsync(track.EntryKey);
if (result.Success)
{
(_profileStatus ??= new())[track.EntryKey] = true;
Snackbar.Add($"Generated profile for '{track.TrackName}'.", Severity.Success);
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"Generate failed for '{track.TrackName}': {error}", Severity.Error);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Waveform generation failed for {EntryKey}", track.EntryKey);
Snackbar.Add($"Generate failed for '{track.TrackName}' — please try again.", Severity.Error);
}
finally
{
_generating.Remove(track.EntryKey);
StateHasChanged();
}
}
private async Task GenerateHighResAsync(TrackDto track)
{
_generatingHighRes.Add(track.EntryKey);
StateHasChanged();
try
{
var result = await CmsTrackService.GenerateHighResWaveformAsync(track.EntryKey);
if (result.Success)
{
(_highResStatus ??= new())[track.EntryKey] = true;
Snackbar.Add($"Generated high-res datum for '{track.TrackName}'.", Severity.Success);
}
else
{
var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error";
Snackbar.Add($"High-res generate failed for '{track.TrackName}': {error}", Severity.Error);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "High-res waveform generation failed for {EntryKey}", track.EntryKey);
Snackbar.Add($"High-res generate failed for '{track.TrackName}' — please try again.", Severity.Error);
}
finally
{
_generatingHighRes.Remove(track.EntryKey);
StateHasChanged();
}
}
private async Task ToggleExpand(AlbumRow row)
{
row.IsExpanded = !row.IsExpanded;
@@ -189,6 +322,9 @@ else
row.IsLoading = true;
StateHasChanged();
row.Tracks = await LoadTracksAsync(row.Release.Title);
// The per-track Profile / High-res columns need waveform status for the rows just loaded.
// Loaded once for the catalogue on first expand and cached for this grid instance.
await EnsureWaveformStatusAsync();
row.IsLoading = false;
}
}