import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import '../../library/widgets/play_circle_button.dart'; import '../../models/playlist.dart'; import '../../models/track.dart'; import '../../player/player_provider.dart'; import '../../shared/widgets/server_image.dart'; import '../../theme/theme_extension.dart'; import '../playlists_provider.dart'; /// Mirrors the web PlaylistCard. ~176dp wide, square cover (~144dp), /// name + optional system-variant badge below. Tap pushes /// `/playlists/{id}`. The bottom-right play button fetches the /// playlist and starts playback from track 0; disabled when the /// playlist has no tracks. class PlaylistCard extends ConsumerWidget { const PlaylistCard({super.key, required this.playlist}); final Playlist playlist; @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; final hasTracks = playlist.trackCount > 0; return SizedBox( width: 176, child: Material( color: Colors.transparent, child: InkWell( onTap: () => context.push('/playlists/${playlist.id}', extra: playlist), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stack: collage + overlaid play button at bottom-right. Stack( 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), ), ), ), Positioned( bottom: 6, right: 6, child: PlayCircleButton( enabled: hasTracks, onPressed: () => _playPlaylist(ref), ), ), ], ), 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), ), ), ), ]), ), ), ), ); } /// Fetches the playlist via /api/playlists/{id}, materializes each /// 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 _playPlaylist(WidgetRef ref) async { final api = await ref.read(playlistsApiProvider.future); final detail = await api.get(playlist.id); final refs = []; for (final t in detail.tracks) { if (t.trackId == null) continue; refs.add(TrackRef( id: t.trackId!, title: t.title, albumId: t.albumId ?? '', albumTitle: t.albumTitle, artistId: t.artistId ?? '', artistName: t.artistName, durationSec: t.durationSec, streamUrl: t.streamUrl ?? '', )); } if (refs.isEmpty) return; await ref.read(playerActionsProvider).playTracks(refs, initialIndex: 0); } }