fix(flutter): keep artist avatars round in the Library grid
ArtistCard hardcoded its avatar at Container(width: 124, height: 124) inside a ClipOval. In the Library Artists 3-column grid the cell is narrower than the card's nominal 140dp width — on a typical phone the cell is ~109dp, the padded inner content area ~93dp. The parent constrained the Container's width to ~93dp but the explicit height stayed 124dp, so ClipOval clipped a 93×124 rectangle and the avatar rendered as a vertical ellipse. Fix mirrors AlbumCard's pattern: ArtistCard takes an optional `width` parameter (default 140 for horizontal carousels) and derives coverSize = width - 16, so the Container is always square. ArtistsTab now uses LayoutBuilder to compute cell width and passes it through, same as AlbumsTab. Avatar stays a true circle at any cell width. mainAxisExtent on the grid replaces the previous fixed childAspectRatio so cell height tracks cellW + name line, with slack matching AlbumsTab's overflow guard.
This commit is contained in:
@@ -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<ScrollNotification>(
|
||||
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<ScrollNotification>(
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<FabledSwordTheme>()!;
|
||||
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',
|
||||
|
||||
Reference in New Issue
Block a user