Files
deepdrft/DeepDrftPublic.Client/Pages/CutDetailBase.cs
T
daniel-c-harvey 07ddc69cee feat(public): add /cuts/{id} album-detail page
Compose ReleaseDetailScaffold via Header + BodyContent slots for the Cut
album view: left meta + Play/Share, right theme-bordered cover, TrackNumber-
ordered track list with per-row play. CutDetailBase carries the multi-track
prerender bridge.
2026-06-15 23:59:19 -04:00

69 lines
2.9 KiB
C#

using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.ViewModels;
using Microsoft.AspNetCore.Components;
namespace DeepDrftPublic.Client.Pages;
/// <summary>
/// Load + prerender-bridge logic for the Cut album-detail page (/cuts/{id}). Mirrors
/// <see cref="ReleaseDetailBase"/>'s discipline (id-addressed load in OnParametersSetAsync,
/// PersistentComponentState bridge guarded on id) but carries the multi-track payload (release +
/// ordered track list) the Cut page needs. Kept separate from the single-track base so neither
/// grows a medium conditional — the two release shapes are genuinely different (one track vs many).
/// </summary>
public abstract class CutDetailBase : ComponentBase, IDisposable
{
private const string PersistKey = "cut-detail";
[Parameter] public long Id { get; set; }
[Inject] public required CutDetailViewModel ViewModel { get; set; }
[Inject] public required PersistentComponentState PersistentState { get; set; }
private PersistingComponentStateSubscription _persistingSubscription;
// The release id the ViewModel currently holds — tracks param-only navigations (e.g.
// /cuts/5 -> /cuts/8) which reuse this component instance and fire OnParametersSet without
// re-running OnInitialized. Without it the page would keep the prior album's tracks.
private long _loadedId;
private bool _loaded;
protected override void OnInitialized()
=> _persistingSubscription = PersistentState.RegisterOnPersisting(Persist);
protected override async Task OnParametersSetAsync()
{
if (_loaded && _loadedId == Id) return;
// Capture the id synchronously before any await so a re-entrant call (rapid navigation or a
// re-render that changes Id while Load is in flight) sees the correct guard state.
_loadedId = Id;
_loaded = true;
// The bridged payload carries the release and its ordered tracks so the interactive pass
// renders identically without a second round-trip. Guard on the id: a payload for a different
// release must not seed this page (stale-bridge bleed across navigation).
if (PersistentState.TryTakeFromJson<BridgedCut>(PersistKey, out var restored)
&& restored?.Release is not null
&& restored.Release.Id == Id)
{
ViewModel.Restore(restored.Release, restored.Tracks);
}
else
{
await ViewModel.Load(Id);
}
}
private Task Persist()
{
if (ViewModel.Release is not null)
PersistentState.PersistAsJson(PersistKey, new BridgedCut(ViewModel.Release, ViewModel.Tracks));
return Task.CompletedTask;
}
public void Dispose() => _persistingSubscription.Dispose();
// JSON-serializable bridge payload. Round-trips through PersistentComponentState's serializer.
protected sealed record BridgedCut(ReleaseDto Release, IReadOnlyList<TrackDto> Tracks);
}