fix(android): Erkunden (Apple) overlay back no longer jumps to Home

The iTunes artist / album detail in the Erkunden tab are in-screen overlays on the
NavHost "erkunden" destination, not real routes. Their BackHandler lost back priority
to the NavHost, so one back press popped Erkunden → Home instead of dismissing the
overlay (artist→search, album→artist).

Handle the overlay's back at the app root via OnBackPressedCallback (the same proven
mechanism as the NowPlaying overlay): ExploreScreen reports its current overlay close
action up through onOverlayBackChange, and AppNavigation registers a root-level back
callback for it. Also covers an album opened straight from search (no artist), which
had no back handling at all. Removes the temporary nav-debug strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-17 11:11:52 +02:00
1 parent b07212fdce
commit e50b4dc93c
2 files changed
+38 -20

No files matched your search

@@ -96,6 +96,10 @@ fun AppNavigation() {
var showNotifications by remember { mutableStateOf(false) } var showNotifications by remember { mutableStateOf(false) }
var selectedAbsItem by remember { mutableStateOf<AbsLibraryItem?>(null) } var selectedAbsItem by remember { mutableStateOf<AbsLibraryItem?>(null) }
var showAbsPlayer by remember { mutableStateOf(false) } var showAbsPlayer by remember { mutableStateOf(false) }
// Close action for the Erkunden (Apple) in-screen overlay (iTunes artist / album). Handled at the
// app root because a BackHandler inside the NavHost "erkunden" destination loses back priority to
// the NavHost, which pops Erkunden → Home instead of dismissing the overlay.
var exploreOverlayBack by remember { mutableStateOf<(() -> Unit)?>(null) }
val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher val backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
DisposableEffect(showNowPlaying, backDispatcher) { DisposableEffect(showNowPlaying, backDispatcher) {
@@ -122,6 +126,15 @@ fun AppNavigation() {
backDispatcher.addCallback(cb) backDispatcher.addCallback(cb)
onDispose { cb.remove() } onDispose { cb.remove() }
} }
DisposableEffect(exploreOverlayBack, backDispatcher) {
val action = exploreOverlayBack
if (action == null || backDispatcher == null) return@DisposableEffect onDispose {}
val cb = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { action() }
}
backDispatcher.addCallback(cb)
onDispose { cb.remove() }
}
fun coverUrl(coverId: String?) = repo.coverArtUrl(coverId, 600) fun coverUrl(coverId: String?) = repo.coverArtUrl(coverId, 600)
fun heroCoverUrl(coverId: String?) = repo.coverArtUrl(coverId, 1000) fun heroCoverUrl(coverId: String?) = repo.coverArtUrl(coverId, 1000)
@@ -275,6 +288,7 @@ fun AppNavigation() {
onAlbumClick = { album -> navController.navigate("album/${album.id}") }, onAlbumClick = { album -> navController.navigate("album/${album.id}") },
onArtistClick = { artist -> navController.navigate("artist/${artist.id}") }, onArtistClick = { artist -> navController.navigate("artist/${artist.id}") },
onShowContextMenu = ::showContextMenu, onShowContextMenu = ::showContextMenu,
onOverlayBackChange = { exploreOverlayBack = it },
onOpenImporter = { navController.navigate("importer") }, onOpenImporter = { navController.navigate("importer") },
) )
} }
@@ -395,21 +409,6 @@ fun AppNavigation() {
} }
} }
// ── TEMP NAV DEBUG — zeigt den Back-Stack; wird nach der Diagnose entfernt ──
val dbgBackStack by navController.currentBackStack.collectAsStateWithLifecycle()
Box(Modifier.fillMaxWidth().statusBarsPadding(), contentAlignment = androidx.compose.ui.Alignment.TopCenter) {
Text(
"🧭 " + dbgBackStack.mapNotNull { it.destination.route?.substringBefore("/") }
.filter { it != "null" }.joinToString(" › "),
color = Color(0xFFFFEB3B),
fontSize = 9.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.background(Color(0xCC000000))
.padding(horizontal = 6.dp, vertical = 2.dp),
)
}
// NowPlaying overlay // NowPlaying overlay
AnimatedVisibility( AnimatedVisibility(
visible = showNowPlaying, visible = showNowPlaying,
@@ -1,6 +1,5 @@
package com.novasonic.app.ui.screen package com.novasonic.app.ui.screen
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -50,6 +49,7 @@ fun ExploreScreen(
onAlbumClick: (com.novasonic.app.api.model.Album) -> Unit, onAlbumClick: (com.novasonic.app.api.model.Album) -> Unit,
onArtistClick: (com.novasonic.app.api.model.Artist) -> Unit, onArtistClick: (com.novasonic.app.api.model.Artist) -> Unit,
onShowContextMenu: ((com.novasonic.app.api.model.Song) -> Unit)? = null, onShowContextMenu: ((com.novasonic.app.api.model.Song) -> Unit)? = null,
onOverlayBackChange: ((() -> Unit)?) -> Unit = {},
onOpenImporter: () -> Unit = {}, onOpenImporter: () -> Unit = {},
) { ) {
val vm: ExploreViewModel = viewModel() val vm: ExploreViewModel = viewModel()
@@ -57,10 +57,29 @@ fun ExploreScreen(
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val context = LocalContext.current val context = LocalContext.current
// BackHandlers for state-based navigation (must be unconditional) // Overlay back is handled at the app root (see AppNavigation.exploreOverlayBack): a BackHandler
BackHandler(enabled = state.artistDetail != null && state.partialAlbum != null) { vm.dismissPartialAlbum() } // inside this NavHost destination loses back priority to the NavHost, which pops Erkunden → Home.
BackHandler(enabled = state.artistDetail != null && state.albumTracks != null && state.partialAlbum == null) { vm.dismissAlbum() } // Report the current overlay's close action up; clear it when this screen leaves composition
BackHandler(enabled = state.artistDetail != null && state.albumTracks == null && state.partialAlbum == null) { vm.closeArtistDetail() } // (e.g. drilling into a real album route) so that screen's own back works normally.
val overlayKind = when {
state.artistDetail != null && state.partialAlbum != null -> 4 // artist → partial album
state.artistDetail != null && state.albumTracks != null -> 3 // artist → iTunes album
state.artistDetail != null -> 2 // iTunes artist
state.albumTracks != null -> 1 // album straight from search
else -> 0
}
DisposableEffect(overlayKind) {
onOverlayBackChange(
when (overlayKind) {
4 -> vm::dismissPartialAlbum
3 -> vm::dismissAlbum
2 -> vm::closeArtistDetail
1 -> vm::dismissAlbum
else -> null
}
)
onDispose { onOverlayBackChange(null) }
}
// Download sheet // Download sheet
state.downloadSheetItem?.let { item -> state.downloadSheetItem?.let { item ->