Files
deepdrft/DeepDrftPublic.Client/Controls/QueueList.razor
T

125 lines
5.3 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"/>
}
@if (Editable)
{
<MudIconButton Icon="@Icons.Material.Filled.Close" Size="Size.Small"
Class="deepdrft-queue-remove" aria-label="Remove from queue"
OnClick="() => OnRemove.InvokeAsync(row.Index)"/>
}
</div>
};
}