Skip to content

Commit 37c4fb0

Browse files
committed
sync up player state
1 parent d8aceab commit 37c4fb0

4 files changed

Lines changed: 103 additions & 31 deletions

File tree

HomeSpeaker.WebAssembly/Components/Music/Player/PlayControls.razor

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@inject HomeSpeakerService svc
22
@inject IJSRuntime JSRuntime
3+
@inject PlayerStateService PlayerState
4+
@implements IDisposable
35

46
<div class="play-controls">
57
<button @onclick=Play title="Resume playing" class="btn btn-icon-lg btn-primary play-button">
@@ -14,7 +16,7 @@
1416
<button @onclick=Shuffle title="Shuffle queue" class="btn btn-icon btn-outline-secondary">
1517
<i class="fas fa-random"></i>
1618
</button>
17-
<button @onclick=ToggleRepeat title="@(repeatMode ? "Repeat: ON" : "Repeat: OFF")" class="btn btn-icon @(repeatMode ? "btn-warning" : "btn-outline-secondary")">
19+
<button @onclick=ToggleRepeat title="@(PlayerState.RepeatMode ? "Repeat: ON" : "Repeat: OFF")" class="btn btn-icon @(PlayerState.RepeatMode ? "btn-warning" : "btn-outline-secondary")">
1820
<i class="fas fa-redo"></i>
1921
</button>
2022
<button @onclick=Clear title="Clear queue" class="btn btn-icon btn-outline-danger">
@@ -104,16 +106,15 @@
104106
</style>
105107

106108
@code {
107-
bool repeatMode = false;
108109
bool sleepTimerActive = false;
109110
string sleepTimerRemaining = "";
110111
System.Threading.Timer? refreshTimer;
111112

112113
protected override async Task OnInitializedAsync()
113114
{
114-
await RefreshRepeatMode();
115+
PlayerState.StateChanged += OnStateChanged;
115116
await RefreshSleepTimer();
116-
117+
117118
// Refresh sleep timer display every 10 seconds
118119
refreshTimer = new System.Threading.Timer(async _ =>
119120
{
@@ -122,17 +123,7 @@
122123
}, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
123124
}
124125

125-
async Task RefreshRepeatMode()
126-
{
127-
try
128-
{
129-
repeatMode = await svc.GetRepeatModeAsync();
130-
}
131-
catch (Exception ex)
132-
{
133-
await JSRuntime.InvokeVoidAsync("console.error", $"Error getting repeat mode: {ex.Message}");
134-
}
135-
}
126+
private void OnStateChanged() => InvokeAsync(StateHasChanged);
136127

137128
async Task RefreshSleepTimer()
138129
{
@@ -156,8 +147,9 @@
156147

157148
async Task ToggleRepeat()
158149
{
159-
repeatMode = !repeatMode;
160-
await svc.SetRepeatModeAsync(repeatMode);
150+
var newMode = !PlayerState.RepeatMode;
151+
PlayerState.UpdateRepeatMode(newMode); // optimistic update — notifies both instances
152+
await svc.SetRepeatModeAsync(newMode);
161153
}
162154

163155
async Task SetSleepTimer(int minutes)
@@ -181,8 +173,7 @@
181173

182174
public void Dispose()
183175
{
176+
PlayerState.StateChanged -= OnStateChanged;
184177
refreshTimer?.Dispose();
185178
}
186179
}
187-
188-
@implements IDisposable

HomeSpeaker.WebAssembly/Pages/Index.razor

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@inject HomeSpeakerService svc
33
@inject ILogger<Index> logger
44
@inject HttpClient Http
5+
@inject PlayerStateService PlayerState
56
@implements IDisposable
67

78
<PageTitle>Home Speaker</PageTitle>
@@ -12,28 +13,28 @@
1213
<i class="fas fa-headphones me-2"></i>Now Playing
1314
</div>
1415
<div class="now-playing-status">
15-
@if (playerStatus == null)
16+
@if (PlayerState.Status == null)
1617
{
1718
<div class="loading-spinner mx-auto mb-2"></div>
1819
<span class="status-text text-muted">Connecting...</span>
1920
}
20-
else if (playerStatus.StilPlaying && playerStatus.CurrentSong != null)
21+
else if (PlayerState.Status.StilPlaying && PlayerState.Status.CurrentSong != null)
2122
{
2223
<div class="song-info">
23-
<div class="song-title">@playerStatus.CurrentSong.Name</div>
24-
@if (!string.IsNullOrEmpty(playerStatus.CurrentSong.Artist))
24+
<div class="song-title">@PlayerState.Status.CurrentSong.Name</div>
25+
@if (!string.IsNullOrEmpty(PlayerState.Status.CurrentSong.Artist))
2526
{
26-
<div class="song-artist">@playerStatus.CurrentSong.Artist</div>
27+
<div class="song-artist">@PlayerState.Status.CurrentSong.Artist</div>
2728
}
28-
@if (!string.IsNullOrEmpty(playerStatus.CurrentSong.Album))
29+
@if (!string.IsNullOrEmpty(PlayerState.Status.CurrentSong.Album))
2930
{
30-
<div class="song-album">@playerStatus.CurrentSong.Album</div>
31+
<div class="song-album">@PlayerState.Status.CurrentSong.Album</div>
3132
}
3233
<div class="progress-row mt-2">
3334
<div class="progress-bar-track">
34-
<div class="progress-bar-fill" style="width: @(playerStatus.PercentComplete.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))%"></div>
35+
<div class="progress-bar-fill" style="width: @(PlayerState.Status.PercentComplete.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))%"></div>
3536
</div>
36-
<span class="volume-badge"><i class="fas fa-volume-up me-1"></i>@playerStatus.Volume</span>
37+
<span class="volume-badge"><i class="fas fa-volume-up me-1"></i>@PlayerState.Status.Volume</span>
3738
</div>
3839
</div>
3940
}
@@ -177,10 +178,58 @@
177178
font-size: 1rem;
178179
}
179180
}
181+
182+
/* Pi 7" screen — ultra compact now-playing */
183+
@@media (max-height: 520px) {
184+
.now-playing-card {
185+
padding: 0.4rem var(--hs-space-sm);
186+
margin-bottom: 0.5rem !important;
187+
}
188+
189+
.now-playing-label {
190+
display: none;
191+
}
192+
193+
.now-playing-status {
194+
min-height: 28px;
195+
margin-bottom: 0.25rem;
196+
flex-direction: row;
197+
justify-content: flex-start;
198+
gap: 0.5rem;
199+
}
200+
201+
.song-title {
202+
font-size: 0.875rem;
203+
white-space: nowrap;
204+
overflow: hidden;
205+
text-overflow: ellipsis;
206+
max-width: 60vw;
207+
}
208+
209+
.song-artist {
210+
font-size: 0.75rem;
211+
display: inline;
212+
}
213+
214+
.song-album {
215+
display: none;
216+
}
217+
218+
.progress-row {
219+
display: none;
220+
}
221+
222+
.idle-icon {
223+
font-size: 1rem;
224+
}
225+
226+
.now-playing-controls {
227+
padding-top: 0.25rem;
228+
}
229+
}
180230
</style>
181231

182232
@code {
183-
private GetStatusReply? playerStatus;
184233
private bool showTemperature;
185234
private bool showBloodSugar;
186235
private Timer? refreshTimer;
@@ -200,6 +249,7 @@
200249
logger.LogWarning(ex, "Failed to load feature flags");
201250
}
202251

252+
PlayerState.StateChanged += OnStateChanged;
203253
await RefreshStatusAsync();
204254

205255
refreshTimer = new Timer(async _ =>
@@ -216,12 +266,17 @@
216266
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
217267
}
218268

269+
private void OnStateChanged() => InvokeAsync(StateHasChanged);
270+
219271
private async Task RefreshStatusAsync()
220272
{
221273
try
222274
{
223-
playerStatus = await svc.GetStatusAsync();
224-
StateHasChanged();
275+
var status = await svc.GetStatusAsync();
276+
bool repeatMode = await svc.GetRepeatModeAsync();
277+
PlayerState.UpdateStatus(status);
278+
PlayerState.UpdateRepeatMode(repeatMode);
279+
// StateHasChanged is triggered by the event subscription above
225280
}
226281
catch (Exception ex)
227282
{
@@ -231,6 +286,7 @@
231286

232287
public void Dispose()
233288
{
289+
PlayerState.StateChanged -= OnStateChanged;
234290
refreshTimer?.Dispose();
235291
}
236292
}

HomeSpeaker.WebAssembly/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
builder.Services.AddScoped((_) => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
1919
builder.Services.AddSingleton<HomeSpeakerService>();
20+
builder.Services.AddSingleton<PlayerStateService>();
2021
builder.Services.AddScoped<ITemperatureService, TemperatureService>();
2122
builder.Services.AddScoped<IBloodSugarService, BloodSugarService>();
2223
builder.Services.AddScoped<IForecastService, ForecastService>();
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace HomeSpeaker.WebAssembly.Services;
2+
3+
public class PlayerStateService
4+
{
5+
private GetStatusReply? _status;
6+
private bool _repeatMode;
7+
8+
public GetStatusReply? Status => _status;
9+
public bool RepeatMode => _repeatMode;
10+
11+
public event Action? StateChanged;
12+
13+
public void UpdateStatus(GetStatusReply? status)
14+
{
15+
_status = status;
16+
StateChanged?.Invoke();
17+
}
18+
19+
public void UpdateRepeatMode(bool repeatMode)
20+
{
21+
_repeatMode = repeatMode;
22+
StateChanged?.Invoke();
23+
}
24+
}

0 commit comments

Comments
 (0)