fix: enable player controls on load, clear track selection on stop and end-of-track

Add StateChanged multicast event to IPlayerService so AudioPlayerBar and TracksView
re-render themselves without relying on the IsFixed cascade re-render path. Clear
_selectedTrack in TracksView when IsLoaded drops (stop, unload, end-of-track). Set
IsLoaded=false in OnPlaybackEndCallback so end-of-track triggers the same clear path.
Add JS-module readiness probe in AudioInteropService; delete dead TS and buffered C#
path; consolidate GetPlayIcon/FormatTime helpers; fix misleading minimize dock icon.
This commit is contained in:
daniel-c-harvey
2026-06-03 14:30:15 -04:00
parent b8a51e4656
commit 7d49c64a5d
9 changed files with 135 additions and 443 deletions
@@ -15,6 +15,7 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
private double _seekPosition = 0;
private bool _isDesktop = true;
private Guid _viewportSubscriptionId;
private IStreamingPlayerService? _subscribedService;
private bool IsLoaded => PlayerService?.IsLoaded ?? false;
private bool IsLoading => PlayerService?.IsLoading ?? false;
@@ -42,14 +43,23 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
{
// 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. Re-renders propagate
// from the provider via the standard Blazor child render path.
if (PlayerService != null)
// 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)
@@ -58,11 +68,6 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
StateHasChanged();
}
}
private static string FormatTime(double seconds)
{
var timeSpan = TimeSpan.FromSeconds(seconds);
return timeSpan.ToString(timeSpan.TotalHours >= 1 ? @"h\:mm\:ss" : @"m\:ss");
}
private async Task TogglePlayPause()
{
@@ -127,11 +132,6 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
}
}
private string GetPlayIcon()
{
return IsPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
@@ -156,6 +156,11 @@ public partial class AudioPlayerBar : ComponentBase, IAsyncDisposable
public async ValueTask DisposeAsync()
{
if (_subscribedService != null)
{
_subscribedService.StateChanged -= OnPlayerStateChanged;
_subscribedService = null;
}
await BrowserViewportService.UnsubscribeAsync(_viewportSubscriptionId);
}
}