feat(player): docked queue overlay with reorder, remove, jump, and clear-upcoming
Add a Queue toggle to the docked player bar opening a centered editable queue overlay. New additive QueueService.ClearUpcoming keeps the playing track while dropping the rest. Current track is non-removable.
This commit is contained in:
@@ -25,6 +25,9 @@ else
|
||||
HasPrevious="HasPrevious"
|
||||
SkipNext="@SkipNext"
|
||||
SkipPrevious="@SkipPrevious"
|
||||
ShowQueueButton="ShowQueueButton"
|
||||
QueueOpen="_queueOpen"
|
||||
QueueToggle="@ToggleQueue"
|
||||
Class="transport-zone"/>
|
||||
|
||||
<VolumeZone Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
@@ -49,12 +52,27 @@ else
|
||||
|
||||
@if (!string.IsNullOrEmpty(ErrorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error"
|
||||
ShowCloseIcon="true"
|
||||
CloseIconClicked="ClearError"
|
||||
<MudAlert Severity="Severity.Error"
|
||||
ShowCloseIcon="true"
|
||||
CloseIconClicked="ClearError"
|
||||
Class="ma-2">
|
||||
@ErrorMessage
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@* Docked queue overlay (Phase 17 §3.2). MudOverlay portals to the body, so its position here in
|
||||
the dock subtree does not affect its screen-centered rendering. Only mounted in docked mode —
|
||||
the Fixed embed gets its own inline panel in a later wave. *@
|
||||
@if (ShowQueueButton)
|
||||
{
|
||||
<QueueOverlay Visible="_queueOpen"
|
||||
Items="QueueItems"
|
||||
CurrentIndex="QueueCurrentIndex"
|
||||
OnClose="@CloseQueue"
|
||||
OnClear="@ClearUpcoming"
|
||||
OnReorder="@OnQueueReorder"
|
||||
OnRemove="@OnQueueRemove"
|
||||
OnJump="@OnQueueJump"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
private bool _isMinimized = true;
|
||||
private bool _isSeeking = false;
|
||||
private double _seekPosition = 0;
|
||||
private bool _queueOpen = false;
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
private IQueueService? _subscribedQueue;
|
||||
|
||||
@@ -63,6 +64,13 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
private bool HasNext => QueueService?.HasNext ?? false;
|
||||
private bool HasPrevious => QueueService?.HasPrevious ?? false;
|
||||
|
||||
// Queue overlay state. The button (and overlay) appear only in docked mode with a non-empty queue,
|
||||
// mirroring the skip-affordance gating (AC1): with no queue the bar is byte-for-byte its pre-queue
|
||||
// self. The Fixed embed gets an inline panel in a later wave, so the docked overlay is !Fixed-only.
|
||||
private bool ShowQueueButton => !Fixed && (QueueService?.Items.Count ?? 0) > 0;
|
||||
private IReadOnlyList<TrackDto> QueueItems => QueueService?.Items ?? [];
|
||||
private int QueueCurrentIndex => QueueService?.CurrentIndex ?? -1;
|
||||
|
||||
/// <summary>
|
||||
/// Display time - shows seek position while dragging, otherwise current playback time.
|
||||
/// </summary>
|
||||
@@ -106,7 +114,16 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
|
||||
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
private void OnQueueChanged() => InvokeAsync(StateHasChanged);
|
||||
private void OnQueueChanged()
|
||||
{
|
||||
// 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.
|
||||
if (_queueOpen && (QueueService?.Items.Count ?? 0) == 0)
|
||||
_queueOpen = false;
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task SkipNext()
|
||||
{
|
||||
@@ -120,6 +137,27 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
await QueueService.Previous();
|
||||
}
|
||||
|
||||
private void ToggleQueue() => _queueOpen = !_queueOpen;
|
||||
|
||||
private void CloseQueue() => _queueOpen = false;
|
||||
|
||||
// Reorder/remove/clear are interop-free engine mutations (C2/C5): they never re-stream or interrupt
|
||||
// the playing track. QueueChanged re-renders the bar and the overlay's list.
|
||||
private void OnQueueReorder((int FromIndex, int ToIndex) move) =>
|
||||
QueueService?.Move(move.FromIndex, move.ToIndex);
|
||||
|
||||
private void OnQueueRemove(int index) => QueueService?.RemoveAt(index);
|
||||
|
||||
private void ClearUpcoming() => QueueService?.ClearUpcoming();
|
||||
|
||||
// Jump reuses the existing "play from index" semantics (OQ2). This is the one queue action that
|
||||
// touches playback — it streams the chosen track via the player.
|
||||
private async Task OnQueueJump(int index)
|
||||
{
|
||||
if (QueueService == null) return;
|
||||
await QueueService.PlayRelease(QueueService.Items, index);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// The Fixed embed is already in normal flow — no spacer/clip needed.
|
||||
@@ -260,6 +298,12 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
_subscribedService = null;
|
||||
}
|
||||
|
||||
if (_subscribedQueue != null)
|
||||
{
|
||||
_subscribedQueue.QueueChanged -= OnQueueChanged;
|
||||
_subscribedQueue = null;
|
||||
}
|
||||
|
||||
if (_spacerModule is not null)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -20,5 +20,20 @@
|
||||
Indeterminate="@(LoadProgress == 0)"/>
|
||||
}
|
||||
</MudStack>
|
||||
@* Queue toggle: a second row between the transport controls and the timestamp (§3.1 placement —
|
||||
"below the control buttons, to the left of the timestamps"). Shown only when a queue is loaded,
|
||||
mirroring the skip-affordance gating, so an empty/single-track player is byte-for-byte unchanged. *@
|
||||
@if (ShowQueueButton)
|
||||
{
|
||||
<MudTooltip Text="Queue">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.QueueMusic"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Medium"
|
||||
OnClick="QueueToggle"
|
||||
aria-label="Queue"
|
||||
aria-expanded="@QueueOpen"
|
||||
Class="@($"deepdrft-queue-toggle{(QueueOpen ? " deepdrft-queue-toggle-active" : "")}")"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
<TimestampLabel CurrentTime="DisplayTime" Duration="@Duration"/>
|
||||
</MudStack>
|
||||
|
||||
@@ -18,5 +18,15 @@ public partial class PlayerTransportZone : ComponentBase
|
||||
[Parameter] public bool HasPrevious { get; set; }
|
||||
[Parameter] public EventCallback SkipNext { get; set; }
|
||||
[Parameter] public EventCallback SkipPrevious { get; set; }
|
||||
|
||||
/// <summary>Whether to render the Queue toggle button. Gated on a non-empty queue by the bar.</summary>
|
||||
[Parameter] public bool ShowQueueButton { get; set; }
|
||||
|
||||
/// <summary>Whether the queue overlay is open. Drives the button's active state.</summary>
|
||||
[Parameter] public bool QueueOpen { get; set; }
|
||||
|
||||
/// <summary>Raised when the Queue button is clicked. The bar toggles the overlay.</summary>
|
||||
[Parameter] public EventCallback QueueToggle { get; set; }
|
||||
|
||||
[Parameter] public string? Class { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user