@page "/tracks" @page "/tracks/albums" @page "/tracks/genres" @page "/tracks/archive" @using DeepDrftManager.Services @inject CmsTrackBrowserViewModel VM @inject ICmsTrackService CmsTrackService @inject ISnackbar Snackbar @inject ILogger Logger @inject NavigationManager NavigationManager @attribute [Authorize] Tracks — DeepDrft CMS Tracks @if (VM.Mode == BrowseMode.Tracks) { @if (_bulkRunning) { Generating @_bulkDone / @_bulkTotal… } else { Generate All Missing (@(_grid?.GetMissingCount() ?? 0)) } } Tracks Releases Release Archive @if (VM.Mode == BrowseMode.Tracks) { } else if (VM.Mode == BrowseMode.Albums) { @* The all-releases grid is now a self-contained component (Phase 9 §8.B): it owns its own load and refresh, so the host renders it with no parameters. The 8.A tab strip hosts this same component as its ALL tab. Genre mode still uses the VM cache below; only album loading moved into the component, so VM.Albums / VM.AlbumsLoading are no longer read here. *@ } else if (VM.Mode == BrowseMode.Archive) { } 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. *@ } @code { private CmsTrackGrid? _grid; // 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() { var uri = NavigationManager.Uri; var initial = uri.Contains("/tracks/albums", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Albums : uri.Contains("/tracks/archive", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Archive : 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.Archive => "/tracks/archive", BrowseMode.Genres => "/tracks/genres", _ => "/tracks" }; NavigationManager.NavigateTo(path, replace: true); StateHasChanged(); } private void OnExpandedGenreChanged(string? genre) { VM.SetExpandedGenre(genre); StateHasChanged(); } /// /// 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. /// 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); } } }