fix(android): playbar clears system nav bar on detail screens; Erkunden all-albums survives drill-down

- On detail screens (album/artist/playlist) the MiniPlayer was the bottom-most bar
  with no NavigationBar to absorb the system-navigation inset, so it sat behind the
  phone's gesture/nav bar. Pad the bar with navigationBarsPadding() when the tab bar
  is absent.
- The Erkunden (iTunes) artist "show all albums" view used local remember state, so
  drilling into an album and back reset it to the main artist view. Hoist the open
  section into ExploreViewModel.artistAllAlbums so it survives both nav-route and
  overlay round-trips, and wire its back into the root overlay-back handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-17 16:21:01 +02:00
1 parent e50b4dc93c
commit 9270d31608
4 files changed
+53 -48

No files matched your search

@@ -169,8 +169,10 @@ fun AppNavigation() {
modifier = Modifier.background(NsBackground).background(bgGlow), modifier = Modifier.background(NsBackground).background(bgGlow),
bottomBar = { bottomBar = {
// MiniPlayer shows on EVERY screen (incl. album/artist/playlist detail) so playback // MiniPlayer shows on EVERY screen (incl. album/artist/playlist detail) so playback
// is always reachable; the tab bar stays top-level-only. // is always reachable; the tab bar stays top-level-only. On detail screens there is no
Column { // NavigationBar to absorb the system-navigation inset, so pad the bar itself — otherwise
// the MiniPlayer sits behind the phone's gesture/nav bar.
Column(modifier = if (isTopLevel) Modifier else Modifier.navigationBarsPadding()) {
// ABS audio routes through MusicService — one notification, one mini player. // ABS audio routes through MusicService — one notification, one mini player.
// When an ABS book is loaded, tapping the mini player opens the ABS overlay // When an ABS book is loaded, tapping the mini player opens the ABS overlay
// instead of the music NowPlaying screen. // instead of the music NowPlaying screen.
@@ -62,8 +62,9 @@ fun ExploreScreen(
// Report the current overlay's close action up; clear it when this screen leaves composition // Report the current overlay's close action up; clear it when this screen leaves composition
// (e.g. drilling into a real album route) so that screen's own back works normally. // (e.g. drilling into a real album route) so that screen's own back works normally.
val overlayKind = when { val overlayKind = when {
state.artistDetail != null && state.partialAlbum != null -> 4 // artist → partial album state.artistDetail != null && state.partialAlbum != null -> 5 // artist → partial album
state.artistDetail != null && state.albumTracks != null -> 3 // artist → iTunes album state.artistDetail != null && state.albumTracks != null -> 4 // artist → iTunes album
state.artistDetail != null && state.artistAllAlbums != null -> 3 // artist → all-albums grid
state.artistDetail != null -> 2 // iTunes artist state.artistDetail != null -> 2 // iTunes artist
state.albumTracks != null -> 1 // album straight from search state.albumTracks != null -> 1 // album straight from search
else -> 0 else -> 0
@@ -71,8 +72,9 @@ fun ExploreScreen(
DisposableEffect(overlayKind) { DisposableEffect(overlayKind) {
onOverlayBackChange( onOverlayBackChange(
when (overlayKind) { when (overlayKind) {
4 -> vm::dismissPartialAlbum 5 -> vm::dismissPartialAlbum
3 -> vm::dismissAlbum 4 -> vm::dismissAlbum
3 -> vm::closeArtistAllAlbums
2 -> vm::closeArtistDetail 2 -> vm::closeArtistDetail
1 -> vm::dismissAlbum 1 -> vm::dismissAlbum
else -> null else -> null
@@ -137,6 +139,9 @@ fun ExploreScreen(
onItunesAlbumOpen = vm::loadAlbum, onItunesAlbumOpen = vm::loadAlbum,
onItunesTrackPreview = vm::togglePreview, onItunesTrackPreview = vm::togglePreview,
previewingId = state.previewingId, previewingId = state.previewingId,
allAlbumsOpen = state.artistAllAlbums,
onOpenAllAlbums = vm::openArtistAllAlbums,
onCloseAllAlbums = vm::closeArtistAllAlbums,
) )
} }
} }
@@ -96,21 +96,44 @@ fun ItunesArtistScreen(
onItunesAlbumOpen: (ItunesItem) -> Unit, onItunesAlbumOpen: (ItunesItem) -> Unit,
onItunesTrackPreview: (ItunesItem) -> Unit, onItunesTrackPreview: (ItunesItem) -> Unit,
previewingId: Long?, previewingId: Long?,
allAlbumsOpen: String?,
onOpenAllAlbums: (String) -> Unit,
onCloseAllAlbums: () -> Unit,
) { ) {
val artist = artistState.artist val artist = artistState.artist
val cats = if (!artistState.isLoading) val cats = if (!artistState.isLoading)
categorizeAlbums(artistState.ndAlbums, artistState.iAlbums) categorizeAlbums(artistState.ndAlbums, artistState.iAlbums)
else null else null
var showAllAlbums by remember { mutableStateOf(false) } // "All albums" state is hoisted (ExploreViewModel.artistAllAlbums) so it survives an album
var allAlbumsTitle by remember { mutableStateOf("") } // drill-down and back; entries are re-derived from cats for whichever section is open.
var allAlbumEntries by remember { mutableStateOf<List<AllAlbumEntry>>(emptyList()) } val allAlbumEntries: List<AllAlbumEntry> = when (allAlbumsOpen) {
"In Library" -> cats?.downloaded?.map { (nd, _) ->
AllAlbumEntry(name = nd.name, year = nd.year?.toString(), artUrl = coverUrl(nd.coverArt), onClick = { onAlbumClick(nd) })
}.orEmpty()
"Unvollständig" -> cats?.partial?.map { (nd, ia, counts) ->
AllAlbumEntry(
name = nd.name, year = nd.year?.toString(),
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb") ?: coverUrl(nd.coverArt),
badge = "${counts.first}/${counts.second}",
onClick = { onPartialAlbumOpen(nd, ia) }, onDownload = { onDownload(ia) },
)
}.orEmpty()
"Nicht heruntergeladen" -> cats?.notDownloaded?.map { ia ->
AllAlbumEntry(
name = ia.collectionName ?: "—", year = ia.releaseDate?.take(4),
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb"), dimmed = true,
onClick = { onItunesAlbumOpen(ia) }, onDownload = { onDownload(ia) },
)
}.orEmpty()
else -> emptyList()
}
if (showAllAlbums) { if (allAlbumsOpen != null) {
AllAlbumsOverlay( AllAlbumsOverlay(
title = allAlbumsTitle, title = allAlbumsOpen,
entries = allAlbumEntries, entries = allAlbumEntries,
onBack = { showAllAlbums = false }, onBack = onCloseAllAlbums,
) )
return return
} }
@@ -224,13 +247,7 @@ fun ItunesArtistScreen(
if (c.downloaded.isNotEmpty()) { if (c.downloaded.isNotEmpty()) {
item { item {
ArtistSectionHeader("In Library", c.downloaded.size, ArtistSectionHeader("In Library", c.downloaded.size,
onShowAll = if (c.downloaded.size > 4) ({ onShowAll = if (c.downloaded.size > 4) ({ onOpenAllAlbums("In Library") }) else null
allAlbumsTitle = "In Library"
allAlbumEntries = c.downloaded.map { (nd, _) ->
AllAlbumEntry(name = nd.name, year = nd.year?.toString(), artUrl = coverUrl(nd.coverArt), onClick = { onAlbumClick(nd) })
}
showAllAlbums = true
}) else null
) )
} }
item { item {
@@ -255,20 +272,7 @@ fun ItunesArtistScreen(
if (c.partial.isNotEmpty()) { if (c.partial.isNotEmpty()) {
item { item {
ArtistSectionHeader("Unvollständig", c.partial.size, ArtistSectionHeader("Unvollständig", c.partial.size,
onShowAll = if (c.partial.size > 4) ({ onShowAll = if (c.partial.size > 4) ({ onOpenAllAlbums("Unvollständig") }) else null
allAlbumsTitle = "Unvollständig"
allAlbumEntries = c.partial.map { (nd, ia, counts) ->
AllAlbumEntry(
name = nd.name,
year = nd.year?.toString(),
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb") ?: coverUrl(nd.coverArt),
badge = "${counts.first}/${counts.second}",
onClick = { onPartialAlbumOpen(nd, ia) },
onDownload = { onDownload(ia) },
)
}
showAllAlbums = true
}) else null
) )
} }
item { item {
@@ -295,20 +299,7 @@ fun ItunesArtistScreen(
if (c.notDownloaded.isNotEmpty()) { if (c.notDownloaded.isNotEmpty()) {
item { item {
ArtistSectionHeader("Nicht heruntergeladen", c.notDownloaded.size, ArtistSectionHeader("Nicht heruntergeladen", c.notDownloaded.size,
onShowAll = if (c.notDownloaded.size > 4) ({ onShowAll = if (c.notDownloaded.size > 4) ({ onOpenAllAlbums("Nicht heruntergeladen") }) else null
allAlbumsTitle = "Nicht heruntergeladen"
allAlbumEntries = c.notDownloaded.map { ia ->
AllAlbumEntry(
name = ia.collectionName ?: "—",
year = ia.releaseDate?.take(4),
artUrl = ia.artworkUrl100?.replace("100x100bb", "300x300bb"),
dimmed = true,
onClick = { onItunesAlbumOpen(ia) },
onDownload = { onDownload(ia) },
)
}
showAllAlbums = true
}) else null
) )
} }
item { item {
@@ -50,6 +50,9 @@ data class ExploreUiState(
val albumItem: ItunesItem? = null, val albumItem: ItunesItem? = null,
// Artist detail (iTunes) // Artist detail (iTunes)
val artistDetail: ItunesArtistState? = null, val artistDetail: ItunesArtistState? = null,
// Which "all albums" section is expanded inside the iTunes artist overlay (by title), or null.
// Hoisted here so it survives drilling into an album (nav route OR overlay) and coming back.
val artistAllAlbums: String? = null,
// Partial album detail (opened from artist's "Unvollständig" section) // Partial album detail (opened from artist's "Unvollständig" section)
val partialAlbum: PartialAlbumState? = null, val partialAlbum: PartialAlbumState? = null,
// Download sheet // Download sheet
@@ -168,9 +171,13 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
_state.value = _state.value.copy(albumTracks = null, albumItem = null) _state.value = _state.value.copy(albumTracks = null, albumItem = null)
} }
fun openArtistAllAlbums(title: String) { _state.value = _state.value.copy(artistAllAlbums = title) }
fun closeArtistAllAlbums() { _state.value = _state.value.copy(artistAllAlbums = null) }
fun openArtistDetail(artist: ItunesItem) { fun openArtistDetail(artist: ItunesItem) {
_state.value = _state.value.copy( _state.value = _state.value.copy(
artistDetail = ItunesArtistState(artist = artist, isLoading = true), artistDetail = ItunesArtistState(artist = artist, isLoading = true),
artistAllAlbums = null,
) )
viewModelScope.launch { viewModelScope.launch {
val artistId = artist.artistId val artistId = artist.artistId
@@ -195,7 +202,7 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) {
} }
fun closeArtistDetail() { fun closeArtistDetail() {
_state.value = _state.value.copy(artistDetail = null, partialAlbum = null) _state.value = _state.value.copy(artistDetail = null, partialAlbum = null, artistAllAlbums = null)
} }
fun loadPartialAlbum(nd: Album, ia: ItunesItem) { fun loadPartialAlbum(nd: Album, ia: ItunesItem) {