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"
|
@page "/tracks/mixes"
|
||||||
@using DeepDrftManager.Services
|
@inherits CmsMediumBrowserBase<CmsMixBrowser.MixRow>
|
||||||
@using DeepDrftModels.DTOs
|
@using DeepDrftModels.DTOs
|
||||||
@using DeepDrftModels.Enums
|
@using DeepDrftModels.Enums
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject ICmsReleaseService CmsReleaseService
|
|
||||||
@inject ISnackbar Snackbar
|
|
||||||
@inject ILogger<CmsMixBrowser> Logger
|
@inject ILogger<CmsMixBrowser> Logger
|
||||||
|
|
||||||
<PageTitle>Mixes — DeepDrft CMS</PageTitle>
|
<PageTitle>Mixes — DeepDrft CMS</PageTitle>
|
||||||
@@ -19,39 +17,15 @@
|
|||||||
|
|
||||||
<MudText Typo="Typo.h4" GutterBottom="true">Mixes</MudText>
|
<MudText Typo="Typo.h4" GutterBottom="true">Mixes</MudText>
|
||||||
|
|
||||||
@if (_loading)
|
<CmsMediumTable TRow="MixRow"
|
||||||
{
|
Rows="Rows"
|
||||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
Loading="Loading"
|
||||||
}
|
ReleaseAccessor="@(row => row.Release)"
|
||||||
else if (_rows.Count == 0)
|
ThumbUrl="@(key => ThumbUrl(key))"
|
||||||
{
|
TitleHeader="Mix"
|
||||||
<MudText Typo="Typo.body1" Class="mt-4">No mixes found.</MudText>
|
EmptyMessage="No mixes found.">
|
||||||
}
|
<ActionContent Context="row">
|
||||||
else
|
@if (row.HasWaveform)
|
||||||
{
|
|
||||||
<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">
|
<MudTooltip Text="Waveform generated">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
||||||
@@ -63,65 +37,34 @@
|
|||||||
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Warning" Size="Size.Small" />
|
<MudIcon Icon="@Icons.Material.Filled.Cancel" Color="Color.Warning" Size="Size.Small" />
|
||||||
</MudTooltip>
|
</MudTooltip>
|
||||||
}
|
}
|
||||||
</MudTd>
|
|
||||||
<MudTd DataLabel="Actions">
|
|
||||||
<MudButton Variant="Variant.Outlined"
|
<MudButton Variant="Variant.Outlined"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
StartIcon="@Icons.Material.Filled.GraphicEq"
|
StartIcon="@Icons.Material.Filled.GraphicEq"
|
||||||
Disabled="@context.IsGenerating"
|
Disabled="@row.IsGenerating"
|
||||||
OnClick="@(() => GenerateWaveformAsync(context))">
|
OnClick="@(() => GenerateWaveformAsync(row))">
|
||||||
@if (context.IsGenerating)
|
@if (row.IsGenerating)
|
||||||
{
|
{
|
||||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||||
<span>Generating…</span>
|
<span>Generating…</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>@(context.HasWaveform ? "Regenerate" : "Generate")</span>
|
<span>@(row.HasWaveform ? "Regenerate" : "Generate")</span>
|
||||||
}
|
}
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudTd>
|
</ActionContent>
|
||||||
</RowTemplate>
|
</CmsMediumTable>
|
||||||
</MudTable>
|
|
||||||
}
|
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<MixRow> _rows = new();
|
protected override ReleaseMedium Medium => ReleaseMedium.Mix;
|
||||||
private bool _loading = true;
|
protected override string MediumNoun => "mixes";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() => await LoadAsync();
|
protected override MixRow ToRow(ReleaseDto release) => new()
|
||||||
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
{
|
||||||
_loading = true;
|
Release = release,
|
||||||
// Mixes are single-track releases; a single generous page covers the CMS catalogue.
|
HasWaveform = !string.IsNullOrEmpty(release.MixMetadata?.WaveformEntryKey)
|
||||||
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)}";
|
|
||||||
|
|
||||||
private async Task GenerateWaveformAsync(MixRow row)
|
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 required ReleaseDto Release { get; set; }
|
||||||
public bool HasWaveform { 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"
|
@page "/tracks/sessions"
|
||||||
@using DeepDrftManager.Services
|
@inherits CmsMediumBrowserBase<CmsSessionBrowser.SessionRow>
|
||||||
@using DeepDrftModels.DTOs
|
@using DeepDrftModels.DTOs
|
||||||
@using DeepDrftModels.Enums
|
@using DeepDrftModels.Enums
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject ICmsReleaseService CmsReleaseService
|
|
||||||
@inject ISnackbar Snackbar
|
|
||||||
@inject ILogger<CmsSessionBrowser> Logger
|
@inject ILogger<CmsSessionBrowser> Logger
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
|
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
|
||||||
|
|
||||||
@@ -21,37 +18,15 @@
|
|||||||
|
|
||||||
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
|
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
|
||||||
|
|
||||||
@if (_loading)
|
<CmsMediumTable TRow="SessionRow"
|
||||||
{
|
Rows="Rows"
|
||||||
<MudProgressCircular Indeterminate="true" Class="mt-4" />
|
Loading="Loading"
|
||||||
}
|
ReleaseAccessor="@(row => row.Release)"
|
||||||
else if (_rows.Count == 0)
|
ThumbUrl="@(key => ThumbUrl(key))"
|
||||||
{
|
TitleHeader="Session"
|
||||||
<MudText Typo="Typo.body1" Class="mt-4">No sessions found.</MudText>
|
EmptyMessage="No sessions found.">
|
||||||
}
|
<ActionContent Context="row">
|
||||||
else
|
@if (row.HeroImageEntryKey is { Length: > 0 } heroKey)
|
||||||
{
|
|
||||||
<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>
|
<div class="cms-album-thumb" style="background-image: url('@ThumbUrl(heroKey)');"></div>
|
||||||
}
|
}
|
||||||
@@ -59,74 +34,40 @@
|
|||||||
{
|
{
|
||||||
<div class="cms-album-thumb cms-album-thumb--fallback"></div>
|
<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"
|
<MudFileUpload T="IBrowserFile"
|
||||||
Accept="image/*"
|
Accept="image/*"
|
||||||
FilesChanged="@(file => UploadHeroAsync(context, file))"
|
FilesChanged="@(file => UploadHeroAsync(row, file))"
|
||||||
Disabled="@context.IsUploading">
|
Disabled="@row.IsUploading">
|
||||||
<ActivatorContent>
|
<ActivatorContent>
|
||||||
<MudButton Variant="Variant.Outlined"
|
<MudButton Variant="Variant.Outlined"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
StartIcon="@Icons.Material.Filled.Image"
|
StartIcon="@Icons.Material.Filled.Image"
|
||||||
Disabled="@context.IsUploading">
|
Disabled="@row.IsUploading">
|
||||||
@if (context.IsUploading)
|
@if (row.IsUploading)
|
||||||
{
|
{
|
||||||
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
<MudProgressCircular Class="mr-2" Size="Size.Small" Indeterminate="true" />
|
||||||
<span>Uploading…</span>
|
<span>Uploading…</span>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span>@(context.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
|
<span>@(row.HeroImageEntryKey is { Length: > 0 } ? "Replace hero" : "Set hero")</span>
|
||||||
}
|
}
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</ActivatorContent>
|
</ActivatorContent>
|
||||||
</MudFileUpload>
|
</MudFileUpload>
|
||||||
</MudTd>
|
</ActionContent>
|
||||||
</RowTemplate>
|
</CmsMediumTable>
|
||||||
</MudTable>
|
|
||||||
}
|
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<SessionRow> _rows = new();
|
protected override ReleaseMedium Medium => ReleaseMedium.Session;
|
||||||
private bool _loading = true;
|
protected override string MediumNoun => "sessions";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() => await LoadAsync();
|
protected override SessionRow ToRow(ReleaseDto release) => new()
|
||||||
|
|
||||||
private async Task LoadAsync()
|
|
||||||
{
|
{
|
||||||
_loading = true;
|
Release = release,
|
||||||
// Sessions are single-track releases; a single generous page covers the CMS catalogue (same
|
HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey
|
||||||
// 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)}";
|
|
||||||
|
|
||||||
private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file)
|
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 required ReleaseDto Release { get; set; }
|
||||||
public string? HeroImageEntryKey { 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
|
/* Hero-thumbnail idiom for the session row's action cell. The cover thumb lives in CmsMediumTable's
|
||||||
class defined in CmsAlbumBrowser.razor.css does not reach this component's markup — a small,
|
own scoped CSS; this scoped copy reaches only the hero <div> rendered in this component's
|
||||||
intentional duplication rather than promoting a two-rule block to global app.css. */
|
ActionContent markup (Blazor CSS isolation is per-component). */
|
||||||
.cms-album-thumb {
|
.cms-album-thumb {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|||||||
Reference in New Issue
Block a user