From c3ec3acafa07c20a4bf74cf1385b42f7c48cedc9 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 20 Jun 2026 18:51:30 -0400 Subject: [PATCH] fix(queue): route scaffold masthead PLAY through queue; cache QueueItems snapshot --- .../AudioPlayerBar/AudioPlayerBar.razor.cs | 19 +++++++++++++------ .../Controls/ReleaseDetailScaffold.razor.cs | 11 +++++++++-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs index 8149894..45811eb 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs @@ -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). private bool ShowFixedPanel => Fixed && HasQueue; - // Snapshot the live queue into a NEW list every render. QueueService.Items returns the service's - // backing list by reference, so passing it straight through means Blazor parameter diffing sees 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 - // access forces the parameter to change identity on every mutation, so the panel re-flows immediately. + // 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 unchanged reference after an in-place Clear/remove/reorder and the child (QueueList / + // MudDropContainer) keeps its stale snapshot until reopened. We snapshot on first access and + // 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? _queueItemsCache; private IReadOnlyList QueueItems => - QueueService is null ? [] : QueueService.Items.ToList(); + _queueItemsCache ??= QueueService is null ? [] : QueueService.Items.ToList(); private int QueueCurrentIndex => QueueService?.CurrentIndex ?? -1; // 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() { + // 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 // 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. diff --git a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs index 567165f..07f076d 100644 --- a/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs +++ b/DeepDrftPublic.Client/Controls/ReleaseDetailScaffold.razor.cs @@ -13,6 +13,7 @@ namespace DeepDrftPublic.Client.Controls; public partial class ReleaseDetailScaffold : ComponentBase { [CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; } + [CascadingParameter] public IQueueService? Queue { get; set; } [Parameter] public required string Title { 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; - // Toggle if this track is already active (playing or paused); otherwise start a fresh - // stream. SelectTrackStreaming is the live entry point — the buffered path is dead. + // Toggle if this track is already active (playing or paused); otherwise PLAY it — + // 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; if (isThisTrack && (PlayerService.IsPlaying || PlayerService.IsPaused)) { await PlayerService.TogglePlayPause(); } + else if (Queue is not null) + { + await Queue.PlayTrack(Track); + } else { await PlayerService.SelectTrackStreaming(Track);