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/CmsMixBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor index 3faacf9..e2d89a6 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor @@ -52,46 +52,54 @@ 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" />; + + private IReadOnlyList _specialColumns => new[] + { + new SpecialActionColumn("Waveform", WaveformCell) + }; + + // 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..11e2d8d 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor @@ -53,48 +53,56 @@ 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" />; + + private IReadOnlyList _specialColumns => new[] + { + new SpecialActionColumn("Hero", HeroCell) + }; + + // 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);