Unify AudioPlayerBar to one responsive CSS layout and fix SpectrumVisualizer startup via StateChanged subscription
This commit is contained in:
@@ -12,64 +12,28 @@ else
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
||||
<MudPaper Elevation="8" Class="player-surface pa-3">
|
||||
|
||||
@if (_isDesktop)
|
||||
{
|
||||
@* Desktop Layout *@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Class="player-row">
|
||||
<PlayerTransportZone IsPlaying="IsPlaying"
|
||||
IsLoaded="IsLoaded"
|
||||
IsLoading="IsLoading"
|
||||
IsStreaming="IsStreaming"
|
||||
LoadProgress="LoadProgress"
|
||||
DisplayTime="DisplayTime"
|
||||
Duration="Duration"
|
||||
TogglePlayPause="@TogglePlayPause"
|
||||
Stop="@Stop"
|
||||
Class="controls-left"/>
|
||||
<div class="player-layout">
|
||||
<PlayerTransportZone IsPlaying="IsPlaying"
|
||||
IsLoaded="IsLoaded"
|
||||
IsLoading="IsLoading"
|
||||
IsStreaming="IsStreaming"
|
||||
LoadProgress="LoadProgress"
|
||||
DisplayTime="DisplayTime"
|
||||
Duration="Duration"
|
||||
TogglePlayPause="@TogglePlayPause"
|
||||
Stop="@Stop"
|
||||
Class="transport-zone"/>
|
||||
|
||||
<PlayerSeekZone DisplayTime="DisplayTime"
|
||||
Duration="Duration"
|
||||
CanSeek="CanSeek"
|
||||
OnSeekStart="@OnSeekStart"
|
||||
OnSeekEnd="@OnSeekEnd"
|
||||
OnSeekChange="@OnSeekChange"
|
||||
Class="flex-grow-1"/>
|
||||
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
|
||||
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
</MudStack>
|
||||
}
|
||||
else
|
||||
{
|
||||
@* Mobile Layout *@
|
||||
<div>
|
||||
<div class="d-flex align-center justify-space-between mb-3">
|
||||
<div class="d-flex align-center gap-2">
|
||||
<PlayerControls IsPlaying="IsPlaying"
|
||||
IsLoaded="IsLoaded"
|
||||
TogglePlayPause="@TogglePlayPause"
|
||||
Stop="@Stop"/>
|
||||
@if (IsLoading && !IsStreaming)
|
||||
{
|
||||
<MudProgressCircular Color="Color.Tertiary"
|
||||
Size="Size.Small"
|
||||
Max="1D"
|
||||
Value="@LoadProgress"
|
||||
Indeterminate="@(LoadProgress == 0)"/>
|
||||
}
|
||||
</div>
|
||||
<TimestampLabel CurrentTime="DisplayTime" Duration="Duration"/>
|
||||
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||
</div>
|
||||
|
||||
<PlayerSeekZone DisplayTime="DisplayTime"
|
||||
Duration="Duration"
|
||||
CanSeek="CanSeek"
|
||||
OnSeekStart="@OnSeekStart"
|
||||
OnSeekEnd="@OnSeekEnd"
|
||||
OnSeekChange="@OnSeekChange"
|
||||
Class="flex-grow-1"/>
|
||||
</div>
|
||||
}
|
||||
<PlayerSeekZone DisplayTime="DisplayTime"
|
||||
Duration="Duration"
|
||||
CanSeek="CanSeek"
|
||||
OnSeekStart="@OnSeekStart"
|
||||
OnSeekEnd="@OnSeekEnd"
|
||||
OnSeekChange="@OnSeekChange"
|
||||
Class="seek-zone"/>
|
||||
</div>
|
||||
|
||||
@* Minimize / close — positioned absolutely top-right *@
|
||||
<PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
using DeepDrftPublic.Client.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Services;
|
||||
|
||||
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||
|
||||
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
{
|
||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||
[Inject] public required IBrowserViewportService BrowserViewportService { get; set; }
|
||||
|
||||
private bool _isMinimized = true;
|
||||
private bool _isSeeking = false;
|
||||
private double _seekPosition = 0;
|
||||
private bool _isDesktop = true;
|
||||
private Guid _viewportSubscriptionId;
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
|
||||
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
|
||||
@@ -132,35 +128,13 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var breakpoint = await BrowserViewportService.GetCurrentBreakpointAsync();
|
||||
_isDesktop = breakpoint >= Breakpoint.Sm;
|
||||
|
||||
_viewportSubscriptionId = Guid.NewGuid();
|
||||
await BrowserViewportService.SubscribeAsync(
|
||||
_viewportSubscriptionId,
|
||||
args =>
|
||||
{
|
||||
_isDesktop = args.Breakpoint >= Breakpoint.Sm;
|
||||
InvokeAsync(StateHasChanged);
|
||||
},
|
||||
new ResizeOptions { NotifyOnBreakpointOnly = true },
|
||||
fireImmediately: true);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (_subscribedService != null)
|
||||
{
|
||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||
_subscribedService = null;
|
||||
}
|
||||
await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -71,3 +71,30 @@
|
||||
height: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Unified responsive player layout.
|
||||
Wide and narrow shapes are pure CSS — no runtime breakpoint subscription.
|
||||
Children are targeted by their stable classes (transport-zone, volume-controls,
|
||||
seek-zone) rather than positional nth-child, since all three render a .mud-stack
|
||||
root and positional selectors would be fragile. */
|
||||
.player-layout {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-right: 2.5rem; /* clear the abs-positioned PlayerWindowControls */
|
||||
}
|
||||
|
||||
/* Wide (>= 600px): single row, seek zone grows and sits between transport and volume */
|
||||
@media (min-width: 600px) {
|
||||
::deep .transport-zone { order: 1; }
|
||||
::deep .seek-zone { order: 2; flex-grow: 1; flex-basis: 0; }
|
||||
::deep .volume-controls { order: 3; }
|
||||
}
|
||||
|
||||
/* Narrow (< 600px): transport + volume on top row, seek full-width below */
|
||||
@media (max-width: 599.98px) {
|
||||
::deep .transport-zone { order: 1; }
|
||||
::deep .volume-controls { order: 2; }
|
||||
::deep .seek-zone { flex-basis: 100%; order: 3; }
|
||||
}
|
||||
|
||||
@@ -2,3 +2,13 @@
|
||||
::deep .controls-left {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Narrow (< 600px): lay the transport root horizontally (controls beside
|
||||
timestamp) so it fits on one line next to VolumeControls. Scoped to the
|
||||
root .transport-zone stack so the nested controls stack is untouched. */
|
||||
@media (max-width: 599.98px) {
|
||||
::deep .transport-zone {
|
||||
flex-direction: row !important;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
||||
private double[] _spectrumData = Array.Empty<double>();
|
||||
private bool _isAnimating = false;
|
||||
private string? _playerId;
|
||||
private IStreamingPlayerService? _subscribedService;
|
||||
|
||||
private bool IsVisible => (PlayerService?.IsPlaying ?? false) || (PlayerService?.IsPaused ?? false) || _isAnimating;
|
||||
|
||||
@@ -25,9 +26,20 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Provider re-renders cascade down to children and re-run OnParametersSet.
|
||||
// Pick up the player id once the cascade arrives, then drive animation
|
||||
// state from the parent's current IsPlaying — no callback wrapping needed.
|
||||
// The cascade is IsFixed, so the provider's re-renders do NOT re-run
|
||||
// OnParametersSet here, and this component has no incoming parameters
|
||||
// that change. Subscribe to the multicast StateChanged side-channel so
|
||||
// animation state stays correct independent of parent re-renders —
|
||||
// notably when the bar expands while a track is already playing.
|
||||
if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
|
||||
{
|
||||
if (_subscribedService != null)
|
||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||
|
||||
PlayerService.StateChanged += OnPlayerStateChanged;
|
||||
_subscribedService = PlayerService;
|
||||
}
|
||||
|
||||
if (_playerId == null && PlayerService is AudioPlayerService baseService)
|
||||
{
|
||||
_playerId = baseService.PlayerId;
|
||||
@@ -36,6 +48,15 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
||||
await UpdateAnimationState();
|
||||
}
|
||||
|
||||
private void OnPlayerStateChanged() => InvokeAsync(async () =>
|
||||
{
|
||||
if (_playerId == null && PlayerService is AudioPlayerService baseService)
|
||||
{
|
||||
_playerId = baseService.PlayerId;
|
||||
}
|
||||
await UpdateAnimationState();
|
||||
});
|
||||
|
||||
private async Task UpdateAnimationState()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
|
||||
@@ -93,6 +114,11 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_subscribedService != null)
|
||||
{
|
||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||
_subscribedService = null;
|
||||
}
|
||||
await StopAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user