diff --git a/lib/screens/home_tab_widgets.dart b/lib/screens/home_tab_widgets.dart index 420bd4ea..8da8d0f1 100644 --- a/lib/screens/home_tab_widgets.dart +++ b/lib/screens/home_tab_widgets.dart @@ -1078,30 +1078,34 @@ class _ExtensionAlbumScreenState extends ConsumerState { @override Widget build(BuildContext context) { if (_isLoading || _error != null) { - return _LoadingOrErrorScaffold( - title: widget.albumName, - isLoading: _isLoading, - error: _error, - loadingBody: const AlbumTrackListSkeleton( - itemCount: 10, - showCoverHeader: true, + return SkeletonCrossfade( + child: _LoadingOrErrorScaffold( + title: widget.albumName, + isLoading: _isLoading, + error: _error, + loadingBody: const AlbumTrackListSkeleton( + itemCount: 10, + showCoverHeader: true, + ), + onRetry: _fetchTracks, ), - onRetry: _fetchTracks, ); } - return AlbumScreen( - albumId: widget.albumId, - albumName: widget.albumName, - coverUrl: widget.coverUrl, - headerVideoUrl: _headerVideoUrl, - headerImageUrl: _headerImageUrl, - audioTraits: _audioTraits, - tracks: _tracks, - extensionId: widget.extensionId, - artistId: _artistId, - artistName: _artistName, - heroTag: widget.heroTag, + return SkeletonCrossfade( + child: AlbumScreen( + albumId: widget.albumId, + albumName: widget.albumName, + coverUrl: widget.coverUrl, + headerVideoUrl: _headerVideoUrl, + headerImageUrl: _headerImageUrl, + audioTraits: _audioTraits, + tracks: _tracks, + extensionId: widget.extensionId, + artistId: _artistId, + artistName: _artistName, + heroTag: widget.heroTag, + ), ); } } @@ -1219,24 +1223,28 @@ class _ExtensionPlaylistScreenState @override Widget build(BuildContext context) { if (_isLoading || _error != null) { - return _LoadingOrErrorScaffold( - title: widget.playlistName, - isLoading: _isLoading, - error: _error, - loadingBody: const TrackListSkeleton( - itemCount: 8, - showCoverHeader: true, + return SkeletonCrossfade( + child: _LoadingOrErrorScaffold( + title: widget.playlistName, + isLoading: _isLoading, + error: _error, + loadingBody: const TrackListSkeleton( + itemCount: 8, + showCoverHeader: true, + ), + onRetry: _fetchTracks, ), - onRetry: _fetchTracks, ); } - return PlaylistScreen( - playlistName: widget.playlistName, - coverUrl: widget.coverUrl, - headerVideoUrl: _headerVideoUrl, - tracks: _tracks!, - recommendedService: widget.extensionId, + return SkeletonCrossfade( + child: PlaylistScreen( + playlistName: widget.playlistName, + coverUrl: widget.coverUrl, + headerVideoUrl: _headerVideoUrl, + tracks: _tracks!, + recommendedService: widget.extensionId, + ), ); } } @@ -1376,26 +1384,30 @@ class _ExtensionArtistScreenState extends ConsumerState { @override Widget build(BuildContext context) { if (_isLoading || _error != null) { - return _LoadingOrErrorScaffold( - title: widget.artistName, - isLoading: _isLoading, - error: _error, - loadingBody: const ArtistScreenSkeleton(), - onRetry: _fetchArtist, + return SkeletonCrossfade( + child: _LoadingOrErrorScaffold( + title: widget.artistName, + isLoading: _isLoading, + error: _error, + loadingBody: const ArtistScreenSkeleton(), + onRetry: _fetchArtist, + ), ); } - return ArtistScreen( - artistId: widget.artistId, - artistName: widget.artistName, - coverUrl: widget.coverUrl, - headerImageUrl: _headerImageUrl, - headerVideoUrl: _headerVideoUrl, - monthlyListeners: _monthlyListeners, - albums: _albums, - topTracks: _topTracks, - extensionId: widget.extensionId, // Skip Spotify/Deezer fetch - heroTag: widget.heroTag, + return SkeletonCrossfade( + child: ArtistScreen( + artistId: widget.artistId, + artistName: widget.artistName, + coverUrl: widget.coverUrl, + headerImageUrl: _headerImageUrl, + headerVideoUrl: _headerVideoUrl, + monthlyListeners: _monthlyListeners, + albums: _albums, + topTracks: _topTracks, + extensionId: widget.extensionId, // Skip Spotify/Deezer fetch + heroTag: widget.heroTag, + ), ); } } diff --git a/lib/widgets/animation_utils.dart b/lib/widgets/animation_utils.dart index e9b78355..50bb1aea 100644 --- a/lib/widgets/animation_utils.dart +++ b/lib/widgets/animation_utils.dart @@ -624,6 +624,22 @@ class HomeSearchSkeleton extends StatelessWidget { } } +/// Crossfades when the child's runtime type changes — used to swap a loading +/// skeleton for the loaded screen without a single-frame hard cut. +class SkeletonCrossfade extends StatelessWidget { + final Widget child; + + const SkeletonCrossfade({super.key, required this.child}); + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + child: child, + ); + } +} + /// An animated selection indicator that scales in/out and crossfades the /// checked/unchecked state. class AnimatedSelectionCheckbox extends StatelessWidget {