bf0ef5e0c3
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.
102 lines
3.6 KiB
Dart
102 lines
3.6 KiB
Dart
import 'dart:math';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:flutter_svg/flutter_svg.dart';
|
||
|
||
import '../../models/artist.dart';
|
||
import '../../player/player_provider.dart';
|
||
import '../../shared/widgets/server_image.dart';
|
||
import '../../theme/theme_extension.dart';
|
||
import '../library_providers.dart';
|
||
import 'play_circle_button.dart';
|
||
|
||
class ArtistCard extends ConsumerWidget {
|
||
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: width,
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||
// Stack: circular avatar + overlaid play button at bottom-right.
|
||
// 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: coverSize,
|
||
height: coverSize,
|
||
color: fs.slate,
|
||
child: artist.coverUrl.isEmpty
|
||
? SvgPicture.asset('assets/svg/album-fallback.svg',
|
||
fit: BoxFit.cover)
|
||
: ServerImage(url: artist.coverUrl, fit: BoxFit.cover),
|
||
),
|
||
),
|
||
Positioned(
|
||
bottom: 4,
|
||
right: 4,
|
||
child: PlayCircleButton(
|
||
onPressed: () => _playArtistShuffle(ref),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
artist.name,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: TextStyle(color: fs.parchment, fontSize: 14),
|
||
),
|
||
]),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
/// Fetches the artist's tracks via /api/artists/{id}/tracks, shuffles
|
||
/// them (Fisher-Yates, default Random), and plays from index 0.
|
||
/// Matches the web ArtistCard's `playQueue(shuffle(tracks), 0)`.
|
||
Future<void> _playArtistShuffle(WidgetRef ref) async {
|
||
final api = await ref.read(libraryApiProvider.future);
|
||
final tracks = await api.getArtistTracks(artist.id);
|
||
if (tracks.isEmpty) return;
|
||
final shuffled = List.of(tracks);
|
||
shuffled.shuffle(Random());
|
||
await ref
|
||
.read(playerActionsProvider)
|
||
.playTracks(shuffled, initialIndex: 0);
|
||
}
|
||
}
|