diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor index a6b018b..c80127b 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsAlbumBrowser.razor @@ -33,6 +33,10 @@ else Release Date Type Tracks + @foreach (var column in SpecialColumns) + { + @column.Header + } Actions @@ -64,11 +68,14 @@ else @context.TrackCount + @foreach (var column in SpecialColumns) + { + @* One dedicated cell per host-declared special-action column (Mix waveform, Session hero). + The Cell fragment recovers its typed row state via the host's RowFor lookup. Sits between + Tracks and Actions so the universal Edit/Delete stay rightmost. *@ + @column.Cell(context.Release) + } - @* Medium-specific row action (Session hero, Mix waveform) when a host supplies one; - the ALL tab supplies none. Rendered before the shared edit/delete so the medium - affordance reads left-to-right ahead of the universal actions. *@ - @RowActions?.Invoke(context.Release) - + @if (context.IsLoading) { @@ -126,12 +133,19 @@ else [Parameter] public bool IsLoading { get; set; } [Parameter] public EventCallback OnReleasesChanged { get; set; } - // Optional per-row, medium-specific action slot (Session hero upload, Mix waveform generate), - // rendered in the Actions cell ahead of the shared edit/delete buttons. The ALL tab leaves it - // unset and renders the grid exactly as before. A per-medium host (CmsCut/Session/MixBrowser) - // supplies it so the rich grid filtered to one medium keeps that medium's bespoke affordance — - // the rich expand/delete/Type-chip/edit logic stays here, single-sourced, rather than forked. - [Parameter] public RenderFragment? RowActions { get; set; } + // Zero or more dedicated, header-labelled special-action columns (Session hero upload, Mix waveform + // generate), each rendered as its own header cell + per-row cell between the Tracks and Actions + // columns. The ALL and Cut tabs leave this empty and render exactly as before — only the standard + // columns plus Edit/Delete. A per-medium host supplies its bespoke affordances here so the rich + // expand/delete/Type-chip/edit logic stays single-sourced in this grid rather than forked. + [Parameter] public IReadOnlyList SpecialColumns { get; set; } = Array.Empty(); + + // Base columns: expand, Art, Album, Artist, Genre, Release Date, Type, Tracks, Actions = 9. + private const int BaseColumnCount = 9; + + // Total rendered columns, driving the expanded child-row colspan so it always spans the full table + // regardless of how many special-action columns the host declared. + private int ColumnCount => BaseColumnCount + SpecialColumns.Count; private List _rows = new(); diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor index 4e6ced7..b2c23db 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsCutBrowser.razor @@ -4,8 +4,8 @@ @* CUTS tab content (Phase 9 §8.A/§8.C): the rich CmsAlbumBrowser grid filtered to Cut releases, so the tab carries expand-tracks, delete, the Type chip, and per-row edit identically to the ALL tab — no - forked grid. Cuts have no medium-specific row action, so no RowActions slot is supplied; the grid - renders its shared edit/delete only. Embedded as tab content only; no standalone @page route. *@ + forked grid. Cuts have no medium-specific action, so no SpecialColumns are supplied; the grid renders + its shared edit/delete only. Embedded as tab content only; no standalone @page route. *@ diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs b/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs index 72b15b0..0d93a73 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs @@ -13,7 +13,7 @@ namespace DeepDrftManager.Components.Pages.Tracks; /// 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 . +/// rich grid's SpecialColumns column model, 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 @@ -42,8 +42,8 @@ public abstract class CmsMediumBrowserBase : ComponentBase where TRow : cl // 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. + // release.Id → action-state row, so a SpecialColumns cell delegate (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(); diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor index 3faacf9..d291411 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor @@ -8,9 +8,10 @@ @* Embedded as the MIXES tab content of the Release Archive (Phase 9 §8.A), and still routable at /tracks/mixes for direct-URL access. The grid is the rich CmsAlbumBrowser filtered to Mixes (§8.C parity: expand-tracks, delete, Type chip, per-row edit), with the Mix waveform generate supplied as - its medium-specific RowActions slot so that affordance survives the move off the thin table. When - embedded, the page chrome (title, container, the now-meaningless "Back to Release Archive" button) is - suppressed; the standalone route keeps it. The waveform affordance (9.5.E) is preserved in both. *@ + its medium-specific special-action column so that affordance survives the move off the thin table. + When embedded, the page chrome (title, container, the now-meaningless "Back to Release Archive" + button) is suppressed; the standalone route keeps it. The waveform affordance (9.5.E) is preserved + in both. *@ @if (Embedded) { @GridContent @@ -52,46 +53,59 @@ else protected override ReleaseDto ReleaseOf(MixRow row) => row.Release; // The grid itself — identical in the embedded and standalone contexts. Defined once as a fragment so - // both branches above render the same markup without duplication. The waveform generate is the Mix's - // medium-specific RowActions content; the grid hands it each release, and RowFor recovers the - // matching MixRow's generate state. + // both branches above render the same markup without duplication. The Mix declares one dedicated + // "Waveform" special-action column; the grid renders it between Tracks and Actions, handing the cell + // each release, and RowFor recovers the matching MixRow's generate state. private RenderFragment GridContent => @ - - @{ var row = RowFor(release); } - @if (row is not null) + OnReleasesChanged="ReloadAsync" + SpecialColumns="_specialColumns" />; + + // Allocated once per component instance in OnInitialized (field initializers cannot reference + // instance members, so initialization is deferred to the first lifecycle hook). + private IReadOnlyList _specialColumns = Array.Empty(); + + protected override void OnInitialized() + { + _specialColumns = new[] { new SpecialActionColumn("Waveform", WaveformCell) }; + base.OnInitialized(); + } + + // Per-row cell for the dedicated "Waveform" column: status icon plus generate/regenerate button with + // progress. Recovers the typed MixRow via RowFor; skips rendering for a release not on the page. + private RenderFragment WaveformCell => release =>@ + @{ var row = RowFor(release); } + @if (row is not null) + { + @if (row.HasWaveform) { - @if (row.HasWaveform) + + + + } + else + { + + + + } + + @if (row.IsGenerating) { - - - + + Generating… } else { - - - + @(row.HasWaveform ? "Regenerate" : "Generate") } - - @if (row.IsGenerating) - { - - Generating… - } - else - { - @(row.HasWaveform ? "Regenerate" : "Generate") - } - - } - - ; + + } + ; private async Task GenerateWaveformAsync(MixRow row) { diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor index 927a3fe..7e56266 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor @@ -9,9 +9,10 @@ @* Embedded as the SESSIONS tab content of the Release Archive (Phase 9 §8.A), and still routable at /tracks/sessions for direct-URL access. The grid is the rich CmsAlbumBrowser filtered to Sessions (§8.C parity: expand-tracks, delete, Type chip, per-row edit), with the Session hero upload supplied - as its medium-specific RowActions slot so that affordance survives the move off the thin table. When - embedded, the page chrome (title, container, the now-meaningless "Back to Release Archive" button) is - suppressed; the standalone route keeps it. The hero affordance (9.5.E) is preserved in both contexts. *@ + as its medium-specific special-action column so that affordance survives the move off the thin table. + When embedded, the page chrome (title, container, the now-meaningless "Back to Release Archive" + button) is suppressed; the standalone route keeps it. The hero affordance (9.5.E) is preserved in + both contexts. *@ @if (Embedded) { @GridContent @@ -53,48 +54,61 @@ else protected override ReleaseDto ReleaseOf(SessionRow row) => row.Release; // The grid itself — identical in the embedded and standalone contexts. Defined once as a fragment so - // both branches above render the same markup without duplication. The hero upload is the Session's - // medium-specific RowActions content; the grid hands it each release, and RowFor recovers the - // matching SessionRow's upload state. + // both branches above render the same markup without duplication. The Session declares one dedicated + // "Hero" special-action column; the grid renders it between Tracks and Actions, handing the cell each + // release, and RowFor recovers the matching SessionRow's upload state. private RenderFragment GridContent => @ - - @{ var row = RowFor(release); } - @if (row is not null) + OnReleasesChanged="ReloadAsync" + SpecialColumns="_specialColumns" />; + + // Allocated once per component instance in OnInitialized (field initializers cannot reference + // instance members, so initialization is deferred to the first lifecycle hook). + private IReadOnlyList _specialColumns = Array.Empty(); + + protected override void OnInitialized() + { + _specialColumns = new[] { new SpecialActionColumn("Hero", HeroCell) }; + base.OnInitialized(); + } + + // Per-row cell for the dedicated "Hero" column: thumbnail preview plus set/replace upload button with + // progress. Recovers the typed SessionRow via RowFor; skips rendering for a release not on the page. + private RenderFragment HeroCell => release =>@ + @{ var row = RowFor(release); } + @if (row is not null) + { + @if (row.HeroImageEntryKey is { Length: > 0 } heroKey) { - @if (row.HeroImageEntryKey is { Length: > 0 } heroKey) - { -
- } - else - { -
- } - - - - @if (row.IsUploading) - { - - Uploading… - } - else - { - @(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero") - } - - - +
} -
-
; + else + { +
+ } + + + + @if (row.IsUploading) + { + + Uploading… + } + else + { + @(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero") + } + + + + } + ; private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file) { diff --git a/DeepDrftManager/Components/Pages/Tracks/SpecialActionColumn.cs b/DeepDrftManager/Components/Pages/Tracks/SpecialActionColumn.cs new file mode 100644 index 0000000..689f67c --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/SpecialActionColumn.cs @@ -0,0 +1,15 @@ +using DeepDrftModels.DTOs; +using Microsoft.AspNetCore.Components; + +namespace DeepDrftManager.Components.Pages.Tracks; + +/// +/// A dedicated, header-labelled grid column for a medium-specific row affordance (e.g. Mix waveform +/// generate, Session hero upload) in . A per-medium host declares zero or +/// more of these; the grid renders one extra header cell and one extra per-row cell for each, positioned +/// between the Tracks column and the universal Actions (Edit/Delete) column. The +/// fragment is handed each release; the host recovers its typed row state via its own RowFor lookup. +/// +/// Column header label (e.g. "Waveform", "Hero"). +/// Per-row cell content for a given release. +public sealed record SpecialActionColumn(string Header, RenderFragment Cell);