20084ace4f
Replace the navigate-away ReleaseArchiveBrowser cards and the redundant top-level Releases toggle with an in-page MudTabs strip under the Releases mode: ALL (CmsAllReleasesGrid) plus one enum-driven tab per ReleaseMedium. Sessions/Mixes browsers gain an Embedded flag that suppresses standalone page chrome when hosted as tab content; CmsCutBrowser is the new Cut-filtered grid. /tracks/sessions, /tracks/mixes, /tracks/archive stay reachable by URL.
223 lines
8.7 KiB
Plaintext
223 lines
8.7 KiB
Plaintext
@page "/tracks"
|
|
@page "/tracks/albums"
|
|
@page "/tracks/genres"
|
|
@page "/tracks/archive"
|
|
@using DeepDrftManager.Services
|
|
@using DeepDrftModels.Enums
|
|
@inject CmsTrackBrowserViewModel VM
|
|
@inject ICmsTrackService CmsTrackService
|
|
@inject ISnackbar Snackbar
|
|
@inject ILogger<TrackList> Logger
|
|
@inject NavigationManager NavigationManager
|
|
@attribute [Authorize]
|
|
|
|
<PageTitle>Tracks — DeepDrft CMS</PageTitle>
|
|
|
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
|
|
<MudText Typo="Typo.h3">Tracks</MudText>
|
|
|
|
@if (VM.Mode == BrowseMode.Tracks)
|
|
{
|
|
<MudButton Variant="Variant.Outlined"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.AutoFixHigh"
|
|
Disabled="@(_bulkRunning || (_grid?.GetMissingCount() ?? 0) == 0)"
|
|
OnClick="GenerateAllMissingAsync">
|
|
@if (_bulkRunning)
|
|
{
|
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
|
<span>Generating @_bulkDone / @_bulkTotal…</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Generate All Missing (@(_grid?.GetMissingCount() ?? 0))</span>
|
|
}
|
|
</MudButton>
|
|
}
|
|
</MudStack>
|
|
|
|
@* Top-level browse dimension. The former three-way toggle (Tracks / Releases / Release Archive)
|
|
collapsed to two (§8.A): "Releases" now hosts the in-page medium tab strip below, subsuming both
|
|
the old Releases grid (as the ALL tab) and the retired Release Archive cards. *@
|
|
<MudToggleGroup T="BrowseMode"
|
|
Value="VM.Mode"
|
|
ValueChanged="OnModeChanged"
|
|
SelectionMode="SelectionMode.SingleSelection"
|
|
Color="Color.Primary"
|
|
Size="Size.Small"
|
|
Class="mb-4">
|
|
<MudToggleItem Value="BrowseMode.Tracks">Tracks</MudToggleItem>
|
|
<MudToggleItem Value="BrowseMode.Albums">Releases</MudToggleItem>
|
|
</MudToggleGroup>
|
|
|
|
@if (VM.Mode == BrowseMode.Tracks)
|
|
{
|
|
<CmsTrackGrid @ref="_grid" ShowAddButton="true" PageSize="20" OnStatusLoaded="StateHasChanged" />
|
|
}
|
|
else if (VM.Mode == BrowseMode.Albums)
|
|
{
|
|
@* The Release Archive tab strip (§8.A): an ALL tab plus one tab per ReleaseMedium, ALL left-most.
|
|
The medium tabs are enum-driven — a fourth medium adds a tab automatically; only a label-lookup
|
|
entry (MediumTabLabels) and a content arm (MediumGrid) are needed, no markup fork. Selecting a
|
|
tab swaps the grid below in place; no navigation to a separate page occurs. *@
|
|
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
|
|
<MudTabPanel Text="ALL">
|
|
<CmsAllReleasesGrid OnReleasesChanged="OnAlbumsChanged" />
|
|
</MudTabPanel>
|
|
@foreach (var medium in Enum.GetValues<ReleaseMedium>())
|
|
{
|
|
<MudTabPanel Text="@MediumTabLabels[medium]">
|
|
@MediumGrid(medium)
|
|
</MudTabPanel>
|
|
}
|
|
</MudTabs>
|
|
}
|
|
else
|
|
{
|
|
@* Genre browse keeps its route (/tracks/genres) but lost its tab to Release Archive (§3.1).
|
|
Reachable by direct URL; no longer in the toggle group. *@
|
|
<CmsGenreBrowser Genres="VM.Genres"
|
|
IsLoading="VM.GenresLoading"
|
|
ExpandedGenre="@VM.ExpandedGenre"
|
|
OnExpandedGenreChanged="OnExpandedGenreChanged" />
|
|
}
|
|
</MudContainer>
|
|
|
|
@code {
|
|
private CmsTrackGrid? _grid;
|
|
|
|
// Medium → tab label. The one place medium display text lives for the tab strip; a future medium adds
|
|
// one entry here and surfaces a tab automatically. Mirrors the extension discipline the retired
|
|
// ReleaseArchiveBrowser used for its cards. The ALL tab is rendered separately (it is not a medium).
|
|
private static readonly IReadOnlyDictionary<ReleaseMedium, string> MediumTabLabels =
|
|
new Dictionary<ReleaseMedium, string>
|
|
{
|
|
[ReleaseMedium.Cut] = "CUTS",
|
|
[ReleaseMedium.Session] = "SESSIONS",
|
|
[ReleaseMedium.Mix] = "MIXES",
|
|
};
|
|
|
|
// Medium → embedded grid. Each medium's grid is its own component (Cut has no per-row action; Session
|
|
// carries hero upload; Mix carries waveform generation), so the content dispatch is a per-medium
|
|
// mapping by nature — but it is a single switch returning a fragment, not a markup fork. The browsers
|
|
// render Embedded so their standalone page chrome (container, title, back button) is suppressed here.
|
|
private RenderFragment MediumGrid(ReleaseMedium medium) => medium switch
|
|
{
|
|
ReleaseMedium.Cut => @<CmsCutBrowser />,
|
|
ReleaseMedium.Session => @<CmsSessionBrowser Embedded="true" />,
|
|
ReleaseMedium.Mix => @<CmsMixBrowser Embedded="true" />,
|
|
_ => @<MudText Typo="Typo.body1" Class="mt-4">No grid for this medium.</MudText>
|
|
};
|
|
|
|
// The all-releases grid refreshes its own list after a delete; this notification lets us invalidate
|
|
// the VM's genre cache so genre counts reflect the deletion on the next switch into Genre mode.
|
|
private void OnAlbumsChanged()
|
|
{
|
|
VM.Invalidate();
|
|
StateHasChanged();
|
|
}
|
|
|
|
// Local state for the parent-owned "Generate All Missing" bulk run.
|
|
private bool _bulkRunning;
|
|
private int _bulkTotal;
|
|
private int _bulkDone;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
// /tracks/archive and /tracks/albums both land on the Releases view (the tab strip); the old
|
|
// separate Archive mode is retired (§8.A) but the route stays reachable rather than 404ing.
|
|
var uri = NavigationManager.Uri;
|
|
var initial =
|
|
uri.Contains("/tracks/albums", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Albums
|
|
: uri.Contains("/tracks/archive", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Albums
|
|
: uri.Contains("/tracks/genres", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Genres
|
|
: BrowseMode.Tracks;
|
|
await VM.SwitchModeAsync(initial);
|
|
}
|
|
|
|
private async Task OnModeChanged(BrowseMode mode)
|
|
{
|
|
await VM.SwitchModeAsync(mode);
|
|
var path = mode switch
|
|
{
|
|
BrowseMode.Albums => "/tracks/albums",
|
|
BrowseMode.Genres => "/tracks/genres",
|
|
_ => "/tracks"
|
|
};
|
|
NavigationManager.NavigateTo(path, replace: true);
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void OnExpandedGenreChanged(string? genre)
|
|
{
|
|
VM.SetExpandedGenre(genre);
|
|
StateHasChanged();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Backfill every track missing a waveform profile, one request at a time so a large backfill
|
|
/// does not flood the API with concurrent WAV decodes. On completion, refreshes the grid's
|
|
/// status map so the per-row icons reflect the new state.
|
|
/// </summary>
|
|
private async Task GenerateAllMissingAsync()
|
|
{
|
|
var statusResult = await CmsTrackService.GetWaveformStatusAsync();
|
|
if (!statusResult.Success || statusResult.Value is null)
|
|
{
|
|
var error = statusResult.Messages.FirstOrDefault()?.Message ?? "Unknown error";
|
|
Snackbar.Add($"Failed to load waveform status: {error}", Severity.Error);
|
|
return;
|
|
}
|
|
|
|
var missing = statusResult.Value.Where(s => !s.HasProfile).ToList();
|
|
if (missing.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_bulkRunning = true;
|
|
_bulkTotal = missing.Count;
|
|
_bulkDone = 0;
|
|
_grid?.SetBulkRunning(true);
|
|
var failures = 0;
|
|
|
|
foreach (var status in missing)
|
|
{
|
|
try
|
|
{
|
|
var result = await CmsTrackService.GenerateWaveformProfileAsync(status.EntryKey);
|
|
if (!result.Success)
|
|
{
|
|
failures++;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Waveform generation failed for {EntryKey}", status.EntryKey);
|
|
failures++;
|
|
}
|
|
_bulkDone++;
|
|
StateHasChanged();
|
|
}
|
|
|
|
_bulkRunning = false;
|
|
_grid?.SetBulkRunning(false);
|
|
|
|
if (_grid is not null)
|
|
{
|
|
await _grid.RefreshWaveformStatusAsync();
|
|
}
|
|
|
|
var succeeded = missing.Count - failures;
|
|
if (failures == 0)
|
|
{
|
|
Snackbar.Add($"Generated {succeeded} profile(s).", Severity.Success);
|
|
}
|
|
else
|
|
{
|
|
Snackbar.Add($"Generated {succeeded} profile(s); {failures} failed.", Severity.Warning);
|
|
}
|
|
}
|
|
}
|