From a7e2335c20c1ae10767b872a7ed008b868758e52 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 13 Jun 2026 11:08:43 -0400 Subject: [PATCH] Add Edit action to medium browsers; extract CmsMediumBrowserBase + CmsMediumTable Session/Mix browsers share base (load/state/thumb) and a shared table shell carrying the per-row Edit link to BatchEdit; subclasses supply only their medium action. --- .../Pages/Tracks/CmsMediumBrowserBase.cs | 62 +++++++ .../Pages/Tracks/CmsMediumTable.razor | 74 +++++++++ .../Pages/Tracks/CmsMediumTable.razor.css | 14 ++ .../Pages/Tracks/CmsMixBrowser.razor | 147 ++++++----------- .../Pages/Tracks/CmsMixBrowser.razor.css | 15 -- .../Pages/Tracks/CmsSessionBrowser.razor | 153 ++++++------------ .../Pages/Tracks/CmsSessionBrowser.razor.css | 6 +- 7 files changed, 245 insertions(+), 226 deletions(-) create mode 100644 DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs create mode 100644 DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor create mode 100644 DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor.css delete mode 100644 DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor.css diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs b/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs new file mode 100644 index 0000000..bd77849 --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMediumBrowserBase.cs @@ -0,0 +1,62 @@ +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 single-track medium browsers (Sessions, Mixes). Analogous to the +/// public-site MediumBrowseBase: subclasses supply the , the noun used in +/// error text, and a per-row projection from to their own row model; this base +/// owns the loading flag, the row list, the initial load, and the cover-thumbnail URL helper. The shared +/// table structure lives in CmsMediumTable; subclasses render it and fill only the action column. +/// +/// The subclass's row model wrapping a . +public abstract class CmsMediumBrowserBase : ComponentBase +{ + [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); + + protected List Rows { get; private set; } = new(); + protected bool Loading { get; private set; } = true; + + protected override async Task OnInitializedAsync() => await LoadAsync(); + + 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(); + } + + 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)}"; +} diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor b/DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor new file mode 100644 index 0000000..07e97e4 --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor @@ -0,0 +1,74 @@ +@namespace DeepDrftManager.Components.Pages.Tracks +@typeparam TRow +@using DeepDrftModels.DTOs + +@* Shared table shell for the single-track medium browsers (Sessions, Mixes). Renders the cover + thumbnail, title, artist, and a shared Edit affordance that every medium gets (9.5.E). The + medium-specific cells (hero / waveform / generate-or-upload action) are supplied per row via the + ActionContent slot, which receives the subclass's typed row. Fully controlled by the parent: + loading and row state are passed in. *@ + +@if (Loading) +{ + +} +else if (Rows.Count == 0) +{ + @EmptyMessage +} +else +{ + + + Cover + @TitleHeader + Artist + Actions + + + + @{ var release = ReleaseAccessor(context); } + @if (!string.IsNullOrEmpty(release.ImagePath)) + { +
+ } + else + { +
+ } +
+ @ReleaseAccessor(context).Title + @ReleaseAccessor(context).Artist + + + @ActionContent(context) + + + + + +
+
+} + +@code { + [Parameter] public required IReadOnlyList Rows { get; set; } + [Parameter] public bool Loading { get; set; } + + /// Projects a row to its underlying release for the cover/title/artist cells. + [Parameter] public required Func ReleaseAccessor { get; set; } + + /// Medium-specific cell content (hero / waveform / generate action) for each row. + [Parameter] public required RenderFragment ActionContent { get; set; } + + /// Relative thumbnail URL builder; the base class supplies its proxy-aware helper. + [Parameter] public required Func ThumbUrl { get; set; } + + /// Column header / data-label for the title column (e.g. "Session", "Mix"). + [Parameter] public string TitleHeader { get; set; } = "Title"; + + [Parameter] public string EmptyMessage { get; set; } = "Nothing here yet."; +} diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor.css b/DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor.css new file mode 100644 index 0000000..3b1ba83 --- /dev/null +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMediumTable.razor.css @@ -0,0 +1,14 @@ +/* Cover-thumbnail idiom shared by the medium browsers' tables. Blazor CSS isolation is per-component, + so this scoped copy of the album-browser thumb classes reaches only this component's own markup. */ +.cms-album-thumb { + width: 40px; + height: 40px; + border-radius: 4px; + background-size: cover; + background-position: center; + flex-shrink: 0; +} + +.cms-album-thumb--fallback { + background-color: var(--mud-palette-action-default-hover); +} diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor index dca76a2..e39be57 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor @@ -1,10 +1,8 @@ @page "/tracks/mixes" -@using DeepDrftManager.Services +@inherits CmsMediumBrowserBase @using DeepDrftModels.DTOs @using DeepDrftModels.Enums @attribute [Authorize] -@inject ICmsReleaseService CmsReleaseService -@inject ISnackbar Snackbar @inject ILogger Logger Mixes — DeepDrft CMS @@ -19,109 +17,54 @@ Mixes - @if (_loading) - { - - } - else if (_rows.Count == 0) - { - No mixes found. - } - else - { - - - Cover - Mix - Artist - Waveform - Actions - - - - @if (!string.IsNullOrEmpty(context.Release.ImagePath)) - { -
- } - else - { -
- } -
- @context.Release.Title - @context.Release.Artist - - @if (context.HasWaveform) - { - - - - } - else - { - - - - } - - - - @if (context.IsGenerating) - { - - Generating… - } - else - { - @(context.HasWaveform ? "Regenerate" : "Generate") - } - - -
-
- } + + + @if (row.HasWaveform) + { + + + + } + else + { + + + + } + + @if (row.IsGenerating) + { + + Generating… + } + else + { + @(row.HasWaveform ? "Regenerate" : "Generate") + } + + + @code { - private List _rows = new(); - private bool _loading = true; + protected override ReleaseMedium Medium => ReleaseMedium.Mix; + protected override string MediumNoun => "mixes"; - protected override async Task OnInitializedAsync() => await LoadAsync(); - - private async Task LoadAsync() + protected override MixRow ToRow(ReleaseDto release) => new() { - _loading = true; - // Mixes are single-track releases; a single generous page covers the CMS catalogue. - var result = await CmsReleaseService.GetPagedAsync( - ReleaseMedium.Mix, page: 1, pageSize: 100, - sortColumn: "Title", sortDescending: false); - - if (result.Success && result.Value is not null) - { - _rows = result.Value.Items - .Select(r => new MixRow - { - Release = r, - HasWaveform = !string.IsNullOrEmpty(r.MixMetadata?.WaveformEntryKey) - }) - .ToList(); - } - else - { - var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - Snackbar.Add($"Failed to load mixes: {error}", Severity.Error); - _rows = new List(); - } - _loading = false; - } - - // Relative path — resolves against the Manager's own origin, proxied by ImageProxyController. - private static string ThumbUrl(string entryKey) => - $"/api/image/{Uri.EscapeDataString(entryKey)}"; + Release = release, + HasWaveform = !string.IsNullOrEmpty(release.MixMetadata?.WaveformEntryKey) + }; private async Task GenerateWaveformAsync(MixRow row) { @@ -156,7 +99,7 @@ } } - private sealed class MixRow + public sealed class MixRow { public required ReleaseDto Release { get; set; } public bool HasWaveform { get; set; } diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor.css b/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor.css deleted file mode 100644 index dcb8151..0000000 --- a/DeepDrftManager/Components/Pages/Tracks/CmsMixBrowser.razor.css +++ /dev/null @@ -1,15 +0,0 @@ -/* Scoped duplicate of the album-browser thumb idiom. Blazor CSS isolation is per-component, so the - class defined in CmsAlbumBrowser.razor.css does not reach this component's markup — a small, - intentional duplication rather than promoting a two-rule block to global app.css. */ -.cms-album-thumb { - width: 40px; - height: 40px; - border-radius: 4px; - background-size: cover; - background-position: center; - flex-shrink: 0; -} - -.cms-album-thumb--fallback { - background-color: var(--mud-palette-action-default-hover); -} diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor index 5e93ccc..3740495 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor +++ b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor @@ -1,13 +1,10 @@ @page "/tracks/sessions" -@using DeepDrftManager.Services +@inherits CmsMediumBrowserBase @using DeepDrftModels.DTOs @using DeepDrftModels.Enums @using Microsoft.AspNetCore.Components.Forms @attribute [Authorize] -@inject ICmsReleaseService CmsReleaseService -@inject ISnackbar Snackbar @inject ILogger Logger -@inject NavigationManager Navigation Sessions — DeepDrft CMS @@ -21,112 +18,56 @@ Sessions - @if (_loading) - { - - } - else if (_rows.Count == 0) - { - No sessions found. - } - else - { - - - Cover - Hero - Session - Artist - Actions - - - - @if (!string.IsNullOrEmpty(context.Release.ImagePath)) - { -
- } - else - { -
- } -
- - @if (context.HeroImageEntryKey is { Length: > 0 } heroKey) - { -
- } - else - { -
- } -
- @context.Release.Title - @context.Release.Artist - - - - - @if (context.IsUploading) - { - - Uploading… - } - else - { - @(context.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero") - } - - - - -
-
- } + + + @if (row.HeroImageEntryKey is { Length: > 0 } heroKey) + { +
+ } + else + { +
+ } + + + + @if (row.IsUploading) + { + + Uploading… + } + else + { + @(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero") + } + + + +
+
@code { - private List _rows = new(); - private bool _loading = true; + protected override ReleaseMedium Medium => ReleaseMedium.Session; + protected override string MediumNoun => "sessions"; - protected override async Task OnInitializedAsync() => await LoadAsync(); - - private async Task LoadAsync() + protected override SessionRow ToRow(ReleaseDto release) => new() { - _loading = true; - // Sessions are single-track releases; a single generous page covers the CMS catalogue (same - // small-catalogue assumption the album browser makes). - var result = await CmsReleaseService.GetPagedAsync( - ReleaseMedium.Session, page: 1, pageSize: 100, - sortColumn: "Title", sortDescending: false); - - if (result.Success && result.Value is not null) - { - _rows = result.Value.Items - .Select(r => new SessionRow - { - Release = r, - HeroImageEntryKey = r.SessionMetadata?.HeroImageEntryKey - }) - .ToList(); - } - else - { - var error = result.Messages.FirstOrDefault()?.Message ?? "Unknown error"; - Snackbar.Add($"Failed to load sessions: {error}", Severity.Error); - _rows = new List(); - } - _loading = false; - } - - // Relative path — resolves against the Manager's own origin, proxied by ImageProxyController. - private static string ThumbUrl(string entryKey) => - $"/api/image/{Uri.EscapeDataString(entryKey)}"; + Release = release, + HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey + }; private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file) { @@ -169,7 +110,7 @@ } } - private sealed class SessionRow + public sealed class SessionRow { public required ReleaseDto Release { get; set; } public string? HeroImageEntryKey { get; set; } diff --git a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor.css b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor.css index dcb8151..ed71232 100644 --- a/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor.css +++ b/DeepDrftManager/Components/Pages/Tracks/CmsSessionBrowser.razor.css @@ -1,6 +1,6 @@ -/* Scoped duplicate of the album-browser thumb idiom. Blazor CSS isolation is per-component, so the - class defined in CmsAlbumBrowser.razor.css does not reach this component's markup — a small, - intentional duplication rather than promoting a two-rule block to global app.css. */ +/* Hero-thumbnail idiom for the session row's action cell. The cover thumb lives in CmsMediumTable's + own scoped CSS; this scoped copy reaches only the hero
rendered in this component's + ActionContent markup (Blazor CSS isolation is per-component). */ .cms-album-thumb { width: 40px; height: 40px;