diff --git a/flutter_client/lib/library/library_screen.dart b/flutter_client/lib/library/library_screen.dart index 33ceb20a..c300294a 100644 --- a/flutter_client/lib/library/library_screen.dart +++ b/flutter_client/lib/library/library_screen.dart @@ -398,36 +398,52 @@ class _ArtistsTab extends ConsumerWidget { // Listen for scroll-near-bottom and ask the notifier // to fetch the next page. 800px lookahead so the next // page lands before the user reaches the visible end. - child: NotificationListener( - onNotification: (n) { - if (n.metrics.pixels >= - n.metrics.maxScrollExtent - 800) { - ref - .read(_libraryArtistsProvider.notifier) - .loadMore(); - } - return false; - }, - child: GridView.builder( - padding: const EdgeInsets.all(8), - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - childAspectRatio: 0.78, - ), - itemCount: page.items.length, - itemBuilder: (ctx, i) { - final artist = page.items[i]; - return ArtistCard( - artist: artist, - onTap: () => ctx.push('/artists/${artist.id}', - extra: artist), - ); + // LayoutBuilder + cell-aware ArtistCard width mirrors + // the AlbumsTab pattern so the circular avatar stays + // a true circle on narrow grid cells. + child: 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; + // avatar (cellW - 16) + gap (8) + 1 line name (~18) + // + slack matched to AlbumsTab's overflow guard. + final cellH = (cellW - 16) + 8 + 18 + 8; + return NotificationListener( + onNotification: (n) { + if (n.metrics.pixels >= + n.metrics.maxScrollExtent - 800) { + ref + .read(_libraryArtistsProvider.notifier) + .loadMore(); + } + return false; }, - ), - ), + child: GridView.builder( + padding: const EdgeInsets.all(sidePad), + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: cols, + mainAxisExtent: cellH, + mainAxisSpacing: gap, + crossAxisSpacing: gap, + ), + itemCount: page.items.length, + itemBuilder: (ctx, i) { + final artist = page.items[i]; + return ArtistCard( + artist: artist, + width: cellW, + onTap: () => ctx.push('/artists/${artist.id}', + extra: artist), + ); + }, + ), + ); + }), ); }, ); diff --git a/flutter_client/lib/library/widgets/artist_card.dart b/flutter_client/lib/library/widgets/artist_card.dart index 3d6bd6b2..fed8c74f 100644 --- a/flutter_client/lib/library/widgets/artist_card.dart +++ b/flutter_client/lib/library/widgets/artist_card.dart @@ -12,15 +12,27 @@ import '../library_providers.dart'; import 'play_circle_button.dart'; class ArtistCard extends ConsumerWidget { - const ArtistCard({required this.artist, required this.onTap, super.key}); + const ArtistCard({ + required this.artist, + required this.onTap, + this.width = 140, + super.key, + }); final ArtistRef artist; final VoidCallback onTap; + /// Outer width of the card. Avatar is a circle of width-16 (8dp + /// horizontal padding either side). Default suits horizontal lists; + /// grids should pass the cell width so the avatar shrinks + /// proportionally and stays a true circle. + final double width; + @override Widget build(BuildContext context, WidgetRef ref) { final fs = Theme.of(context).extension()!; + final coverSize = width - 16; return SizedBox( - width: 140, + width: width, child: Material( color: Colors.transparent, child: InkWell( @@ -29,15 +41,20 @@ class ArtistCard extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 8), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ // Stack: circular avatar + overlaid play button at bottom-right. - // The avatar is a circle (ClipOval) — bottom-right of its - // bounding box still places the button inside the visible area - // because the button is small relative to the radius. + // Avatar size derives from the [width] parameter so the + // Library Artists 3-column grid can pass its cell width + // (~109dp on typical phones) and the avatar stays a + // perfect circle. Previous hardcoded 124×124 got squeezed + // horizontally in the grid (cell narrower than 124+16 + // padding) but kept its 124dp height — ClipOval produced + // a visible vertical ellipse. Mirrors AlbumCard's width- + // parameter convention. Stack( children: [ ClipOval( child: Container( - width: 124, - height: 124, + width: coverSize, + height: coverSize, color: fs.slate, child: artist.coverUrl.isEmpty ? SvgPicture.asset('assets/svg/album-fallback.svg',