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
|
// Listen for scroll-near-bottom and ask the notifier
|
||||||
// to fetch the next page. 800px lookahead so the next
|
// to fetch the next page. 800px lookahead so the next
|
||||||
// page lands before the user reaches the visible end.
|
// page lands before the user reaches the visible end.
|
||||||
child: NotificationListener<ScrollNotification>(
|
// LayoutBuilder + cell-aware ArtistCard width mirrors
|
||||||
onNotification: (n) {
|
// the AlbumsTab pattern so the circular avatar stays
|
||||||
if (n.metrics.pixels >=
|
// a true circle on narrow grid cells.
|
||||||
n.metrics.maxScrollExtent - 800) {
|
child: LayoutBuilder(builder: (ctx, constraints) {
|
||||||
ref
|
const cols = 3;
|
||||||
.read(_libraryArtistsProvider.notifier)
|
const sidePad = 8.0;
|
||||||
.loadMore();
|
const gap = 8.0;
|
||||||
}
|
final cellW = (constraints.maxWidth -
|
||||||
return false;
|
sidePad * 2 -
|
||||||
},
|
gap * (cols - 1)) /
|
||||||
child: GridView.builder(
|
cols;
|
||||||
padding: const EdgeInsets.all(8),
|
// avatar (cellW - 16) + gap (8) + 1 line name (~18)
|
||||||
gridDelegate:
|
// + slack matched to AlbumsTab's overflow guard.
|
||||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
final cellH = (cellW - 16) + 8 + 18 + 8;
|
||||||
crossAxisCount: 3,
|
return NotificationListener<ScrollNotification>(
|
||||||
mainAxisSpacing: 8,
|
onNotification: (n) {
|
||||||
crossAxisSpacing: 8,
|
if (n.metrics.pixels >=
|
||||||
childAspectRatio: 0.78,
|
n.metrics.maxScrollExtent - 800) {
|
||||||
),
|
ref
|
||||||
itemCount: page.items.length,
|
.read(_libraryArtistsProvider.notifier)
|
||||||
itemBuilder: (ctx, i) {
|
.loadMore();
|
||||||
final artist = page.items[i];
|
}
|
||||||
return ArtistCard(
|
return false;
|
||||||
artist: artist,
|
|
||||||
onTap: () => ctx.push('/artists/${artist.id}',
|
|
||||||
extra: artist),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
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';
|
import 'play_circle_button.dart';
|
||||||
|
|
||||||
class ArtistCard extends ConsumerWidget {
|
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 ArtistRef artist;
|
||||||
final VoidCallback onTap;
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||||
|
final coverSize = width - 16;
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 140,
|
width: width,
|
||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -29,15 +41,20 @@ class ArtistCard extends ConsumerWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
// Stack: circular avatar + overlaid play button at bottom-right.
|
// Stack: circular avatar + overlaid play button at bottom-right.
|
||||||
// The avatar is a circle (ClipOval) — bottom-right of its
|
// Avatar size derives from the [width] parameter so the
|
||||||
// bounding box still places the button inside the visible area
|
// Library Artists 3-column grid can pass its cell width
|
||||||
// because the button is small relative to the radius.
|
// (~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(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
ClipOval(
|
ClipOval(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 124,
|
width: coverSize,
|
||||||
height: 124,
|
height: coverSize,
|
||||||
color: fs.slate,
|
color: fs.slate,
|
||||||
child: artist.coverUrl.isEmpty
|
child: artist.coverUrl.isEmpty
|
||||||
? SvgPicture.asset('assets/svg/album-fallback.svg',
|
? SvgPicture.asset('assets/svg/album-fallback.svg',
|
||||||
|
|||||||
Reference in New Issue
Block a user