fe3819f378
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.
128 lines
5.5 KiB
Plaintext
128 lines
5.5 KiB
Plaintext
@namespace DeepDrftPublic.Client.Controls
|
||
@using DeepDrftModels.DTOs
|
||
|
||
@* Shared presentational queue list. Renders the ordered queue with the current track marked, and
|
||
(when Editable) drag-reorder handles + per-row remove controls. This is the single "view" both
|
||
the docked overlay (17.2) and the embedded panel (17.3) consume — one source, multiple views.
|
||
|
||
Purely presentational: owns no data fetch, no player wiring, and no IQueueService mutation of its
|
||
own. Order changes, removals, and row jumps are surfaced to the parent as EventCallbacks; the
|
||
parent calls the queue engine. It runs during prerender without JS interop (MudDropContainer's
|
||
drag work is client-only and inert when no drag occurs). *@
|
||
|
||
@if (Items is { Count: > 0 })
|
||
{
|
||
@if (Editable)
|
||
{
|
||
<MudDropContainer T="QueueRow" @ref="_dropContainer" Items="Rows" ItemsSelector="@((row, zone) => true)"
|
||
ItemDropped="OnItemDropped" Class="deepdrft-queue-list">
|
||
<ChildContent>
|
||
<MudDropZone T="QueueRow" Identifier="queue" Class="deepdrft-queue-zone" AllowReorder="true"/>
|
||
</ChildContent>
|
||
<ItemRenderer>
|
||
@RenderRow(context)
|
||
</ItemRenderer>
|
||
</MudDropContainer>
|
||
}
|
||
else
|
||
{
|
||
<div class="deepdrft-queue-list">
|
||
@foreach (var row in Rows)
|
||
{
|
||
@RenderRow(row)
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
|
||
@code {
|
||
/// <summary>The ordered tracks to render. Empty/null renders nothing.</summary>
|
||
[Parameter] public IReadOnlyList<TrackDto>? Items { get; set; }
|
||
|
||
/// <summary>
|
||
/// Index of the current track within <see cref="Items"/>, or -1 when none. The matching row is
|
||
/// rendered with a now-playing marker.
|
||
/// </summary>
|
||
[Parameter] public int CurrentIndex { get; set; } = -1;
|
||
|
||
/// <summary>
|
||
/// When true, rows show drag handles and a remove control and reorder is enabled. When false the
|
||
/// list is a read-only display (the embed's fixed-order shared queue).
|
||
/// </summary>
|
||
[Parameter] public bool Editable { get; set; }
|
||
|
||
/// <summary>
|
||
/// Raised when the user reorders a row: <c>(fromIndex, toIndex)</c>. The parent calls
|
||
/// <c>IQueueService.Move</c>. Only fires when <see cref="Editable"/>.
|
||
/// </summary>
|
||
[Parameter] public EventCallback<(int FromIndex, int ToIndex)> OnReorder { get; set; }
|
||
|
||
/// <summary>
|
||
/// Raised when the user removes a row, carrying the row's index. The parent calls
|
||
/// <c>IQueueService.RemoveAt</c>. Only fires when <see cref="Editable"/>.
|
||
/// </summary>
|
||
[Parameter] public EventCallback<int> OnRemove { get; set; }
|
||
|
||
/// <summary>
|
||
/// Raised when the user clicks a row body to jump playback to it, carrying the row's index. The
|
||
/// parent decides whether/how to honour it (e.g. play from that index).
|
||
/// </summary>
|
||
[Parameter] public EventCallback<int> OnJump { get; set; }
|
||
|
||
private MudDropContainer<QueueRow>? _dropContainer;
|
||
|
||
// Index-tagged view rows. The index is the row's position in Items at render time and is the
|
||
// value surfaced to the parent's callbacks — the component never mutates the underlying list.
|
||
private List<QueueRow> Rows =>
|
||
Items is null
|
||
? []
|
||
: Items.Select((track, index) => new QueueRow(index, track)).ToList();
|
||
|
||
private async Task OnItemDropped(MudItemDropInfo<QueueRow> dropInfo)
|
||
{
|
||
var from = dropInfo.Item!.Index;
|
||
var to = dropInfo.IndexInZone;
|
||
// MudDropContainer recomputes the list from the parent's next render; refresh its snapshot so
|
||
// the dragged row snaps back until the parent's Move re-flows the cascaded Items.
|
||
_dropContainer?.Refresh();
|
||
if (from == to) return;
|
||
await OnReorder.InvokeAsync((from, to));
|
||
}
|
||
|
||
private sealed record QueueRow(int Index, TrackDto Track);
|
||
|
||
private RenderFragment RenderRow(QueueRow row) => __builder =>
|
||
{
|
||
var isCurrent = row.Index == CurrentIndex;
|
||
<div class="@($"deepdrft-queue-row{(isCurrent ? " deepdrft-queue-row-current" : "")}")">
|
||
@if (Editable)
|
||
{
|
||
<MudIcon Icon="@Icons.Material.Filled.DragIndicator" Size="Size.Small"
|
||
Class="deepdrft-queue-drag-handle"/>
|
||
}
|
||
<span class="deepdrft-queue-position">@(row.Index + 1)</span>
|
||
<div class="deepdrft-queue-body" @onclick="() => OnJump.InvokeAsync(row.Index)">
|
||
<span class="deepdrft-queue-title">@row.Track.TrackName</span>
|
||
@if (row.Track.Release is { Artist: var artist } && !string.IsNullOrWhiteSpace(artist))
|
||
{
|
||
<span class="deepdrft-queue-artist">@artist</span>
|
||
}
|
||
</div>
|
||
@if (isCurrent)
|
||
{
|
||
<MudIcon Icon="@Icons.Material.Filled.GraphicEq" Size="Size.Small"
|
||
Color="Color.Primary" Class="deepdrft-queue-nowplaying"/>
|
||
}
|
||
@* The current track cannot be removed (OQ3/OQ11): the queue empties only organically as the
|
||
current ends with nothing after it. Suppress the × on the current row only — reorder of the
|
||
current track is still allowed. *@
|
||
@if (Editable && !isCurrent)
|
||
{
|
||
<MudIconButton Icon="@Icons.Material.Filled.Close" Size="Size.Small"
|
||
Class="deepdrft-queue-remove" aria-label="Remove from queue"
|
||
OnClick="() => OnRemove.InvokeAsync(row.Index)"/>
|
||
}
|
||
</div>
|
||
};
|
||
}
|