Merge p9-w8-8a-tab-strip into dev (8.A: CMS Release Archive medium tab strip)
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
@inherits CmsMediumBrowserBase<CmsCutBrowser.CutRow>
|
||||
@using DeepDrftModels.DTOs
|
||||
@using DeepDrftModels.Enums
|
||||
|
||||
@* Cut-filtered release grid for the Release Archive's CUTS tab (Phase 9 §8.A). Derived from the same
|
||||
CmsMediumBrowserBase pattern the Session/Mix browsers use, so a fourth medium would follow the same
|
||||
shape with no parallel path. Cuts carry no medium-specific row action (no hero, no waveform), so the
|
||||
ActionContent slot renders nothing — every row still gets the shared Edit affordance from
|
||||
CmsMediumTable. Embedded as tab content only; it has no standalone @page route. *@
|
||||
<CmsMediumTable TRow="CutRow"
|
||||
Rows="Rows"
|
||||
Loading="Loading"
|
||||
ReleaseAccessor="@(row => row.Release)"
|
||||
ThumbUrl="@(key => ThumbUrl(key))"
|
||||
TitleHeader="Cut"
|
||||
EmptyMessage="No cuts found.">
|
||||
<ActionContent Context="row">
|
||||
</ActionContent>
|
||||
</CmsMediumTable>
|
||||
|
||||
@code {
|
||||
protected override ReleaseMedium Medium => ReleaseMedium.Cut;
|
||||
protected override string MediumNoun => "cuts";
|
||||
|
||||
protected override CutRow ToRow(ReleaseDto release) => new() { Release = release };
|
||||
|
||||
public sealed class CutRow
|
||||
{
|
||||
public required ReleaseDto Release { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,25 +5,58 @@
|
||||
@attribute [Authorize]
|
||||
@inject ILogger<CmsMixBrowser> Logger
|
||||
|
||||
<PageTitle>Mixes — DeepDrft CMS</PageTitle>
|
||||
@* Embedded as the MIXES tab content of the Release Archive (Phase 9 §8.A), and still routable at
|
||||
/tracks/mixes for direct-URL access. When embedded, the page chrome (title, container, the now-
|
||||
meaningless "Back to Release Archive" button) is suppressed — the host tab strip owns that frame; only
|
||||
the grid renders. The standalone route keeps the full page chrome. The per-row waveform affordance
|
||||
(9.5.E) is preserved in both contexts. *@
|
||||
@if (Embedded)
|
||||
{
|
||||
@GridContent
|
||||
}
|
||||
else
|
||||
{
|
||||
<PageTitle>Mixes — DeepDrft CMS</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||
<MudButton Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
Href="/tracks/archive"
|
||||
Class="mb-4">
|
||||
Back to Release Archive
|
||||
</MudButton>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||
<MudButton Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
Href="/tracks/archive"
|
||||
Class="mb-4">
|
||||
Back to Release Archive
|
||||
</MudButton>
|
||||
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Mixes</MudText>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Mixes</MudText>
|
||||
|
||||
<CmsMediumTable TRow="MixRow"
|
||||
Rows="Rows"
|
||||
Loading="Loading"
|
||||
ReleaseAccessor="@(row => row.Release)"
|
||||
ThumbUrl="@(key => ThumbUrl(key))"
|
||||
TitleHeader="Mix"
|
||||
EmptyMessage="No mixes found.">
|
||||
@GridContent
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// True when rendered as tab content inside the Release Archive; suppresses the standalone page
|
||||
/// chrome (title, container, back button). False (default) renders the full routable page.
|
||||
/// </summary>
|
||||
[Parameter] public bool Embedded { get; set; }
|
||||
|
||||
protected override ReleaseMedium Medium => ReleaseMedium.Mix;
|
||||
protected override string MediumNoun => "mixes";
|
||||
|
||||
protected override MixRow ToRow(ReleaseDto release) => new()
|
||||
{
|
||||
Release = release,
|
||||
HasWaveform = !string.IsNullOrEmpty(release.MixMetadata?.WaveformEntryKey)
|
||||
};
|
||||
|
||||
// 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.
|
||||
private RenderFragment GridContent => @<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)
|
||||
{
|
||||
@@ -53,18 +86,7 @@
|
||||
}
|
||||
</MudButton>
|
||||
</ActionContent>
|
||||
</CmsMediumTable>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
protected override ReleaseMedium Medium => ReleaseMedium.Mix;
|
||||
protected override string MediumNoun => "mixes";
|
||||
|
||||
protected override MixRow ToRow(ReleaseDto release) => new()
|
||||
{
|
||||
Release = release,
|
||||
HasWaveform = !string.IsNullOrEmpty(release.MixMetadata?.WaveformEntryKey)
|
||||
};
|
||||
</CmsMediumTable>;
|
||||
|
||||
private async Task GenerateWaveformAsync(MixRow row)
|
||||
{
|
||||
|
||||
@@ -6,25 +6,58 @@
|
||||
@attribute [Authorize]
|
||||
@inject ILogger<CmsSessionBrowser> Logger
|
||||
|
||||
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
|
||||
@* Embedded as the SESSIONS tab content of the Release Archive (Phase 9 §8.A), and still routable at
|
||||
/tracks/sessions for direct-URL access. When embedded, the page chrome (title, container, the now-
|
||||
meaningless "Back to Release Archive" button) is suppressed — the host tab strip owns that frame; only
|
||||
the grid renders. The standalone route keeps the full page chrome. The per-row hero affordance (9.5.E)
|
||||
is preserved in both contexts. *@
|
||||
@if (Embedded)
|
||||
{
|
||||
@GridContent
|
||||
}
|
||||
else
|
||||
{
|
||||
<PageTitle>Sessions — DeepDrft CMS</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||
<MudButton Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
Href="/tracks/archive"
|
||||
Class="mb-4">
|
||||
Back to Release Archive
|
||||
</MudButton>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
||||
<MudButton Variant="Variant.Text"
|
||||
StartIcon="@Icons.Material.Filled.ArrowBack"
|
||||
Href="/tracks/archive"
|
||||
Class="mb-4">
|
||||
Back to Release Archive
|
||||
</MudButton>
|
||||
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
|
||||
<MudText Typo="Typo.h4" GutterBottom="true">Sessions</MudText>
|
||||
|
||||
<CmsMediumTable TRow="SessionRow"
|
||||
Rows="Rows"
|
||||
Loading="Loading"
|
||||
ReleaseAccessor="@(row => row.Release)"
|
||||
ThumbUrl="@(key => ThumbUrl(key))"
|
||||
TitleHeader="Session"
|
||||
EmptyMessage="No sessions found.">
|
||||
@GridContent
|
||||
</MudContainer>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// True when rendered as tab content inside the Release Archive; suppresses the standalone page
|
||||
/// chrome (title, container, back button). False (default) renders the full routable page.
|
||||
/// </summary>
|
||||
[Parameter] public bool Embedded { get; set; }
|
||||
|
||||
protected override ReleaseMedium Medium => ReleaseMedium.Session;
|
||||
protected override string MediumNoun => "sessions";
|
||||
|
||||
protected override SessionRow ToRow(ReleaseDto release) => new()
|
||||
{
|
||||
Release = release,
|
||||
HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey
|
||||
};
|
||||
|
||||
// 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.
|
||||
private RenderFragment GridContent => @<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)
|
||||
{
|
||||
@@ -56,18 +89,7 @@
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</ActionContent>
|
||||
</CmsMediumTable>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
protected override ReleaseMedium Medium => ReleaseMedium.Session;
|
||||
protected override string MediumNoun => "sessions";
|
||||
|
||||
protected override SessionRow ToRow(ReleaseDto release) => new()
|
||||
{
|
||||
Release = release,
|
||||
HeroImageEntryKey = release.SessionMetadata?.HeroImageEntryKey
|
||||
};
|
||||
</CmsMediumTable>;
|
||||
|
||||
private async Task UploadHeroAsync(SessionRow row, IBrowserFile? file)
|
||||
{
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
@using DeepDrftModels.Enums
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@* Release Archive: one card per ReleaseMedium, driven off Enum.GetValues + a display-metadata table.
|
||||
No hardcoded three-arm switch in markup — adding a medium surfaces a new card automatically and only
|
||||
needs one new entry in MediumCards below. Card idiom mirrors CmsGenreBrowser (MudCard + swatch). *@
|
||||
<MudGrid Spacing="3" Class="mt-2">
|
||||
@foreach (var medium in Enum.GetValues<ReleaseMedium>())
|
||||
{
|
||||
var info = MediumCards[medium];
|
||||
<MudItem xs="12" sm="6" md="4">
|
||||
<MudCard Elevation="1"
|
||||
Style="cursor: pointer;"
|
||||
@onclick="@(() => Navigation.NavigateTo(info.Route))">
|
||||
<div class="@($"cms-medium-swatch cms-medium-swatch--{info.SwatchModifier}")"></div>
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.h6">@info.Label</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary">@info.Descriptor</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@code {
|
||||
private sealed record MediumCardInfo(string Label, string Descriptor, string SwatchModifier, string Route);
|
||||
|
||||
// The one place medium → display + navigation target lives. A future medium adds one entry here;
|
||||
// the markup above is untouched. The enum→record dictionary is a switch, data-structured (§3.1).
|
||||
private static readonly IReadOnlyDictionary<ReleaseMedium, MediumCardInfo> MediumCards =
|
||||
new Dictionary<ReleaseMedium, MediumCardInfo>
|
||||
{
|
||||
[ReleaseMedium.Cut] = new("Cuts", "Studio singles, EPs, and albums", "cut", "/tracks/albums"),
|
||||
[ReleaseMedium.Session] = new("Sessions", "Single-track live recordings", "session", "/tracks/sessions"),
|
||||
[ReleaseMedium.Mix] = new("Mixes", "Single-track DJ mixes", "mix", "/tracks/mixes"),
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
.cms-medium-swatch {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
background-color: var(--mud-palette-action-default-hover);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.cms-medium-swatch--cut {
|
||||
background-color: var(--mud-palette-primary-hover);
|
||||
}
|
||||
|
||||
.cms-medium-swatch--session {
|
||||
background-color: var(--mud-palette-secondary-hover);
|
||||
}
|
||||
|
||||
.cms-medium-swatch--mix {
|
||||
background-color: var(--mud-palette-tertiary-hover);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
@page "/tracks/genres"
|
||||
@page "/tracks/archive"
|
||||
@using DeepDrftManager.Services
|
||||
@using DeepDrftModels.Enums
|
||||
@inject CmsTrackBrowserViewModel VM
|
||||
@inject ICmsTrackService CmsTrackService
|
||||
@inject ISnackbar Snackbar
|
||||
@@ -36,6 +37,9 @@
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@* Top-level browse dimension. The former three-way toggle (Tracks / Releases / Release Archive)
|
||||
collapsed to two (§8.A): "Releases" now hosts the in-page medium tab strip below, subsuming both
|
||||
the old Releases grid (as the ALL tab) and the retired Release Archive cards. *@
|
||||
<MudToggleGroup T="BrowseMode"
|
||||
Value="VM.Mode"
|
||||
ValueChanged="OnModeChanged"
|
||||
@@ -45,7 +49,6 @@
|
||||
Class="mb-4">
|
||||
<MudToggleItem Value="BrowseMode.Tracks">Tracks</MudToggleItem>
|
||||
<MudToggleItem Value="BrowseMode.Albums">Releases</MudToggleItem>
|
||||
<MudToggleItem Value="BrowseMode.Archive">Release Archive</MudToggleItem>
|
||||
</MudToggleGroup>
|
||||
|
||||
@if (VM.Mode == BrowseMode.Tracks)
|
||||
@@ -54,15 +57,21 @@
|
||||
}
|
||||
else if (VM.Mode == BrowseMode.Albums)
|
||||
{
|
||||
@* The all-releases grid is now a self-contained component (Phase 9 §8.B): it owns its own load
|
||||
and refresh, so the host renders it with no parameters. The 8.A tab strip hosts this same
|
||||
component as its ALL tab. Genre mode still uses the VM cache below; only album loading moved
|
||||
into the component, so VM.Albums / VM.AlbumsLoading are no longer read here. *@
|
||||
<CmsAllReleasesGrid OnReleasesChanged="OnAlbumsChanged" />
|
||||
}
|
||||
else if (VM.Mode == BrowseMode.Archive)
|
||||
{
|
||||
<ReleaseArchiveBrowser />
|
||||
@* The Release Archive tab strip (§8.A): an ALL tab plus one tab per ReleaseMedium, ALL left-most.
|
||||
The medium tabs are enum-driven — a fourth medium adds a tab automatically; only a label-lookup
|
||||
entry (MediumTabLabels) and a content arm (MediumGrid) are needed, no markup fork. Selecting a
|
||||
tab swaps the grid below in place; no navigation to a separate page occurs. *@
|
||||
<MudTabs Elevation="0" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pt-4">
|
||||
<MudTabPanel Text="ALL">
|
||||
<CmsAllReleasesGrid OnReleasesChanged="OnAlbumsChanged" />
|
||||
</MudTabPanel>
|
||||
@foreach (var medium in Enum.GetValues<ReleaseMedium>())
|
||||
{
|
||||
<MudTabPanel Text="@MediumTabLabels[medium]">
|
||||
@MediumGrid(medium)
|
||||
</MudTabPanel>
|
||||
}
|
||||
</MudTabs>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -78,6 +87,29 @@
|
||||
@code {
|
||||
private CmsTrackGrid? _grid;
|
||||
|
||||
// Medium → tab label. The one place medium display text lives for the tab strip; a future medium adds
|
||||
// one entry here and surfaces a tab automatically. Mirrors the extension discipline the retired
|
||||
// ReleaseArchiveBrowser used for its cards. The ALL tab is rendered separately (it is not a medium).
|
||||
private static readonly IReadOnlyDictionary<ReleaseMedium, string> MediumTabLabels =
|
||||
new Dictionary<ReleaseMedium, string>
|
||||
{
|
||||
[ReleaseMedium.Cut] = "CUTS",
|
||||
[ReleaseMedium.Session] = "SESSIONS",
|
||||
[ReleaseMedium.Mix] = "MIXES",
|
||||
};
|
||||
|
||||
// Medium → embedded grid. Each medium's grid is its own component (Cut has no per-row action; Session
|
||||
// carries hero upload; Mix carries waveform generation), so the content dispatch is a per-medium
|
||||
// mapping by nature — but it is a single switch returning a fragment, not a markup fork. The browsers
|
||||
// render Embedded so their standalone page chrome (container, title, back button) is suppressed here.
|
||||
private RenderFragment MediumGrid(ReleaseMedium medium) => medium switch
|
||||
{
|
||||
ReleaseMedium.Cut => @<CmsCutBrowser />,
|
||||
ReleaseMedium.Session => @<CmsSessionBrowser Embedded="true" />,
|
||||
ReleaseMedium.Mix => @<CmsMixBrowser Embedded="true" />,
|
||||
_ => @<MudText Typo="Typo.body1" Class="mt-4">No grid for this medium.</MudText>
|
||||
};
|
||||
|
||||
// The all-releases grid refreshes its own list after a delete; this notification lets us invalidate
|
||||
// the VM's genre cache so genre counts reflect the deletion on the next switch into Genre mode.
|
||||
private void OnAlbumsChanged()
|
||||
@@ -93,10 +125,12 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// /tracks/archive and /tracks/albums both land on the Releases view (the tab strip); the old
|
||||
// separate Archive mode is retired (§8.A) but the route stays reachable rather than 404ing.
|
||||
var uri = NavigationManager.Uri;
|
||||
var initial =
|
||||
uri.Contains("/tracks/albums", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Albums
|
||||
: uri.Contains("/tracks/archive", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Archive
|
||||
: uri.Contains("/tracks/archive", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Albums
|
||||
: uri.Contains("/tracks/genres", StringComparison.OrdinalIgnoreCase) ? BrowseMode.Genres
|
||||
: BrowseMode.Tracks;
|
||||
await VM.SwitchModeAsync(initial);
|
||||
@@ -108,7 +142,6 @@
|
||||
var path = mode switch
|
||||
{
|
||||
BrowseMode.Albums => "/tracks/albums",
|
||||
BrowseMode.Archive => "/tracks/archive",
|
||||
BrowseMode.Genres => "/tracks/genres",
|
||||
_ => "/tracks"
|
||||
};
|
||||
|
||||
@@ -6,8 +6,10 @@ namespace DeepDrftManager.Services;
|
||||
public enum BrowseMode
|
||||
{
|
||||
Tracks,
|
||||
|
||||
/// <summary>The release view — hosts the medium tab strip (ALL · CUTS · SESSIONS · MIXES, §8.A).</summary>
|
||||
Albums,
|
||||
Archive,
|
||||
|
||||
Genres,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user