diff --git a/flutter_client/lib/player/player_bar.dart b/flutter_client/lib/player/player_bar.dart index 261e8d8d..d6f31cec 100644 --- a/flutter_client/lib/player/player_bar.dart +++ b/flutter_client/lib/player/player_bar.dart @@ -95,14 +95,25 @@ class _TrackInfo extends StatelessWidget { // artwork from this 48dp footprint to the full-screen size rather // than fade-cutting. Tag is stable per-route (not keyed by media.id) // so the transition works regardless of what's playing. - Widget cover; + // + // AnimatedSwitcher around the cover smooths the artUri null→file:// + // swap that fires on every track change: audio_handler broadcasts + // the new MediaItem without artUri immediately, then re-broadcasts + // with artUri once AlbumCoverCache resolves the path. Without the + // crossfade the cover snaps from the previous track's image to a + // slate placeholder to the new image — visible as a flicker. + final Widget coverChild; if (media.artUri != null) { // CachedNetworkImageProvider for HTTPS art URIs so the mini bar // hits the same disk cache the rest of the UI uses (ServerImage, // discover thumbnails). FileImage stays for the file:// branch // populated by AlbumCoverCache — bytes are already on disk and a // second cache layer would only burn duplicate space. - cover = Image( + coverChild = Image( + // Key includes the artUri so AnimatedSwitcher detects a swap; + // without this it treats successive Image widgets as the same + // tree node and skips the transition. + key: ValueKey('art-${media.artUri}'), image: media.artUri!.isScheme('file') ? FileImage(File.fromUri(media.artUri!)) as ImageProvider : CachedNetworkImageProvider(media.artUri.toString()), @@ -117,8 +128,19 @@ class _TrackInfo extends StatelessWidget { Container(width: 48, height: 48, color: fs.slate), ); } else { - cover = Container(width: 48, height: 48, color: fs.slate); + coverChild = Container( + key: const ValueKey('art-placeholder'), + width: 48, + height: 48, + color: fs.slate, + ); } + final cover = AnimatedSwitcher( + duration: const Duration(milliseconds: 180), + switchInCurve: Curves.easeOut, + switchOutCurve: Curves.easeOut, + child: coverChild, + ); return Row( // Default centering vertically aligns the like/kebab buttons // against the album art (48dp) — visually they span the title +