fix(queue): route scaffold masthead PLAY through queue; cache QueueItems snapshot

This commit is contained in:
daniel-c-harvey
2026-06-20 18:51:30 -04:00
parent 214f708e65
commit c3ec3acafa
2 changed files with 22 additions and 8 deletions
@@ -85,13 +85,15 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
// Gated on Fixed + non-empty so single-track embeds keep their compact, panel-free bar (UC6). // Gated on Fixed + non-empty so single-track embeds keep their compact, panel-free bar (UC6).
private bool ShowFixedPanel => Fixed && HasQueue; private bool ShowFixedPanel => Fixed && HasQueue;
// Snapshot the live queue into a NEW list every render. QueueService.Items returns the service's // Cached snapshot of the queue list (bug #4 fix). QueueService.Items returns the service's
// backing list by reference, so passing it straight through means Blazor parameter diffing sees an // backing list by reference, so passing it straight through means Blazor parameter diffing sees
// unchanged reference after an in-place Clear/remove/reorder and the child (QueueList / // an unchanged reference after an in-place Clear/remove/reorder and the child (QueueList /
// MudDropContainer) keeps its stale snapshot until reopened (bug #4). Materializing a fresh list per // MudDropContainer) keeps its stale snapshot until reopened. We snapshot on first access and
// access forces the parameter to change identity on every mutation, so the panel re-flows immediately. // rebuild in OnQueueChanged, so every real mutation hands the child a NEW reference while
// progress-tick re-renders (the frequent path) reuse the cached one without allocating.
private IReadOnlyList<TrackDto>? _queueItemsCache;
private IReadOnlyList<TrackDto> QueueItems => private IReadOnlyList<TrackDto> QueueItems =>
QueueService is null ? [] : QueueService.Items.ToList(); _queueItemsCache ??= QueueService is null ? [] : QueueService.Items.ToList();
private int QueueCurrentIndex => QueueService?.CurrentIndex ?? -1; private int QueueCurrentIndex => QueueService?.CurrentIndex ?? -1;
// Fixed-mode panel collapse state (OQ1 Option A). Default expanded so a release embed shows the // Fixed-mode panel collapse state (OQ1 Option A). Default expanded so a release embed shows the
@@ -147,6 +149,11 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private void OnQueueChanged() private void OnQueueChanged()
{ {
// Invalidate the snapshot so QueueItems rebuilds a fresh list on the next render.
// This gives Blazor a new reference on every real mutation (bug #4 reactivity preserved)
// while progress-tick re-renders that don't go through here keep the cached reference.
_queueItemsCache = null;
// If a removal emptied the queue while the overlay was open, the button disappears (AC1) — close // If a removal emptied the queue while the overlay was open, the button disappears (AC1) — close
// the overlay so it cannot strand open over an empty queue. The button gate hides the overlay // the overlay so it cannot strand open over an empty queue. The button gate hides the overlay
// mount too, so this keeps state and view consistent. // mount too, so this keeps state and view consistent.
@@ -13,6 +13,7 @@ namespace DeepDrftPublic.Client.Controls;
public partial class ReleaseDetailScaffold : ComponentBase public partial class ReleaseDetailScaffold : ComponentBase
{ {
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; } [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[CascadingParameter] public IQueueService? Queue { get; set; }
[Parameter] public required string Title { get; set; } [Parameter] public required string Title { get; set; }
[Parameter] public string? Artist { get; set; } [Parameter] public string? Artist { get; set; }
@@ -96,13 +97,19 @@ public partial class ReleaseDetailScaffold : ComponentBase
{ {
if (Track is null || PlayerService is null) return; if (Track is null || PlayerService is null) return;
// Toggle if this track is already active (playing or paused); otherwise start a fresh // Toggle if this track is already active (playing or paused); otherwise PLAY it —
// stream. SelectTrackStreaming is the live entry point — the buffered path is dead. // prepend to the queue's front (deque PLAY semantics) so it becomes current and
// the existing queue stays intact behind it. Falls back to a direct stream when
// the queue cascade is absent (prerender / non-interactive).
var isThisTrack = PlayerService.CurrentTrack?.Id == Track.Id; var isThisTrack = PlayerService.CurrentTrack?.Id == Track.Id;
if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused)) if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused))
{ {
await PlayerService.TogglePlayPause(); await PlayerService.TogglePlayPause();
} }
else if (Queue is not null)
{
await Queue.PlayTrack(Track);
}
else else
{ {
await PlayerService.SelectTrackStreaming(Track); await PlayerService.SelectTrackStreaming(Track);