From 6f20a75f9b5a0ebbf17f74a8712adb6434adaba0 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 11 May 2026 20:15:38 -0400 Subject: [PATCH] feat(flutter): playlist cover art via deterministic /api/playlists/{id}/cover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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. --- flutter_client/lib/cache/adapters.dart | 8 +++++++- .../lib/playlists/playlists_list_screen.dart | 6 +++++- .../lib/playlists/widgets/playlist_card.dart | 12 +++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/flutter_client/lib/cache/adapters.dart b/flutter_client/lib/cache/adapters.dart index ba19be86..0785284f 100644 --- a/flutter_client/lib/cache/adapters.dart +++ b/flutter_client/lib/cache/adapters.dart @@ -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: '', diff --git a/flutter_client/lib/playlists/playlists_list_screen.dart b/flutter_client/lib/playlists/playlists_list_screen.dart index 217599b6..7cf4172a 100644 --- a/flutter_client/lib/playlists/playlists_list_screen.dart +++ b/flutter_client/lib/playlists/playlists_list_screen.dart @@ -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), diff --git a/flutter_client/lib/playlists/widgets/playlist_card.dart b/flutter_client/lib/playlists/widgets/playlist_card.dart index e6781f37..060351a4 100644 --- a/flutter_client/lib/playlists/widgets/playlist_card.dart +++ b/flutter_client/lib/playlists/widgets/playlist_card.dart @@ -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),