feat(player): surface playback errors via debounced SnackBar (#58)

_handlePlaybackError silently skipped a dead track (404 / decoder /
EOS / network drop) with only a debugPrint, hiding the signal that
tells a broken track from a flaky app.

- audio_handler: _playbackErrors broadcast stream; emit the failing
  track title (mediaItem.value?.title — correctly mapped post-#49)
  before the skip/pause
- playback_error_reporter (new): global scaffoldMessengerKey +
  reporter that buffers, 2s-debounces, and coalesces bursts into one
  SnackBar ("Couldn't play X — skipping" / "Skipped N unplayable
  tracks")
- app.dart: scaffoldMessengerKey on MaterialApp.router + postFrame read

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 19:39:58 -04:00
parent c659165218
commit 25ee54fca0
3 changed files with 98 additions and 0 deletions
+6
View File
@@ -9,6 +9,7 @@ import 'cache/prefetcher.dart';
import 'cache/resume_controller.dart';
import 'cache/sync_controller.dart';
import 'player/play_events_reporter.dart';
import 'player/playback_error_reporter.dart';
import 'shared/live_events_dispatcher.dart';
import 'shared/routing.dart';
import 'theme/theme_data.dart';
@@ -68,6 +69,10 @@ class _MinstrelAppState extends ConsumerState<MinstrelApp> {
// pause, and app teardown. Pairs with the #52 idle teardown so a
// torn-down session is recoverable instead of lost.
ref.read(resumeControllerProvider);
// Playback-error reporter (#58): turns the handler's silent
// dead-track skips into a debounced/coalesced SnackBar so a
// vanished track isn't mysterious (and aids server/cache debug).
ref.read(playbackErrorReporterProvider);
// Offline marker (#427 S1): periodic /healthz reachability
// probe → offlineProvider. Read here to start the poller; S4
// gates system-playlist play + Shuffle-all on it.
@@ -81,6 +86,7 @@ class _MinstrelAppState extends ConsumerState<MinstrelApp> {
final mode = ref.watch(themeModeProvider).value ?? AppThemeMode.system;
return MaterialApp.router(
title: 'Minstrel',
scaffoldMessengerKey: scaffoldMessengerKey,
theme: buildLightTheme(),
darkTheme: buildDarkTheme(),
themeMode: mode.materialMode,