Merge p11-w1-queue-service into dev (P11 11.F: play-queue IQueueService + skip controls)
This commit is contained in:
@@ -21,6 +21,10 @@ else
|
||||
Fixed="Fixed"
|
||||
TogglePlayPause="@TogglePlayPause"
|
||||
Stop="@Stop"
|
||||
HasNext="HasNext"
|
||||
HasPrevious="HasPrevious"
|
||||
SkipNext="@SkipNext"
|
||||
SkipPrevious="@SkipPrevious"
|
||||
Class="transport-zone"/>
|
||||
|
||||
<VolumeZone Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||
[CascadingParameter] public IQueueService? QueueService { get; set; }
|
||||
[Parameter] public bool Fixed { get; set; } = false;
|
||||
|
||||
[Parameter] public EventCallback<bool> OnMinimized { get; set; }
|
||||
@@ -19,6 +20,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
private bool _isSeeking = false;
|
||||
private double _seekPosition = 0;
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
private IQueueService? _subscribedQueue;
|
||||
|
||||
// Spacer-height bridge: the expanded dock is position:fixed, so MainLayout's
|
||||
// spacer reserves its space. We mirror this element's live height into a CSS
|
||||
@@ -48,6 +50,11 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
private double LoadProgress => PlayerService?.LoadProgress ?? 0;
|
||||
private string? ErrorMessage => PlayerService?.ErrorMessage;
|
||||
|
||||
// Skip affordances reflect live queue state. With no queue (null) or an empty queue both are
|
||||
// false, so the buttons sit disabled and the bar behaves exactly as it did before the queue.
|
||||
private bool HasNext => QueueService?.HasNext ?? false;
|
||||
private bool HasPrevious => QueueService?.HasPrevious ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Display time - shows seek position while dragging, otherwise current playback time.
|
||||
/// </summary>
|
||||
@@ -76,10 +83,35 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
PlayerService.StateChanged += OnPlayerStateChanged;
|
||||
_subscribedService = PlayerService;
|
||||
}
|
||||
|
||||
// The queue cascade is also IsFixed, so re-render the skip affordances off its own
|
||||
// change signal — same posture as the player StateChanged subscription above.
|
||||
if (QueueService != null && !ReferenceEquals(QueueService, _subscribedQueue))
|
||||
{
|
||||
if (_subscribedQueue != null)
|
||||
_subscribedQueue.QueueChanged -= OnQueueChanged;
|
||||
|
||||
QueueService.QueueChanged += OnQueueChanged;
|
||||
_subscribedQueue = QueueService;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
private void OnQueueChanged() => InvokeAsync(StateHasChanged);
|
||||
|
||||
private async Task SkipNext()
|
||||
{
|
||||
if (QueueService == null) return;
|
||||
await QueueService.Next();
|
||||
}
|
||||
|
||||
private async Task SkipPrevious()
|
||||
{
|
||||
if (QueueService == null) return;
|
||||
await QueueService.Previous();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// Only the docked, expanded shape needs a spacer: the Fixed embed is
|
||||
|
||||
@@ -2,12 +2,25 @@
|
||||
@using DeepDrftPublic.Client.Controls
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@if (!Fixed)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SkipPrevious"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
OnClick="@SkipPrevious"
|
||||
Disabled="!HasPrevious"/>
|
||||
}
|
||||
<PlayStateIcon Size="Size.Large"
|
||||
Color="Color.Primary"
|
||||
Disabled="!CanPlay"
|
||||
OnToggle="@TogglePlayPause"/>
|
||||
@if (!Fixed)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SkipNext"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
OnClick="@SkipNext"
|
||||
Disabled="!HasNext"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Stop"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Large"
|
||||
|
||||
@@ -16,4 +16,13 @@ public partial class PlayerControls : ComponentBase
|
||||
[Parameter] public bool Fixed { get; set; } = false;
|
||||
[Parameter] public required EventCallback TogglePlayPause { get; set; }
|
||||
[Parameter] public required EventCallback Stop { get; set; }
|
||||
|
||||
/// <summary>Whether the queue has a track to skip forward to. Drives the skip-next affordance.</summary>
|
||||
[Parameter] public bool HasNext { get; set; }
|
||||
|
||||
/// <summary>Whether the queue has a track to step back to. Drives the skip-previous affordance.</summary>
|
||||
[Parameter] public bool HasPrevious { get; set; }
|
||||
|
||||
[Parameter] public EventCallback SkipNext { get; set; }
|
||||
[Parameter] public EventCallback SkipPrevious { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
CanPlay="CanPlay"
|
||||
Fixed="Fixed"
|
||||
TogglePlayPause="TogglePlayPause"
|
||||
Stop="Stop"/>
|
||||
Stop="Stop"
|
||||
HasNext="HasNext"
|
||||
HasPrevious="HasPrevious"
|
||||
SkipNext="SkipNext"
|
||||
SkipPrevious="SkipPrevious"/>
|
||||
@if (IsLoading && !IsStreaming)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Tertiary"
|
||||
|
||||
@@ -14,5 +14,9 @@ public partial class PlayerTransportZone : ComponentBase
|
||||
[Parameter] public bool Fixed { get; set; } = false;
|
||||
[Parameter] public EventCallback TogglePlayPause { get; set; }
|
||||
[Parameter] public EventCallback Stop { get; set; }
|
||||
[Parameter] public bool HasNext { get; set; }
|
||||
[Parameter] public bool HasPrevious { get; set; }
|
||||
[Parameter] public EventCallback SkipNext { get; set; }
|
||||
[Parameter] public EventCallback SkipPrevious { get; set; }
|
||||
[Parameter] public string? Class { get; set; }
|
||||
}
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
If instance swapping at runtime is ever needed, change IsFixed to false (adds subscription
|
||||
overhead on every parent re-render, but allows children to see the new reference). *@
|
||||
<CascadingValue Value="@(_audioPlayerService)" IsFixed="true">
|
||||
@ChildContent
|
||||
<CascadingValue Value="@(_queueService)" IsFixed="true">
|
||||
@ChildContent
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
@@ -12,6 +12,7 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
[Inject] public required ILogger<StreamingAudioPlayerService> Logger { get; set; }
|
||||
|
||||
private IStreamingPlayerService? _audioPlayerService;
|
||||
private QueueService? _queueService;
|
||||
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
@@ -29,6 +30,13 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
// Children must not wrap or replace this callback.
|
||||
_audioPlayerService.OnStateChanged = new EventCallback(this, () => InvokeAsync(StateHasChanged));
|
||||
// OnTrackSelected will be set by individual child components that need it
|
||||
|
||||
// The queue orchestrates above the single-slot player. The player is not DI-registered
|
||||
// (constructed here), so the queue binds to it via Attach rather than constructor injection —
|
||||
// no construction cycle, no IServiceProvider. Cascaded alongside the player so the bar and a
|
||||
// future up-next panel both read it.
|
||||
_queueService = new QueueService();
|
||||
_queueService.Attach(_audioPlayerService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -38,6 +46,11 @@ public partial class AudioPlayerProvider : ComponentBase, IAsyncDisposable
|
||||
/// </summary>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Dispose the queue first so it unsubscribes from the player's TrackEnded before the
|
||||
// player tears down.
|
||||
_queueService?.Dispose();
|
||||
_queueService = null;
|
||||
|
||||
if (_audioPlayerService is IAsyncDisposable disposable)
|
||||
{
|
||||
await disposable.DisposeAsync();
|
||||
|
||||
Reference in New Issue
Block a user