feat(flutter): playlist cover art via deterministic /api/playlists/{id}/cover

Same fix shape as albums: drift's CachedPlaylistAdapter.toRef was
returning coverUrl: '' because the cache table doesn't persist the
server-derived URL. Set it to /api/playlists/<id>/cover in the
adapter — handleGetPlaylistCover serves the cached collage from
disk, so the URL is deterministic and the round-trip through drift
no longer drops it.

PlaylistCard + PlaylistsListScreen pass the existing queue_music
icon as ServerImage's fallback, so when the server hasn't built a
collage yet (system playlists with no tracks at build time), the
endpoint 404s and the icon shows over the slate background instead
of an empty box.
This commit is contained in:
2026-05-11 20:15:38 -04:00
parent 2d5f0691c2
commit 6f20a75f9b
3 changed files with 23 additions and 3 deletions
+7 -1
View File
@@ -88,6 +88,12 @@ extension TrackRefDriftWrite on TrackRef {
extension CachedPlaylistAdapter on CachedPlaylist {
/// `ownerUsername` is server-derived; cache stores the userId only.
/// Pass empty unless a join supplies it.
/// `coverUrl` is reconstructed deterministically from the playlist
/// id — the server serves the cached collage at this path
/// (handleGetPlaylistCover). Mirrors the album-cover trick. When
/// the server hasn't built a collage yet (system playlists with no
/// tracks at build time), the endpoint 404s and PlaylistCard's
/// ServerImage falls back to its slate placeholder.
Playlist toRef({String ownerUsername = ''}) => Playlist(
id: id,
userId: userId,
@@ -96,7 +102,7 @@ extension CachedPlaylistAdapter on CachedPlaylist {
isPublic: isPublic,
systemVariant: systemVariant,
trackCount: trackCount,
coverUrl: '', // server-derived; cache doesn't persist
coverUrl: '/api/playlists/$id/cover',
ownerUsername: ownerUsername,
createdAt: '',
updatedAt: '',
@@ -83,7 +83,11 @@ class _PlaylistTile extends StatelessWidget {
color: fs.slate,
child: playlist.coverUrl.isEmpty
? Icon(Icons.queue_music, color: fs.ash)
: ServerImage(url: playlist.coverUrl, fit: BoxFit.cover),
: ServerImage(
url: playlist.coverUrl,
fit: BoxFit.cover,
fallback: Icon(Icons.queue_music, color: fs.ash),
),
),
),
const SizedBox(width: 12),
@@ -31,9 +31,19 @@ class PlaylistCard extends StatelessWidget {
width: 144,
height: 144,
color: fs.slate,
// coverUrl is deterministic per playlist (see
// CachedPlaylistAdapter.toRef). When the server's
// collage isn't built yet, ServerImage's
// errorBuilder shows the queue_music icon over the
// slate background.
child: playlist.coverUrl.isEmpty
? Icon(Icons.queue_music, color: fs.ash, size: 56)
: ServerImage(url: playlist.coverUrl, fit: BoxFit.cover),
: ServerImage(
url: playlist.coverUrl,
fit: BoxFit.cover,
fallback:
Icon(Icons.queue_music, color: fs.ash, size: 56),
),
),
),
const SizedBox(height: 8),