Files
minstrel/flutter_client/lib/playlists/widgets/playlist_card.dart
T
bvandeusen 6f20a75f9b 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.
2026-05-11 20:15:38 -04:00

79 lines
2.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../models/playlist.dart';
import '../../shared/widgets/server_image.dart';
import '../../theme/theme_extension.dart';
/// Mirrors the web PlaylistCard. ~176dp wide, square cover (~144dp),
/// name + optional system-variant badge below. Tap pushes
/// `/playlists/{id}`.
class PlaylistCard extends StatelessWidget {
const PlaylistCard({super.key, required this.playlist});
final Playlist playlist;
@override
Widget build(BuildContext context) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
return SizedBox(
width: 176,
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => context.push('/playlists/${playlist.id}'),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Container(
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,
fallback:
Icon(Icons.queue_music, color: fs.ash, size: 56),
),
),
),
const SizedBox(height: 8),
Text(
playlist.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: fs.parchment, fontSize: 14),
),
if (playlist.isSystem)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: fs.iron,
borderRadius: BorderRadius.circular(4),
),
child: Text(
playlist.systemVariant!.replaceAll('_', ' '),
style: TextStyle(color: fs.accent, fontSize: 11),
),
),
),
]),
),
),
),
);
}
}