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.
153 lines
8.2 KiB
C#
153 lines
8.2 KiB
C#
using DeepDrftModels.DTOs;
|
|
|
|
namespace DeepDrftPublic.Client.Services;
|
|
|
|
/// <summary>
|
|
/// Orchestrates ordered playback ("what plays next") <em>above</em> the single-slot
|
|
/// <see cref="IStreamingPlayerService"/>. The player stays a single-track device; the queue owns the
|
|
/// track list, the current position, skip-forward/back, and auto-advance on natural track end. It
|
|
/// drives playback solely through the player's existing <see cref="IStreamingPlayerService.SelectTrackStreaming"/>
|
|
/// — it adds no new playback semantics.
|
|
///
|
|
/// <para>
|
|
/// Extension posture (open/closed): future shuffle, repeat modes, reordering, and persistence are
|
|
/// expected. They are additive — a shuffle/repeat strategy slots in behind <see cref="Next"/>/
|
|
/// <see cref="Previous"/> as the "which index is next" decision; reordering mutates <see cref="Items"/>
|
|
/// and re-emits <see cref="QueueChanged"/>; persistence snapshots/restores <see cref="Items"/> +
|
|
/// <see cref="CurrentIndex"/>. None of those require changing this interface's existing members, only
|
|
/// adding new ones — so consumers written against today's surface keep working.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// With an empty queue (<see cref="CurrentIndex"/> == -1) the queue is dormant: it drives nothing and
|
|
/// auto-advances nothing, so direct single-track play through the player behaves exactly as it did
|
|
/// before the queue existed.
|
|
/// </para>
|
|
/// </summary>
|
|
public interface IQueueService
|
|
{
|
|
/// <summary>The ordered tracks currently queued. Empty when nothing is enqueued.</summary>
|
|
IReadOnlyList<TrackDto> Items { get; }
|
|
|
|
/// <summary>
|
|
/// Index into <see cref="Items"/> of the track the queue considers current, or -1 when the
|
|
/// queue is empty. Always a valid index into <see cref="Items"/> when non-negative.
|
|
/// </summary>
|
|
int CurrentIndex { get; }
|
|
|
|
/// <summary>The current track, or null when the queue is empty.</summary>
|
|
TrackDto? Current { get; }
|
|
|
|
/// <summary>
|
|
/// True when the queue has been loaded via <see cref="Arm"/> but no track has streamed yet —
|
|
/// the embed's pre-gesture state. Set by <see cref="Arm"/>; cleared the moment playback actually
|
|
/// starts (<see cref="Start"/>/<see cref="PlayRelease"/>/<see cref="Next"/>/<see cref="Previous"/>)
|
|
/// or on <see cref="Clear"/>. The player bar reads this to route the first play gesture through
|
|
/// <see cref="Start"/> (which begins the armed release) rather than streaming the staged track alone.
|
|
/// </summary>
|
|
bool IsArmed { get; }
|
|
|
|
/// <summary>True when there is a track after <see cref="CurrentIndex"/> to advance to.</summary>
|
|
bool HasNext { get; }
|
|
|
|
/// <summary>True when there is a track before <see cref="CurrentIndex"/> to step back to.</summary>
|
|
bool HasPrevious { get; }
|
|
|
|
/// <summary>
|
|
/// Raised whenever the queue's contents or current position change. The player bar subscribes
|
|
/// to re-render its skip-forward/back affordances. Fires on enqueue, advance, step-back, and clear.
|
|
/// </summary>
|
|
event Action? QueueChanged;
|
|
|
|
/// <summary>
|
|
/// Replaces the queue with <paramref name="tracks"/> (in the order given) and begins streaming
|
|
/// the track at <paramref name="startIndex"/>. This is the "play album" entry point the Cuts
|
|
/// detail page consumes: pass the release's tracks in ordinal order. A header Play uses
|
|
/// <c>startIndex: 0</c>; a mid-album row play passes that row's index so the queue continues to
|
|
/// the end from there. No-op when <paramref name="tracks"/> is empty.
|
|
/// </summary>
|
|
Task PlayRelease(IEnumerable<TrackDto> tracks, int startIndex = 0);
|
|
|
|
/// <summary>
|
|
/// Loads <paramref name="tracks"/> as the queue and sets the current position to index 0 WITHOUT
|
|
/// streaming anything — the queue is "armed". This is the embed's prerender-safe entry point: it
|
|
/// performs no JS interop, so it runs identically during prerender and after WASM boot. The first
|
|
/// play gesture (see <see cref="IsArmed"/>) then starts playback via <see cref="Start"/>, which
|
|
/// keeps the loaded release queued so it advances through its tracks. No-op when
|
|
/// <paramref name="tracks"/> is empty (the queue stays empty and disarmed).
|
|
/// </summary>
|
|
void Arm(IEnumerable<TrackDto> tracks);
|
|
|
|
/// <summary>
|
|
/// Begins playback of an armed queue (see <see cref="Arm"/>): streams the current track and clears
|
|
/// <see cref="IsArmed"/>, leaving the loaded list and position intact so auto-advance carries on
|
|
/// through the release. This is the first-gesture entry point the embed bar calls. No-op (and stays
|
|
/// disarmed) when the queue is not armed or is empty — so it never double-streams or disturbs a
|
|
/// queue already playing.
|
|
/// </summary>
|
|
Task Start();
|
|
|
|
/// <summary>
|
|
/// Appends a track to the end of the queue without changing what is currently playing.
|
|
/// Into a dormant queue (<see cref="CurrentIndex"/> == -1) the append leaves a coherent
|
|
/// <see cref="CurrentIndex"/> (the first appended track) so a subsequent play/skip is correct —
|
|
/// but it does NOT begin playback (add is not play). Interop-free; safe during prerender.
|
|
/// </summary>
|
|
void Enqueue(TrackDto track);
|
|
|
|
/// <summary>
|
|
/// Appends tracks to the end of the queue without changing what is currently playing.
|
|
/// Into a dormant queue (<see cref="CurrentIndex"/> == -1) the append leaves a coherent
|
|
/// <see cref="CurrentIndex"/> (the first appended track) so a subsequent play/skip is correct —
|
|
/// but it does NOT begin playback (add is not play). Interop-free; safe during prerender.
|
|
/// </summary>
|
|
void EnqueueRange(IEnumerable<TrackDto> tracks);
|
|
|
|
/// <summary>
|
|
/// Reorders the queue, moving the track at <paramref name="fromIndex"/> to
|
|
/// <paramref name="toIndex"/>, and re-emits <see cref="QueueChanged"/>. Adjusts
|
|
/// <see cref="CurrentIndex"/> so the <em>same track</em> stays current across the move — it does
|
|
/// not restart, re-stream, or interrupt the currently-playing track. Interop-free; safe during
|
|
/// prerender. No-op (no throw, no <see cref="QueueChanged"/>) when either index is out of range
|
|
/// or the indices are equal.
|
|
/// </summary>
|
|
void Move(int fromIndex, int toIndex);
|
|
|
|
/// <summary>
|
|
/// Removes the track at <paramref name="index"/> and re-emits <see cref="QueueChanged"/>. Does
|
|
/// not touch playback (the player stays a single-track device): removing the current track does
|
|
/// not stop it — the playing track runs to its natural end while <see cref="CurrentIndex"/>
|
|
/// resolves to the new occupant of that slot (the next track) so the next auto-advance/skip is
|
|
/// coherent. Removing a track before the current decrements <see cref="CurrentIndex"/> (the same
|
|
/// track stays current); removing after the current leaves it unchanged. Removing the last
|
|
/// remaining track empties the queue (<see cref="CurrentIndex"/> == -1, dormant). Interop-free;
|
|
/// safe during prerender. No-op (no throw, no <see cref="QueueChanged"/>) when
|
|
/// <paramref name="index"/> is out of range.
|
|
/// </summary>
|
|
void RemoveAt(int index);
|
|
|
|
/// <summary>
|
|
/// Advances to the next track and streams it. No-op when <see cref="HasNext"/> is false.
|
|
/// </summary>
|
|
Task Next();
|
|
|
|
/// <summary>
|
|
/// Steps back to the previous track and streams it. No-op when <see cref="HasPrevious"/> is false.
|
|
/// </summary>
|
|
Task Previous();
|
|
|
|
/// <summary>Empties the queue and resets the position. Does not stop the player.</summary>
|
|
void Clear();
|
|
|
|
/// <summary>
|
|
/// Empties the up-next while keeping the currently-playing track: removes every item except
|
|
/// <see cref="Current"/>, leaving it as the sole remaining item at <see cref="CurrentIndex"/> == 0,
|
|
/// and re-emits <see cref="QueueChanged"/>. Unlike <see cref="Clear"/> (which empties everything and
|
|
/// goes dormant), this preserves what is playing — the player is never stopped and the current track
|
|
/// stays queued, so playback continues uninterrupted while the rest of the queue is discarded.
|
|
/// Interop-free; safe during prerender. No-op (no throw, no <see cref="QueueChanged"/>) when the queue
|
|
/// is empty/dormant or already holds only the current track.
|
|
/// </summary>
|
|
void ClearUpcoming();
|
|
}
|