Reflect real playback state on gallery cards and toggle pause/resume

Add IsPaused/OnPause to TrackCard, make TracksGallery controlled, and
drive the active track from PlayerService.CurrentTrack as the single
source of truth.
This commit is contained in:
daniel-c-harvey
2026-06-06 09:51:31 -04:00
parent d055c2a548
commit 766e98fd2b
5 changed files with 54 additions and 40 deletions
+5 -2
View File
@@ -8,8 +8,11 @@
{ {
<div class="tracks-content"> <div class="tracks-content">
<TracksGallery Tracks="@ViewModel.Page.Items" <TracksGallery Tracks="@ViewModel.Page.Items"
SelectedTrack="_selectedTrack" ActiveTrack="@PlayerService.CurrentTrack"
SelectedTrackChanged="@PlayTrack"/> IsPlaying="@PlayerService.IsPlaying"
IsPaused="@PlayerService.IsPaused"
OnPlay="@PlayTrack"
OnPause="@PauseTrack"/>
</div> </div>
<div class="tracks-footer py-4"> <div class="tracks-footer py-4">
<MudPagination Count="@ViewModel.Page.TotalPages" <MudPagination Count="@ViewModel.Page.TotalPages"
+17 -23
View File
@@ -14,7 +14,6 @@ public partial class TracksView : ComponentBase, IDisposable
[Inject] public required PersistentComponentState PersistentState { get; set; } [Inject] public required PersistentComponentState PersistentState { get; set; }
[CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; } [CascadingParameter] public required IStreamingPlayerService PlayerService { get; set; }
private TrackDto? _selectedTrack = null;
private IStreamingPlayerService? _subscribedService; private IStreamingPlayerService? _subscribedService;
private PersistingComponentStateSubscription _persistingSubscription; private PersistingComponentStateSubscription _persistingSubscription;
@@ -41,11 +40,11 @@ public partial class TracksView : ComponentBase, IDisposable
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
// The Stop/Close buttons on the player bar reset the player directly, // The gallery's per-card icons read off the player's live state (CurrentTrack /
// bypassing PlayTrack — so the gallery's selection must follow player // IsPlaying / IsPaused), which mutates outside this component's render path:
// state rather than only its own clicks. Subscribe to the multicast // the player bar's play/pause/stop/close all change it directly. The cascade is
// side-channel (the cascade is IsFixed, so provider re-renders don't // IsFixed, so the provider's re-render never reaches us — subscribe to the
// reach us) and clear the highlight when nothing is loaded. // multicast side-channel and re-render on every state change.
if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService)) if (PlayerService != null && !ReferenceEquals(PlayerService, _subscribedService))
{ {
if (_subscribedService != null) if (_subscribedService != null)
@@ -56,17 +55,7 @@ public partial class TracksView : ComponentBase, IDisposable
} }
} }
private void OnPlayerStateChanged() private void OnPlayerStateChanged() => InvokeAsync(StateHasChanged);
{
// Sync the gallery selection to the player. When the player is no longer
// loaded (stopped/closed/ended) drop the highlight; guard against a
// redundant re-render when nothing actually changed.
if (!PlayerService.IsLoaded && _selectedTrack != null)
{
_selectedTrack = null;
InvokeAsync(StateHasChanged);
}
}
private Task PersistTracks() private Task PersistTracks()
{ {
@@ -88,20 +77,25 @@ public partial class TracksView : ComponentBase, IDisposable
} }
} }
private async Task PlayTrack(TrackDto? track) private async Task PlayTrack(TrackDto track)
{ {
if (track == null && _selectedTrack == null || track?.Id == _selectedTrack?.Id) return; // Resume the current track if it's merely paused; otherwise stream the new selection.
if (PlayerService.CurrentTrack?.Id == track.Id && PlayerService.IsPaused)
if (track is null)
{ {
await PlayerService.Unload(); await PlayerService.TogglePlayPause();
} }
else else
{ {
await PlayerService.SelectTrack(track); await PlayerService.SelectTrack(track);
} }
}
_selectedTrack = track; private async Task PauseTrack(TrackDto track)
{
if (PlayerService.CurrentTrack?.Id == track.Id && PlayerService.IsPlaying)
{
await PlayerService.TogglePlayPause();
}
} }
public void Dispose() public void Dispose()
@@ -8,13 +8,24 @@ public partial class TrackCard : ComponentBase
{ {
[Parameter] public required TrackDto TrackModel { get; set; } [Parameter] public required TrackDto TrackModel { get; set; }
[Parameter] public EventCallback<TrackDto> OnPlay { get; set; } [Parameter] public EventCallback<TrackDto> OnPlay { get; set; }
[Parameter] public EventCallback<TrackDto> OnPause { get; set; }
[Parameter] public bool IsPlaying { get; set; } = false; [Parameter] public bool IsPlaying { get; set; } = false;
[Parameter] public bool IsPaused { get; set; } = false;
private string PlayPauseIcon => IsPlaying ? Icons.Material.Filled.MusicNote : Icons.Material.Filled.PlayArrow; // Pause only when actively playing; every other state (idle, paused) reads as "press to play".
private bool IsActivelyPlaying => IsPlaying && !IsPaused;
private string PlayPauseIcon =>
IsActivelyPlaying ? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow;
private async Task PlayClick() private async Task PlayClick()
{ {
if (!IsPlaying && OnPlay.HasDelegate) if (IsActivelyPlaying)
{
if (OnPause.HasDelegate)
await OnPause.InvokeAsync(TrackModel);
}
else if (OnPlay.HasDelegate)
{ {
await OnPlay.InvokeAsync(TrackModel); await OnPlay.InvokeAsync(TrackModel);
} }
@@ -4,7 +4,11 @@
{ {
<MudItem xs="12" sm="6" md="4" lg="3" xl="3"> <MudItem xs="12" sm="6" md="4" lg="3" xl="3">
<div class="deepdrft-track-gallery-item-center"> <div class="deepdrft-track-gallery-item-center">
<TrackCard TrackModel="@track" IsPlaying="@(track.Id == SelectedTrack?.Id)" OnPlay="@HandlePlayClick"/> <TrackCard TrackModel="@track"
IsPlaying="@(IsPlaying && ActiveTrack?.Id == track.Id)"
IsPaused="@(IsPaused && ActiveTrack?.Id == track.Id)"
OnPlay="@HandlePlayClick"
OnPause="@HandlePauseClick"/>
</div> </div>
</MudItem> </MudItem>
} }
@@ -6,18 +6,20 @@ namespace DeepDrftShared.Client.Components;
public partial class TracksGallery : ComponentBase public partial class TracksGallery : ComponentBase
{ {
[Parameter] public IEnumerable<TrackDto> Tracks { get; set; } = []; [Parameter] public IEnumerable<TrackDto> Tracks { get; set; } = [];
[Parameter] public TrackDto? SelectedTrack { get; set; }
[Parameter] public EventCallback<TrackDto?> SelectedTrackChanged { get; set; }
private async Task HandlePlayClick(TrackDto track) // Controlled play-state inputs: the parent owns playback truth (the player service)
{ // and drives these. The gallery is presentational — it only matches by id to decide
if (SelectedTrack == track) return; // which card reflects the active state.
SelectedTrack = track; [Parameter] public TrackDto? ActiveTrack { get; set; }
StateHasChanged(); [Parameter] public bool IsPlaying { get; set; }
[Parameter] public bool IsPaused { get; set; }
if (SelectedTrackChanged.HasDelegate) [Parameter] public EventCallback<TrackDto> OnPlay { get; set; }
{ [Parameter] public EventCallback<TrackDto> OnPause { get; set; }
await SelectedTrackChanged.InvokeAsync(track);
} private Task HandlePlayClick(TrackDto track) =>
} OnPlay.HasDelegate ? OnPlay.InvokeAsync(track) : Task.CompletedTask;
private Task HandlePauseClick(TrackDto track) =>
OnPause.HasDelegate ? OnPause.InvokeAsync(track) : Task.CompletedTask;
} }