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
+48 -70
View File
@@ -1,9 +1,10 @@
@page "/catalogue"
@using DeepDrftManager.Services
@using DeepDrftModels.Enums
@attribute [Authorize]
@layout Layout.CmsLayout
@inject NavigationManager Nav
@inject ICmsTrackService CmsTrackService
@inject ICmsReleaseService CmsReleaseService
@inject ILogger<Index> Logger
<PageTitle>DeepDrft CMS</PageTitle>
@@ -12,114 +13,91 @@
<MudText Typo="Typo.h3" Class="mb-6">Catalogue</MudText>
<MudGrid Spacing="4">
<MudItem xs="12" sm="4">
@SummaryCard("Tracks", Icons.Material.Filled.LibraryMusic, Color.Primary, _tracksLoading, _trackCount)
</MudItem>
<MudItem xs="12" sm="4">
@SummaryCard("Releases", Icons.Material.Filled.Album, Color.Secondary, _albumsLoading, _albumCount)
</MudItem>
<MudItem xs="12" sm="4">
@SummaryCard("Genres", Icons.Material.Filled.Category, Color.Tertiary, _genresLoading, _genreCount)
</MudItem>
@foreach (var card in Cards)
{
<MudItem xs="12" sm="4">
@SummaryCard(card)
</MudItem>
}
</MudGrid>
</MudContainer>
@code {
private bool _tracksLoading = true;
private bool _albumsLoading = true;
private bool _genresLoading = true;
// One card per release medium. Each deep-links to /releases with the medium tab pre-selected via the
// same ?medium= convention the Add Track buttons use. The count is that medium's release total.
private sealed record MediumCard(ReleaseMedium Medium, string Label, string Icon, Color Color);
private int? _trackCount;
private int? _albumCount;
private int? _genreCount;
private static readonly IReadOnlyList<MediumCard> Cards = new[]
{
new MediumCard(ReleaseMedium.Cut, "CUTS", Icons.Material.Filled.Album, Color.Primary),
new MediumCard(ReleaseMedium.Session, "SESSIONS", Icons.Material.Filled.Mic, Color.Secondary),
new MediumCard(ReleaseMedium.Mix, "MIXES", Icons.Material.Filled.GraphicEq, Color.Tertiary),
};
// Medium → release count (null while loading or on failure). Each medium's count is one cheap paged
// read (pageSize 1) for its TotalCount, run concurrently.
private readonly Dictionary<ReleaseMedium, int?> _counts = new();
private readonly HashSet<ReleaseMedium> _loading = Cards.Select(c => c.Medium).ToHashSet();
protected override async Task OnInitializedAsync()
{
// Three independent reads run concurrently. Each loader calls StateHasChanged in its
// finally block so its card updates as soon as its own fetch returns.
await Task.WhenAll(LoadTrackCount(), LoadAlbumCount(), LoadGenreCount());
// Each loader calls StateHasChanged in its finally block so its card updates as soon as its own
// fetch returns, rather than blocking on the slowest of the three.
await Task.WhenAll(Cards.Select(c => LoadCountAsync(c.Medium)));
}
private async Task LoadTrackCount()
private async Task LoadCountAsync(ReleaseMedium medium)
{
try
{
var result = await CmsTrackService.GetTrackCountAsync();
_trackCount = result.Success ? result.Value : null;
// pageSize 1 — we only need TotalCount, not the rows. Sort column is required by the API but
// immaterial to the count.
var result = await CmsReleaseService.GetPagedAsync(
medium, page: 1, pageSize: 1, sortColumn: "Title", sortDescending: false);
_counts[medium] = result.Success && result.Value is not null ? result.Value.TotalCount : null;
if (!result.Success)
{
Logger.LogWarning("Dashboard track count failed: {Error}",
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
Logger.LogWarning("Dashboard {Medium} count failed: {Error}",
medium, result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
}
}
finally
{
_tracksLoading = false;
_loading.Remove(medium);
StateHasChanged();
}
}
private async Task LoadAlbumCount()
{
try
{
var result = await CmsTrackService.GetReleasesAsync();
_albumCount = result.Success && result.Value is not null ? result.Value.Count : null;
if (!result.Success)
{
Logger.LogWarning("Dashboard album summaries failed: {Error}",
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
}
}
finally
{
_albumsLoading = false;
StateHasChanged();
}
}
private async Task LoadGenreCount()
{
try
{
var result = await CmsTrackService.GetGenreSummariesAsync();
_genreCount = result.Success && result.Value is not null ? result.Value.Count : null;
if (!result.Success)
{
Logger.LogWarning("Dashboard genre summaries failed: {Error}",
result.Messages.FirstOrDefault()?.Message ?? "Unknown error");
}
}
finally
{
_genresLoading = false;
StateHasChanged();
}
}
private RenderFragment SummaryCard(string label, string icon, Color color, bool loading, int? count) => __builder =>
private RenderFragment SummaryCard(MediumCard card) => __builder =>
{
var loading = _loading.Contains(card.Medium);
var count = _counts.GetValueOrDefault(card.Medium);
<MudCard Elevation="8" Style="height: 100%;">
<MudCardContent>
<MudStack AlignItems="AlignItems.Center" Spacing="2" Class="py-4">
<MudIcon Icon="@icon" Color="@color" Size="Size.Large" />
<MudIcon Icon="@card.Icon" Color="@card.Color" Size="Size.Large" />
@if (loading)
{
<MudProgressCircular Color="@color" Indeterminate="true" Size="Size.Small" />
<MudProgressCircular Color="@card.Color" Indeterminate="true" Size="Size.Small" />
}
else
{
<MudText Typo="Typo.h3" Color="@color">@(count?.ToString() ?? "—")</MudText>
<MudText Typo="Typo.h3" Color="@card.Color">@(count?.ToString() ?? "—")</MudText>
}
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary text-uppercase">@label</MudText>
<MudText Typo="Typo.subtitle1" Class="mud-text-secondary text-uppercase">@card.Label</MudText>
</MudStack>
</MudCardContent>
<MudCardActions Class="justify-center pb-4">
<MudButton Variant="Variant.Text" Color="@color" EndIcon="@Icons.Material.Filled.ArrowForward"
OnClick="@(() => Nav.NavigateTo("/tracks"))">
<MudButton Variant="Variant.Text" Color="@card.Color" EndIcon="@Icons.Material.Filled.ArrowForward"
OnClick="@(() => Nav.NavigateTo(ReleasesHref(card.Medium)))">
View
</MudButton>
</MudCardActions>
</MudCard>
};
// Deep-link to the Releases page with this medium's tab pre-selected. Mirrors the ?medium= seed the
// Add Track buttons use; the Releases page reads it to set the active tab.
private static string ReleasesHref(ReleaseMedium medium) =>
$"/releases?medium={medium.ToString().ToLowerInvariant()}";
}