diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor index 77d36ab..d07f3dd 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor @@ -12,64 +12,28 @@ else - @if (_isDesktop) - { - @* Desktop Layout *@ - - +
+ - + - - - } - else - { - @* Mobile Layout *@ -
-
-
- - @if (IsLoading && !IsStreaming) - { - - } -
- - -
- - -
- } + +
@* Minimize / close — positioned absolutely top-right *@ diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs index c8dc0be..ae3de8c 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs @@ -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; } } \ No newline at end of file diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css index d48bbfa..38848b5 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.css @@ -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; } +} diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/PlayerTransportZone.razor.css b/DeepDrftPublic.Client/Controls/AudioPlayerBar/PlayerTransportZone.razor.css index eb8607b..d1cfa96 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/PlayerTransportZone.razor.css +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/PlayerTransportZone.razor.css @@ -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; + } +} diff --git a/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs b/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs index 6689089..bb14db0 100644 --- a/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs +++ b/DeepDrftPublic.Client/Controls/AudioPlayerBar/SpectrumVisualizer.razor.cs @@ -15,6 +15,7 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable private double[] _spectrumData = Array.Empty(); 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(); } }