fix(playlists): surface playlist-load failures with SnackBar feedback (#479)
Tapping a system-playlist PlayCircleButton before the mix had loaded
silently stalled — three failure modes all looked identical to the
user (PlayCircleButton's spinner clears with no playback):
- api.systemShuffle returns empty (mix not built server-side yet)
→ silent return at `if (refs.isEmpty)`
- api call slow / hung → spinner spins indefinitely (no client
timeout was set; Dio default is unlimited)
- api throws → uncaught; spinner finalizer clears it silently
Bundle:
- 8s `.timeout` on the systemShuffle / get call; TimeoutException
→ "Couldn't load playlist — check your connection"
- empty refs after filtering → "Mix isn't ready yet — try again
in a moment"
- other throws → "Playlist load failed: <error>"
- thread BuildContext through and capture ScaffoldMessenger before
the first await so no `use_build_context_synchronously` lint
No pre-warm — system playlists are intentionally uncached per the
api endpoint comment ("varies per play"); pre-warming would decide
the shuffle order at home-screen load instead of at tap, breaking
the fresh-per-play contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_lucide/flutter_lucide.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -70,7 +72,7 @@ class PlaylistCard extends ConsumerWidget {
|
||||
right: 6,
|
||||
child: PlayCircleButton(
|
||||
enabled: hasTracks,
|
||||
onPressed: () => _playPlaylist(ref),
|
||||
onPressed: () => _playPlaylist(context, ref),
|
||||
),
|
||||
),
|
||||
// System playlists get a refresh affordance so the
|
||||
@@ -187,16 +189,34 @@ class PlaylistCard extends ConsumerWidget {
|
||||
/// PlaylistTrack into a TrackRef (filtering out unavailable rows
|
||||
/// whose `trackId` is null after a track-delete), and plays from
|
||||
/// index 0. Mirrors the web PlaylistCard's onPlayClick.
|
||||
Future<void> _playPlaylist(WidgetRef ref) async {
|
||||
///
|
||||
/// Failure modes are surfaced via SnackBar so a not-yet-built mix
|
||||
/// or a slow server doesn't look like the tap was ignored.
|
||||
Future<void> _playPlaylist(BuildContext context, WidgetRef ref) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final api = await ref.read(playlistsApiProvider.future);
|
||||
// Refreshable (singleton) system playlists: fetch the server's
|
||||
// rotation-aware order (#415) and play as-is, tagged with the
|
||||
// source so the reporter advances rotation. User playlists AND
|
||||
// non-singleton system kinds (songs_like_artist — no by-kind
|
||||
// endpoint) play the stored order via get(), untagged.
|
||||
final detail = playlist.refreshable
|
||||
? await api.systemShuffle(playlist.systemVariant!)
|
||||
: await api.get(playlist.id);
|
||||
final PlaylistDetail detail;
|
||||
try {
|
||||
// Refreshable (singleton) system playlists: fetch the server's
|
||||
// rotation-aware order (#415) and play as-is, tagged with the
|
||||
// source so the reporter advances rotation. User playlists AND
|
||||
// non-singleton system kinds (songs_like_artist — no by-kind
|
||||
// endpoint) play the stored order via get(), untagged.
|
||||
detail = await (playlist.refreshable
|
||||
? api.systemShuffle(playlist.systemVariant!)
|
||||
: api.get(playlist.id))
|
||||
.timeout(const Duration(seconds: 8));
|
||||
} on TimeoutException {
|
||||
messenger.showSnackBar(const SnackBar(
|
||||
content: Text("Couldn't load playlist — check your connection"),
|
||||
));
|
||||
return;
|
||||
} catch (e) {
|
||||
messenger.showSnackBar(SnackBar(
|
||||
content: Text('Playlist load failed: $e'),
|
||||
));
|
||||
return;
|
||||
}
|
||||
final refs = <TrackRef>[];
|
||||
for (final t in detail.tracks) {
|
||||
if (t.trackId == null) continue;
|
||||
@@ -211,7 +231,12 @@ class PlaylistCard extends ConsumerWidget {
|
||||
streamUrl: t.streamUrl ?? '',
|
||||
));
|
||||
}
|
||||
if (refs.isEmpty) return;
|
||||
if (refs.isEmpty) {
|
||||
messenger.showSnackBar(const SnackBar(
|
||||
content: Text("Mix isn't ready yet — try again in a moment"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
// Rotation-aware kinds arrive pre-ordered from the server — play
|
||||
// as-is, tagged so the reporter advances rotation. No client
|
||||
// shuffle. Everything else plays stored order, untagged.
|
||||
|
||||
Reference in New Issue
Block a user