From dccdb00bcee923444c9a45430f0fd687cf9242b1 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 9 May 2026 11:37:40 -0400 Subject: [PATCH] feat(flutter/widgets): CompactTrackCard for dense home Most-played row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the web CompactTrackCard — small horizontal cell with cover art (48dp), title, artist. Tap plays from this track in the section. Width 176dp matches web's compact density so 25 tracks per row scroll naturally on phone widths. Cover URL is derived from track.albumId (/api/albums//cover) since TrackRef itself doesn't carry a cover field — same pattern as web's coverUrl() helper. Used in the next commit's home Most-played section (3 rows × 25); otherwise unused this slice. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../library/widgets/compact_track_card.dart | 82 +++++++++++++++++++ .../widgets/compact_track_card_test.dart | 38 +++++++++ 2 files changed, 120 insertions(+) create mode 100644 flutter_client/lib/library/widgets/compact_track_card.dart create mode 100644 flutter_client/test/library/widgets/compact_track_card_test.dart 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); + }); +}