From 3162fedd3b8d734aad77310604e33aa62c029b27 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 11 May 2026 07:57:10 -0400 Subject: [PATCH] fix(flutter): card taps via Material+InkWell, not GestureDetector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User reports only the first card in 'Recently added' navigates and the HitTestBehavior.opaque fix in d703fc2 didn't fully resolve it. Switch all three card widgets (album, artist, playlist) from GestureDetector to Material(transparent) + InkWell. This is the more idiomatic Flutter pattern for tap-to-navigate cards: - InkWell registers via the Material ripple system instead of the raw gesture arena, sidestepping any subtle deferToChild edge cases inside horizontal ListViews. - Visible ripple feedback on tap means a missed tap is now diagnosable visually — if the ripple shows but nothing navigates, the issue is on the route side; if no ripple, the tap never landed. Material color is transparent so the existing dark page background shows through unchanged. --- .../lib/library/widgets/album_card.dart | 18 +++-- .../lib/library/widgets/artist_card.dart | 53 +++++++------ .../lib/playlists/widgets/playlist_card.dart | 77 ++++++++++--------- 3 files changed, 78 insertions(+), 70 deletions(-) diff --git a/flutter_client/lib/library/widgets/album_card.dart b/flutter_client/lib/library/widgets/album_card.dart index 73d9bb89..69164f5c 100644 --- a/flutter_client/lib/library/widgets/album_card.dart +++ b/flutter_client/lib/library/widgets/album_card.dart @@ -13,14 +13,15 @@ class AlbumCard extends StatelessWidget { @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: SizedBox( - width: 140, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + return SizedBox( + width: 140, + 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( @@ -46,6 +47,7 @@ class AlbumCard extends StatelessWidget { style: TextStyle(color: fs.ash, fontSize: 12), ), ]), + ), ), ), ); diff --git a/flutter_client/lib/library/widgets/artist_card.dart b/flutter_client/lib/library/widgets/artist_card.dart index d5043397..218a30ba 100644 --- a/flutter_client/lib/library/widgets/artist_card.dart +++ b/flutter_client/lib/library/widgets/artist_card.dart @@ -13,32 +13,35 @@ class ArtistCard extends StatelessWidget { @override Widget build(BuildContext context) { final fs = Theme.of(context).extension()!; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onTap, - child: SizedBox( - width: 140, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipOval( - child: Container( - width: 124, - height: 124, - color: fs.slate, - child: artist.coverUrl.isEmpty - ? SvgPicture.asset('assets/svg/album-fallback.svg', fit: BoxFit.cover) - : ServerImage(url: artist.coverUrl, fit: BoxFit.cover), + return SizedBox( + width: 140, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ClipOval( + child: Container( + width: 124, + height: 124, + color: fs.slate, + child: artist.coverUrl.isEmpty + ? SvgPicture.asset('assets/svg/album-fallback.svg', + fit: BoxFit.cover) + : ServerImage(url: artist.coverUrl, fit: BoxFit.cover), + ), ), - ), - const SizedBox(height: 8), - Text( - artist.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: fs.parchment, fontSize: 14), - ), - ]), + const SizedBox(height: 8), + Text( + artist.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.parchment, fontSize: 14), + ), + ]), + ), ), ), ); diff --git a/flutter_client/lib/playlists/widgets/playlist_card.dart b/flutter_client/lib/playlists/widgets/playlist_card.dart index 884f8a72..e6781f37 100644 --- a/flutter_client/lib/playlists/widgets/playlist_card.dart +++ b/flutter_client/lib/playlists/widgets/playlist_card.dart @@ -18,46 +18,49 @@ class PlaylistCard extends StatelessWidget { final fs = Theme.of(context).extension()!; return SizedBox( width: 176, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => context.push('/playlists/${playlist.id}'), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Container( - width: 144, - height: 144, - color: fs.slate, - child: playlist.coverUrl.isEmpty - ? Icon(Icons.queue_music, color: fs.ash, size: 56) - : ServerImage(url: playlist.coverUrl, fit: BoxFit.cover), - ), - ), - const SizedBox(height: 8), - Text( - playlist.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(color: fs.parchment, fontSize: 14), - ), - if (playlist.isSystem) - Padding( - padding: const EdgeInsets.only(top: 2), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => context.push('/playlists/${playlist.id}'), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ClipRRect( + borderRadius: BorderRadius.circular(6), child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: fs.iron, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - playlist.systemVariant!.replaceAll('_', ' '), - style: TextStyle(color: fs.accent, fontSize: 11), - ), + width: 144, + height: 144, + color: fs.slate, + child: playlist.coverUrl.isEmpty + ? Icon(Icons.queue_music, color: fs.ash, size: 56) + : ServerImage(url: playlist.coverUrl, fit: BoxFit.cover), ), ), - ]), + const SizedBox(height: 8), + Text( + playlist.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(color: fs.parchment, fontSize: 14), + ), + if (playlist.isSystem) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: fs.iron, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + playlist.systemVariant!.replaceAll('_', ' '), + style: TextStyle(color: fs.accent, fontSize: 11), + ), + ), + ), + ]), + ), ), ), );