diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt index 2d07aa7b..79776ba0 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt @@ -109,8 +109,11 @@ private fun AlbumDetailStateContent( playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel, navController: NavHostController, ) { - Crossfade(targetState = state, label = "album-detail") { s -> - when (s) { + // Crossfade keyed on `state::class` so a Success → fresh Success + // refresh (same kind, new detail) doesn't re-fade the whole body; + // only Loading ↔ Success ↔ Error transitions animate. + Crossfade(targetState = state::class, label = "album-detail") { _ -> + when (val s = state) { is AlbumDetailUiState.Loading -> if (s.seed != null) SeededAlbumLoading(s.seed) else SkeletonTrackList() is AlbumDetailUiState.Error -> EmptyState( diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt index 01fa2b7e..335cbf4f 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/ArtistDetailScreen.kt @@ -87,8 +87,11 @@ fun ArtistDetailScreen( onRefresh = { viewModel.refresh().join() }, modifier = Modifier.fillMaxSize().padding(inner), ) { - Crossfade(targetState = state, label = "artist-detail") { s -> - when (s) { + // Crossfade keyed on `state::class` so a Success → fresh + // Success refresh (same kind, new detail) doesn't re-fade + // the body; only Loading ↔ Success ↔ Error animate. + Crossfade(targetState = state::class, label = "artist-detail") { _ -> + when (val s = state) { is ArtistDetailUiState.Loading -> if (s.seed != null) { SeededArtistLoading(s.seed) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt index c0971a5b..ee290b44 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/LibraryScreen.kt @@ -142,8 +142,13 @@ private fun ArtistsTab( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { - Crossfade(targetState = state, label = "library-artists") { s -> - when (s) { + // Crossfade is keyed on `state::class` (not the whole state) + // so a refresh that emits a fresh UiState.Success — same kind, + // new data — doesn't re-fade the entire grid. Only Loading ↔ + // Success ↔ Error ↔ Empty transitions animate; row-level diff + // is owned by LazyVerticalGrid via its item keys. + Crossfade(targetState = state::class, label = "library-artists") { _ -> + when (val s = state) { UiState.Loading -> SkeletonArtistsGrid() UiState.Empty -> EmptyState( title = "No artists yet", @@ -170,8 +175,8 @@ private fun AlbumsTab( ) { val state by viewModel.uiState.collectAsStateWithLifecycle() PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { - Crossfade(targetState = state, label = "library-albums") { s -> - when (s) { + Crossfade(targetState = state::class, label = "library-albums") { _ -> + when (val s = state) { UiState.Loading -> SkeletonAlbumsGrid() UiState.Empty -> EmptyState( title = "No albums yet", diff --git a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt index 1504c0a4..99250266 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt @@ -288,7 +288,10 @@ private fun PlaylistDetailContent( playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel, navController: NavHostController, ) { - Crossfade(targetState = state, label = "playlist-detail") { s -> when (s) { + // Crossfade keyed on `state::class` so refreshes that emit a + // fresh Success (same kind, new detail) don't re-fade the body; + // only Loading ↔ Success ↔ Error transitions animate. + Crossfade(targetState = state::class, label = "playlist-detail") { _ -> when (val s = state) { is PlaylistDetailUiState.Loading -> s.seed?.let { SeededPlaylistLoading(it) } ?: SkeletonPlaylistTrackList() is PlaylistDetailUiState.Error -> ErrorBlock(s.message, viewModel::refresh)