Merge branch 'audioplayer-unified' into dev
This commit is contained in:
@@ -12,10 +12,7 @@ else
|
|||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="player-inner-container">
|
||||||
<MudPaper Elevation="8" Class="player-surface pa-3">
|
<MudPaper Elevation="8" Class="player-surface pa-3">
|
||||||
|
|
||||||
@if (_isDesktop)
|
<div class="player-layout">
|
||||||
{
|
|
||||||
@* Desktop Layout *@
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3" Class="player-row">
|
|
||||||
<PlayerTransportZone IsPlaying="IsPlaying"
|
<PlayerTransportZone IsPlaying="IsPlaying"
|
||||||
IsLoaded="IsLoaded"
|
IsLoaded="IsLoaded"
|
||||||
IsLoading="IsLoading"
|
IsLoading="IsLoading"
|
||||||
@@ -25,7 +22,9 @@ else
|
|||||||
Duration="Duration"
|
Duration="Duration"
|
||||||
TogglePlayPause="@TogglePlayPause"
|
TogglePlayPause="@TogglePlayPause"
|
||||||
Stop="@Stop"
|
Stop="@Stop"
|
||||||
Class="controls-left"/>
|
Class="transport-zone"/>
|
||||||
|
|
||||||
|
<VolumeControls Volume="@Volume" VolumeChanged="@OnVolumeChange"/>
|
||||||
|
|
||||||
<PlayerSeekZone DisplayTime="DisplayTime"
|
<PlayerSeekZone DisplayTime="DisplayTime"
|
||||||
Duration="Duration"
|
Duration="Duration"
|
||||||
@@ -33,43 +32,8 @@ else
|
|||||||
OnSeekStart="@OnSeekStart"
|
OnSeekStart="@OnSeekStart"
|
||||||
OnSeekEnd="@OnSeekEnd"
|
OnSeekEnd="@OnSeekEnd"
|
||||||
OnSeekChange="@OnSeekChange"
|
OnSeekChange="@OnSeekChange"
|
||||||
Class="flex-grow-1"/>
|
Class="seek-zone"/>
|
||||||
|
|
||||||
<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>
|
</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>
|
|
||||||
}
|
|
||||||
|
|
||||||
@* Minimize / close — positioned absolutely top-right *@
|
@* Minimize / close — positioned absolutely top-right *@
|
||||||
<PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
|
<PlayerWindowControls OnMinimize="@ToggleMinimized" OnClose="@Close"/>
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
using DeepDrftPublic.Client.Services;
|
using DeepDrftPublic.Client.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using MudBlazor.Services;
|
|
||||||
|
|
||||||
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
|
||||||
|
|
||||||
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
||||||
{
|
{
|
||||||
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
|
||||||
[Inject] public required IBrowserViewportService BrowserViewportService { get; set; }
|
|
||||||
|
|
||||||
private bool _isMinimized = true;
|
private bool _isMinimized = true;
|
||||||
private bool _isSeeking = false;
|
private bool _isSeeking = false;
|
||||||
private double _seekPosition = 0;
|
private double _seekPosition = 0;
|
||||||
private bool _isDesktop = true;
|
|
||||||
private Guid _viewportSubscriptionId;
|
|
||||||
private IStreamingPlayerService? _subscribedService;
|
private IStreamingPlayerService? _subscribedService;
|
||||||
|
|
||||||
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
|
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
|
||||||
@@ -132,35 +128,13 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
public ValueTask DisposeAsync()
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
if (_subscribedService != null)
|
if (_subscribedService != null)
|
||||||
{
|
{
|
||||||
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||||
_subscribedService = null;
|
_subscribedService = null;
|
||||||
}
|
}
|
||||||
await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId);
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,3 +71,30 @@
|
|||||||
height: 160px;
|
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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,14 @@
|
|||||||
/* Stable minimum width so the transport cluster doesn't reflow */
|
/* Stable minimum width so the transport cluster doesn't reflow */
|
||||||
::deep .controls-left {
|
.transport-zone {
|
||||||
min-width: 200px;
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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) {
|
||||||
|
.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 double[] _spectrumData = Array.Empty<double>();
|
||||||
private bool _isAnimating = false;
|
private bool _isAnimating = false;
|
||||||
private string? _playerId;
|
private string? _playerId;
|
||||||
|
private IStreamingPlayerService? _subscribedService;
|
||||||
|
|
||||||
private bool IsVisible => (PlayerService?.IsPlaying ?? false) || (PlayerService?.IsPaused ?? false) || _isAnimating;
|
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()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
// Provider re-renders cascade down to children and re-run OnParametersSet.
|
// The cascade is IsFixed, so the provider's re-renders do NOT re-run
|
||||||
// Pick up the player id once the cascade arrives, then drive animation
|
// OnParametersSet here, and this component has no incoming parameters
|
||||||
// state from the parent's current IsPlaying — no callback wrapping needed.
|
// 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)
|
if (_playerId == null && PlayerService is AudioPlayerService baseService)
|
||||||
{
|
{
|
||||||
_playerId = baseService.PlayerId;
|
_playerId = baseService.PlayerId;
|
||||||
@@ -36,6 +48,15 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
await UpdateAnimationState();
|
await UpdateAnimationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPlayerStateChanged() => InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
if (_playerId == null && PlayerService is AudioPlayerService baseService)
|
||||||
|
{
|
||||||
|
_playerId = baseService.PlayerId;
|
||||||
|
}
|
||||||
|
await UpdateAnimationState();
|
||||||
|
});
|
||||||
|
|
||||||
private async Task UpdateAnimationState()
|
private async Task UpdateAnimationState()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
|
if (string.IsNullOrEmpty(_playerId) || PlayerService == null) return;
|
||||||
@@ -93,6 +114,11 @@ public partial class SpectrumVisualizer : ComponentBase, IAsyncDisposable
|
|||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
if (_subscribedService != null)
|
||||||
|
{
|
||||||
|
_subscribedService.StateChanged -= OnPlayerStateChanged;
|
||||||
|
_subscribedService = null;
|
||||||
|
}
|
||||||
await StopAnimation();
|
await StopAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user