diff --git a/flutter_client/lib/library/artist_detail_screen.dart b/flutter_client/lib/library/artist_detail_screen.dart index 45178f6d..8a18a0ec 100644 --- a/flutter_client/lib/library/artist_detail_screen.dart +++ b/flutter_client/lib/library/artist_detail_screen.dart @@ -81,27 +81,38 @@ class ArtistDetailScreen extends ConsumerWidget { albums.when( error: (e, _) => Center(child: Text('$e', style: TextStyle(color: fs.error))), loading: () => const Padding(padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator())), - data: (list) => GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.all(8), - // mainAxisExtent locks each cell's height to the actual - // AlbumCard footprint (124 cover + 8 + 14 title + 12 - // artist + ~6 padding), instead of letting - // childAspectRatio inflate it. Removes the visible vertical - // gap below each card. - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - mainAxisExtent: 168, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), - itemCount: list.length, - itemBuilder: (_, i) { - final AlbumRef album = list[i]; - return AlbumCard(album: album, onTap: () => context.push('/albums/${album.id}')); - }, - ), + data: (list) => LayoutBuilder(builder: (ctx, constraints) { + const cols = 3; + const sidePad = 8.0; + const gap = 8.0; + final cellW = + (constraints.maxWidth - sidePad * 2 - gap * (cols - 1)) / + cols; + // Card content: cover (cellW - 16) + 8 + title (≤2 lines + // ≈ 36) + artist line ≈ 16 + small fudge. + final cellH = (cellW - 16) + 8 + 36 + 16 + 4; + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.fromLTRB(sidePad, 0, sidePad, 16), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: cols, + mainAxisExtent: cellH, + mainAxisSpacing: gap, + crossAxisSpacing: gap, + ), + itemCount: list.length, + itemBuilder: (_, i) { + final AlbumRef album = list[i]; + return AlbumCard( + album: album, + width: cellW, + titleMaxLines: 2, + onTap: () => context.push('/albums/${album.id}'), + ); + }, + ); + }), ), ]), ), diff --git a/flutter_client/lib/library/widgets/album_card.dart b/flutter_client/lib/library/widgets/album_card.dart index 69164f5c..54d027e3 100644 --- a/flutter_client/lib/library/widgets/album_card.dart +++ b/flutter_client/lib/library/widgets/album_card.dart @@ -6,47 +6,69 @@ import '../../shared/widgets/server_image.dart'; import '../../theme/theme_extension.dart'; class AlbumCard extends StatelessWidget { - const AlbumCard({required this.album, required this.onTap, super.key}); + const AlbumCard({ + required this.album, + required this.onTap, + this.width = 140, + this.titleMaxLines = 1, + super.key, + }); final AlbumRef album; final VoidCallback onTap; + /// Outer width of the card. Cover is square at width - 16 (8dp + /// horizontal padding either side). Default suits horizontal lists; + /// grids should pass the cell width so the card scales down. + final double width; + + /// Lets callers (e.g. the artist detail grid) allow the title to + /// wrap to a second line so it isn't truncated to a single + /// ellipsized character at narrower widths. + final int titleMaxLines; + @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; + final coverSize = width - 16; return SizedBox( - width: 140, + width: width, child: Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Container( - width: 124, - height: 124, - color: fs.slate, - child: album.coverUrl.isEmpty - ? SvgPicture.asset('assets/svg/album-fallback.svg', fit: BoxFit.cover) - : ServerImage(url: album.coverUrl, fit: BoxFit.cover), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Container( + width: coverSize, + height: coverSize, + color: fs.slate, + child: album.coverUrl.isEmpty + ? SvgPicture.asset('assets/svg/album-fallback.svg', + fit: BoxFit.cover) + : ServerImage(url: album.coverUrl, fit: BoxFit.cover), + ), + ), + const SizedBox(height: 8), + Text( + album.title, + maxLines: titleMaxLines, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.parchment, fontSize: 14), + ), + Text( + album.artistName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.ash, fontSize: 12), + ), + ], ), - const SizedBox(height: 8), - Text( - album.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: fs.parchment, fontSize: 14), - ), - Text( - album.artistName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: fs.ash, fontSize: 12), - ), - ]), ), ), ),