using DeepDrftManager.Services; using DeepDrftModels.DTOs; using DeepDrftModels.Enums; using Microsoft.AspNetCore.Components; using MudBlazor; namespace DeepDrftManager.Components.Pages.Tracks; /// /// Shared fetch + state logic for the per-medium browsers (Cuts, Sessions, Mixes). Each subclass feeds /// the rich CmsAlbumBrowser grid a medium-filtered release list, so the per-medium tabs gain the /// same expand-tracks / delete / Type-chip / edit behaviour as the ALL tab without re-implementing any of /// it (§8.C parity — reuse, don't fork). This base owns the loading flag, the medium-filtered load, the /// per-release row projection, and a cover-thumbnail helper; subclasses supply the , /// an error noun, and their bespoke per-row action (Session hero upload, Mix waveform generate) via the /// rich grid's RowActions slot, looking their action-state row up with . /// /// The subclass's row model wrapping a plus its /// medium-specific action state (upload/generate flags). The rich grid renders from the bare /// projection; only carries the action state. public abstract class CmsMediumBrowserBase : ComponentBase where TRow : class { [Inject] public required ICmsReleaseService CmsReleaseService { get; set; } [Inject] public required ISnackbar Snackbar { get; set; } /// The medium this browser lists. Subclass-supplied constant. protected abstract ReleaseMedium Medium { get; } /// Plural noun for this medium used in error text (e.g. "sessions", "mixes"). protected abstract string MediumNoun { get; } /// Projects a fetched release into the subclass's row model. protected abstract TRow ToRow(ReleaseDto release); /// The release carried by a subclass row, for keying the action-state lookup. protected abstract ReleaseDto ReleaseOf(TRow row); protected List Rows { get; private set; } = new(); protected bool Loading { get; private set; } = true; // Bare release projection handed to the rich grid. The grid does the expand/delete/edit/Type-chip; // it never sees TRow. Rebuilt on every (re)load so the grid re-projects against a fresh reference. protected IReadOnlyList Releases { get; private set; } = Array.Empty(); // release.Id → action-state row, so a RowActions fragment (which the grid hands a ReleaseDto) can // recover its TRow. Rebuilt alongside Rows so a refresh never leaves a stale row behind. private Dictionary _rowsById = new(); protected override async Task OnInitializedAsync() => await LoadAsync(); /// Recovers the action-state row for a release the rich grid is rendering. Null if the /// release is not in the current page (e.g. just deleted), in which case the action is skipped. protected TRow? RowFor(ReleaseDto release) => _rowsById.TryGetValue(release.Id, out var row) ? row : null; /// /// Reloads the medium-filtered release list. Wired to the rich grid's OnReleasesChanged so a /// delete re-fetches the authoritative list (track counts, orphan cleanup) — the same single-load /// posture CmsAllReleasesGrid uses for the ALL tab. /// protected async Task ReloadAsync() { await LoadAsync(); StateHasChanged(); } private async Task LoadAsync() { Loading = true; // Single-track releases; a single generous page covers the CMS catalogue (same small-catalogue // assumption the album browser makes). var result = await CmsReleaseService.GetPagedAsync( Medium, page: 1, pageSize: 100, sortColumn: "Title", sortDescending: false); if (result.Success && result.Value is not null) { Rows = result.Value.Items.Select(ToRow).ToList(); } else { var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; Snackbar.Add($"Failed to load {MediumNoun}: {error}", Severity.Error); Rows = new List(); } Releases = Rows.Select(ReleaseOf).ToList(); _rowsById = Rows.ToDictionary(r => ReleaseOf(r).Id); Loading = false; } // Relative path — resolves against the Manager's own origin, proxied by ImageProxyController. protected static string ThumbUrl(string entryKey) => $"/api/image/{Uri.EscapeDataString(entryKey)}"; }