Files
deepdrft/DeepDrftPublic.Client/Controls/AudioPlayerBar/AudioPlayerBar.razor.cs
T
2026-06-06 16:05:45 -04:00

159 lines
5.1 KiB
C#

using DeepDrftModels.DTOs;
using DeepDrftPublic.Client.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace DeepDrftPublic.Client.Controls.AudioPlayerBar;
public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
{
[CascadingParameter] public IStreamingPlayerService? PlayerService { get; set; }
[Parameter] public bool Fixed { get; set; } = false;
private bool _isMinimized = true;
private bool _isSeeking = false;
private double _seekPosition = 0;
private IStreamingPlayerService? _subscribedService;
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
private bool IsLoading => PlayerService?.IsLoading ?? false;
/// <summary>
/// A track is staged when it has been selected as the current track but not yet loaded into
/// the audio context (the embed's pre-gesture state). The first play click loads + plays it.
/// </summary>
private bool IsStaged => PlayerService is { IsLoaded: false, IsLoading: false, CurrentTrack: not null };
/// <summary>Play is available once a track is loaded, or staged and waiting for the first gesture.</summary>
private bool CanPlay => IsLoaded || IsStaged;
private bool IsStreaming => PlayerService?.CanStartStreaming ?? false;
private bool IsStreamingMode => PlayerService?.IsStreamingMode ?? false;
private double? Duration => PlayerService?.Duration;
private TrackDto? CurrentTrack => PlayerService?.CurrentTrack;
private double Volume => PlayerService?.Volume ?? 0;
private double LoadProgress => PlayerService?.LoadProgress ?? 0;
private string? ErrorMessage => PlayerService?.ErrorMessage;
/// <summary>
/// Display time - shows seek position while dragging, otherwise current playback time.
/// </summary>
private double DisplayTime => _isSeeking ? _seekPosition : (PlayerService?.CurrentTime ?? 0);
private string PlayerModeClass => Fixed ? "player-fixed" : "player-docked";
protected override void OnParametersSet()
{
if (Fixed)
{
_isMinimized = false;
}
// PlayerService is cascaded by AudioPlayerProvider; once it arrives,
// wire our track-selection handler. The provider owns OnStateChanged —
// we intentionally do NOT wrap or replace it. Because the cascade is
// IsFixed, the provider's re-render does NOT reliably re-render this bar
// (it has no incoming parameters that change), so we subscribe to the
// multicast StateChanged side-channel to re-render ourselves.
if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
{
if (_subscribedService != null)
_subscribedService.StateChanged -= OnPlayerStateChanged;
PlayerService.OnTrackSelected = new EventCallback(this, Expand);
PlayerService.StateChanged += OnPlayerStateChanged;
_subscribedService = PlayerService;
}
}
private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
private async Task Expand()
{
if (_isMinimized)
{
_isMinimized = false;
StateHasChanged();
}
}
private async Task TogglePlayPause()
{
if (PlayerService == null) return;
// Gesture-gated start: a staged-but-unloaded track (the embed autoplay path) is loaded on
// the first play click — the user gesture the browser requires before audio can start.
if (IsStaged)
{
await PlayerService.SelectTrackStreaming(PlayerService.CurrentTrack!);
return;
}
await PlayerService.TogglePlayPause();
}
private async Task Stop()
{
if (PlayerService == null) return;
await PlayerService.Stop();
}
private void OnSeekStart()
{
if (PlayerService == null) return;
_isSeeking = true;
_seekPosition = PlayerService.CurrentTime;
}
private void OnSeekChange(double position)
{
_seekPosition = position;
StateHasChanged();
}
private async Task OnSeekEnd(double position)
{
if (PlayerService == null) return;
_isSeeking = false;
await PlayerService.Seek(position);
}
private async Task OnVolumeChange(double volume)
{
if (PlayerService == null) return;
await PlayerService.SetVolume(volume);
}
private void ClearError()
{
PlayerService?.ClearError();
}
private void ToggleMinimized()
{
_isMinimized = !_isMinimized;
StateHasChanged();
}
private async Task Close()
{
if (PlayerService != null && PlayerService.IsLoaded)
{
await PlayerService.Unload();
}
if (!_isMinimized)
{
_isMinimized = true;
StateHasChanged();
}
}
public ValueTask DisposeAsync()
{
if (_subscribedService != null)
{
_subscribedService.StateChanged -= OnPlayerStateChanged;
_subscribedService = null;
}
return ValueTask.CompletedTask;
}
}