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.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using DeepDrftManager.Services;
|
||||
using DeepDrftModels.DTOs;
|
||||
using DeepDrftModels.Enums;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace DeepDrftManager.Components.Pages.Tracks;
|
||||
|
||||
/// <summary>
|
||||
/// Shared fetch + state logic for the single-track medium browsers (Sessions, Mixes). Analogous to the
|
||||
/// public-site <c>MediumBrowseBase</c>: subclasses supply the <see cref="Medium"/>, the noun used in
|
||||
/// error text, and a per-row projection from <see cref="ReleaseDto"/> 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 <c>CmsMediumTable</c>; subclasses render it and fill only the action column.
|
||||
/// </summary>
|
||||
/// <typeparam name="TRow">The subclass's row model wrapping a <see cref="ReleaseDto"/>.</typeparam>
|
||||
public abstract class CmsMediumBrowserBase<TRow> : ComponentBase
|
||||
{
|
||||
[Inject] public required ICmsReleaseService CmsReleaseService { get; set; }
|
||||
[Inject] public required ISnackbar Snackbar { get; set; }
|
||||
|
||||
/// <summary>The medium this browser lists. Subclass-supplied constant.</summary>
|
||||
protected abstract ReleaseMedium Medium { get; }
|
||||
|
||||
/// <summary>Plural noun for this medium used in error text (e.g. "sessions", "mixes").</summary>
|
||||
protected abstract string MediumNoun { get; }
|
||||
|
||||
/// <summary>Projects a fetched release into the subclass's row model.</summary>
|
||||
protected abstract TRow ToRow(ReleaseDto release);
|
||||
|
||||
protected List<TRow> 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<TRow>();
|
||||
}
|
||||
|
||||
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)}";
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (Rows.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-4">@EmptyMessage</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable T="TRow" Items="Rows" Hover="true" Striped="true" Dense="true" Bordered="false" FixedHeader="true">
|
||||
<HeaderContent>
|
||||
<MudTh Style="width: 1%;">Cover</MudTh>
|
||||
<MudTh>@TitleHeader</MudTh>
|
||||
<MudTh>Artist</MudTh>
|
||||
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Cover">
|
||||
@{ var release = ReleaseAccessor(context); }
|
||||
@if (!string.IsNullOrEmpty(release.ImagePath))
|
||||
{
|
||||
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(release.ImagePath)');"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="@TitleHeader">@ReleaseAccessor(context).Title</MudTd>
|
||||
<MudTd DataLabel="Artist">@ReleaseAccessor(context).Artist</MudTd>
|
||||
<MudTd DataLabel="Actions">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@ActionContent(context)
|
||||
<MudTooltip Text="Edit release">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
Color="Color.Primary"
|
||||
Href="@($"/tracks/album/{Uri.EscapeDataString(ReleaseAccessor(context).Title)}/edit")" />
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter] public required IReadOnlyList<TRow> Rows { get; set; }
|
||||
[Parameter] public bool Loading { get; set; }
|
||||
|
||||
/// <summary>Projects a row to its underlying release for the cover/title/artist cells.</summary>
|
||||
[Parameter] public required Func<TRow, ReleaseDto> ReleaseAccessor { get; set; }
|
||||
|
||||
/// <summary>Medium-specific cell content (hero / waveform / generate action) for each row.</summary>
|
||||
[Parameter] public required RenderFragment<TRow> ActionContent { get; set; }
|
||||
|
||||
/// <summary>Relative thumbnail URL builder; the base class supplies its proxy-aware helper.</summary>
|
||||
[Parameter] public required Func<string, string> ThumbUrl { get; set; }
|
||||
|
||||
/// <summary>Column header / data-label for the title column (e.g. "Session", "Mix").</summary>
|
||||
[Parameter] public string TitleHeader { get; set; } = "Title";
|
||||
|
||||
[Parameter] public string EmptyMessage { get; set; } = "Nothing here yet.";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
@page "/tracks/mixes"
|
||||
@using DeepDrftManager.Services
|
||||
@inherits CmsMediumBrowserBase<CmsMixBrowser.MixRow>
|
||||
@using DeepDrftModels.DTOs
|
||||
@using DeepDrftModels.Enums
|
||||
@attribute [Authorize]
|
||||
@inject ICmsReleaseService CmsReleaseService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject ILogger<CmsMixBrowser> Logger
|
||||
|
||||
<PageTitle>Mixes — DeepDrft CMS</PageTitle>
|
||||
@@ -19,109 +17,54 @@
|
||||
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Mixes</MudText>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (_rows.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-4">No mixes found.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable T="MixRow" Items="_rows" Hover="true" Striped="true" Dense="true" Bordered="false" FixedHeader="true">
|
||||
<HeaderContent>
|
||||
<MudTh Style="width: 1%;">Cover</MudTh>
|
||||
<MudTh>Mix</MudTh>
|
||||
<MudTh>Artist</MudTh>
|
||||
<MudTh Style="width: 1%; white-space: nowrap;">Waveform</MudTh>
|
||||
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Cover">
|
||||
@if (!string.IsNullOrEmpty(context.Release.ImagePath))
|
||||
{
|
||||
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(context.Release.ImagePath)');"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Mix">@context.Release.Title</MudTd>
|
||||
<MudTd DataLabel="Artist">@context.Release.Artist</MudTd>
|
||||
<MudTd DataLabel="Waveform">
|
||||
@if (context.HasWaveform)
|
||||
{
|
||||
<MudTooltip Text="Waveform generated">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="No waveform — incomplete">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Warning" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Actions">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
||||
Disabled="@context.IsGenerating"
|
||||
OnClick="@(() => GenerateWaveformAsync(context))">
|
||||
@if (context.IsGenerating)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<span>Generating…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(context.HasWaveform ? "Regenerate" : "Generate")</span>
|
||||
}
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
<CmsMediumTable TRow="MixRow"
|
||||
Rows="Rows"
|
||||
Loading="Loading"
|
||||
ReleaseAccessor="@(row => row.Release)"
|
||||
ThumbUrl="@(key => ThumbUrl(key))"
|
||||
TitleHeader="Mix"
|
||||
EmptyMessage="No mixes found.">
|
||||
<ActionContent Context="row">
|
||||
@if (row.HasWaveform)
|
||||
{
|
||||
<MudTooltip Text="Waveform generated">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTooltip Text="No waveform — incomplete">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Warning" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
||||
Disabled="@row.IsGenerating"
|
||||
OnClick="@(() => GenerateWaveformAsync(row))">
|
||||
@if (row.IsGenerating)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<span>Generating…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(row.HasWaveform ? "Regenerate" : "Generate")</span>
|
||||
}
|
||||
</MudButton>
|
||||
</ActionContent>
|
||||
</CmsMediumTable>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private List<MixRow> _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<MixRow>();
|
||||
}
|
||||
_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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
@page "/tracks/sessions"
|
||||
@using DeepDrftManager.Services
|
||||
@inherits CmsMediumBrowserBase<CmsSessionBrowser.SessionRow>
|
||||
@using DeepDrftModels.DTOs
|
||||
@using DeepDrftModels.Enums
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@attribute [Authorize]
|
||||
@inject ICmsReleaseService CmsReleaseService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject ILogger<CmsSessionBrowser> Logger
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
|
||||
|
||||
@@ -21,112 +18,56 @@
|
||||
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
||||
}
|
||||
else if (_rows.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mt-4">No sessions found.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable T="SessionRow" Items="_rows" Hover="true" Striped="true" Dense="true" Bordered="false" FixedHeader="true">
|
||||
<HeaderContent>
|
||||
<MudTh Style="width: 1%;">Cover</MudTh>
|
||||
<MudTh Style="width: 1%;">Hero</MudTh>
|
||||
<MudTh>Session</MudTh>
|
||||
<MudTh>Artist</MudTh>
|
||||
<MudTh Style="width: 1%; white-space: nowrap;">Actions</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Cover">
|
||||
@if (!string.IsNullOrEmpty(context.Release.ImagePath))
|
||||
{
|
||||
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(context.Release.ImagePath)');"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Hero">
|
||||
@if (context.HeroImageEntryKey is { Length: > 0 } heroKey)
|
||||
{
|
||||
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Session">@context.Release.Title</MudTd>
|
||||
<MudTd DataLabel="Artist">@context.Release.Artist</MudTd>
|
||||
<MudTd DataLabel="Actions">
|
||||
<MudFileUpload T="IBrowserFile"
|
||||
Accept="image/*"
|
||||
FilesChanged="@(file => UploadHeroAsync(context, file))"
|
||||
Disabled="@context.IsUploading">
|
||||
<ActivatorContent>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Disabled="@context.IsUploading">
|
||||
@if (context.IsUploading)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<span>Uploading…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(context.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
|
||||
}
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
<CmsMediumTable TRow="SessionRow"
|
||||
Rows="Rows"
|
||||
Loading="Loading"
|
||||
ReleaseAccessor="@(row => row.Release)"
|
||||
ThumbUrl="@(key => ThumbUrl(key))"
|
||||
TitleHeader="Session"
|
||||
EmptyMessage="No sessions found.">
|
||||
<ActionContent Context="row">
|
||||
@if (row.HeroImageEntryKey is { Length: > 0 } heroKey)
|
||||
{
|
||||
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
||||
}
|
||||
<MudFileUpload T="IBrowserFile"
|
||||
Accept="image/*"
|
||||
FilesChanged="@(file => UploadHeroAsync(row, file))"
|
||||
Disabled="@row.IsUploading">
|
||||
<ActivatorContent>
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Size="Size.Small"
|
||||
StartIcon="@Icons.Material.Filled.Image"
|
||||
Disabled="@row.IsUploading">
|
||||
@if (row.IsUploading)
|
||||
{
|
||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||
<span>Uploading…</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
|
||||
}
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</ActionContent>
|
||||
</CmsMediumTable>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private List<SessionRow> _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<SessionRow>();
|
||||
}
|
||||
_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; }
|
||||
|
||||
@@ -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 <div> rendered in this component's
|
||||
ActionContent markup (Blazor CSS isolation is per-component). */
|
||||
.cms-album-thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
Reference in New Issue
Block a user