diff --git a/flutter_client/lib/library/widgets/compact_track_card.dart b/flutter_client/lib/library/widgets/compact_track_card.dart new file mode 100644 index 00000000..5557b1eb --- /dev/null +++ b/flutter_client/lib/library/widgets/compact_track_card.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../models/track.dart'; +import '../../player/player_provider.dart'; +import '../../shared/widgets/server_image.dart'; +import '../../theme/theme_extension.dart'; + +/// Small horizontal track cell used by the home Most-played section. +/// Mirrors the web CompactTrackCard sizing (~176dp wide, ~56dp tall). +/// Tap plays from this track within [sectionTracks] starting at [index]. +class CompactTrackCard extends ConsumerWidget { + const CompactTrackCard({ + super.key, + required this.track, + required this.sectionTracks, + required this.index, + }); + + final TrackRef track; + + /// All tracks in the home section so play-from-here can queue them + /// in order. + final List sectionTracks; + final int index; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final fs = Theme.of(context).extension()!; + // Cover URL derives from the track's album. Mirrors web + // CompactTrackCard which calls coverUrl(track.album_id). + final coverUrl = track.albumId.isNotEmpty + ? '/api/albums/${track.albumId}/cover' + : ''; + return SizedBox( + width: 176, + child: InkWell( + onTap: () => ref + .read(playerActionsProvider) + .playTracks(sectionTracks, initialIndex: index), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Row(children: [ + ClipRRect( + borderRadius: BorderRadius.circular(4), + child: SizedBox( + width: 48, + height: 48, + child: ServerImage( + url: coverUrl, + fit: BoxFit.cover, + fallback: Container(color: fs.slate), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + track.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.parchment, fontSize: 13), + ), + Text( + track.artistName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.ash, fontSize: 11), + ), + ], + ), + ), + ]), + ), + ), + ); + } +} diff --git a/flutter_client/test/library/widgets/compact_track_card_test.dart b/flutter_client/test/library/widgets/compact_track_card_test.dart new file mode 100644 index 00000000..bdeb4f29 --- /dev/null +++ b/flutter_client/test/library/widgets/compact_track_card_test.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:minstrel/library/widgets/compact_track_card.dart'; +import 'package:minstrel/models/track.dart'; +import 'package:minstrel/theme/theme_data.dart'; + +const _track = TrackRef( + id: 't1', + title: 'Roygbiv', + albumId: 'a1', + albumTitle: 'Geogaddi', + artistId: 'ar1', + artistName: 'Boards of Canada', + durationSec: 137, + trackNumber: 4, + streamUrl: '', +); + +void main() { + testWidgets('renders title and artist', (tester) async { + await tester.pumpWidget(ProviderScope( + child: MaterialApp( + theme: buildThemeData(), + home: Scaffold( + body: CompactTrackCard( + track: _track, + sectionTracks: const [_track], + index: 0, + ), + ), + ), + )); + expect(find.text('Roygbiv'), findsOneWidget); + expect(find.text('Boards of Canada'), findsOneWidget); + }); +}