From 9270d3160838348be42d72dd8f9739df97eca0ba Mon Sep 17 00:00:00 2001 From: Honey Date: Fri, 17 Jul 2026 16:21:01 +0200 Subject: [PATCH] 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 --- .../com/novasonic/app/ui/AppNavigation.kt | 6 +- .../novasonic/app/ui/screen/ExploreScreen.kt | 13 +++- .../app/ui/screen/ItunesArtistScreen.kt | 73 ++++++++----------- .../app/viewmodel/ExploreViewModel.kt | 9 ++- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt index 13a768a..a2c693a 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/AppNavigation.kt @@ -169,8 +169,10 @@ fun AppNavigation() { modifier = Modifier.background(NsBackground).background(bgGlow), bottomBar = { // MiniPlayer shows on EVERY screen (incl. album/artist/playlist detail) so playback - // is always reachable; the tab bar stays top-level-only. - Column { + // is always reachable; the tab bar stays top-level-only. On detail screens there is no + // 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. // When an ABS book is loaded, tapping the mini player opens the ABS overlay // instead of the music NowPlaying screen. diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt index 66019dc..f305195 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ExploreScreen.kt @@ -62,8 +62,9 @@ fun ExploreScreen( // 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. 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 && state.partialAlbum != null -> 5 // artist → partial 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.albumTracks != null -> 1 // album straight from search else -> 0 @@ -71,8 +72,9 @@ fun ExploreScreen( DisposableEffect(overlayKind) { onOverlayBackChange( when (overlayKind) { - 4 -> vm::dismissPartialAlbum - 3 -> vm::dismissAlbum + 5 -> vm::dismissPartialAlbum + 4 -> vm::dismissAlbum + 3 -> vm::closeArtistAllAlbums 2 -> vm::closeArtistDetail 1 -> vm::dismissAlbum else -> null @@ -137,6 +139,9 @@ fun ExploreScreen( onItunesAlbumOpen = vm::loadAlbum, onItunesTrackPreview = vm::togglePreview, previewingId = state.previewingId, + allAlbumsOpen = state.artistAllAlbums, + onOpenAllAlbums = vm::openArtistAllAlbums, + onCloseAllAlbums = vm::closeArtistAllAlbums, ) } } diff --git a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ItunesArtistScreen.kt b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ItunesArtistScreen.kt index eab8bd8..7a693c7 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ItunesArtistScreen.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/ui/screen/ItunesArtistScreen.kt @@ -96,21 +96,44 @@ fun ItunesArtistScreen( onItunesAlbumOpen: (ItunesItem) -> Unit, onItunesTrackPreview: (ItunesItem) -> Unit, previewingId: Long?, + allAlbumsOpen: String?, + onOpenAllAlbums: (String) -> Unit, + onCloseAllAlbums: () -> Unit, ) { val artist = artistState.artist val cats = if (!artistState.isLoading) categorizeAlbums(artistState.ndAlbums, artistState.iAlbums) else null - var showAllAlbums by remember { mutableStateOf(false) } - var allAlbumsTitle by remember { mutableStateOf("") } - var allAlbumEntries by remember { mutableStateOf>(emptyList()) } + // "All albums" state is hoisted (ExploreViewModel.artistAllAlbums) so it survives an album + // drill-down and back; entries are re-derived from cats for whichever section is open. + val allAlbumEntries: List = 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( - title = allAlbumsTitle, + title = allAlbumsOpen, entries = allAlbumEntries, - onBack = { showAllAlbums = false }, + onBack = onCloseAllAlbums, ) return } @@ -224,13 +247,7 @@ fun ItunesArtistScreen( if (c.downloaded.isNotEmpty()) { item { ArtistSectionHeader("In Library", c.downloaded.size, - onShowAll = if (c.downloaded.size > 4) ({ - 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 + onShowAll = if (c.downloaded.size > 4) ({ onOpenAllAlbums("In Library") }) else null ) } item { @@ -255,20 +272,7 @@ fun ItunesArtistScreen( if (c.partial.isNotEmpty()) { item { ArtistSectionHeader("Unvollständig", c.partial.size, - onShowAll = if (c.partial.size > 4) ({ - 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 + onShowAll = if (c.partial.size > 4) ({ onOpenAllAlbums("Unvollständig") }) else null ) } item { @@ -295,20 +299,7 @@ fun ItunesArtistScreen( if (c.notDownloaded.isNotEmpty()) { item { ArtistSectionHeader("Nicht heruntergeladen", c.notDownloaded.size, - onShowAll = if (c.notDownloaded.size > 4) ({ - 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 + onShowAll = if (c.notDownloaded.size > 4) ({ onOpenAllAlbums("Nicht heruntergeladen") }) else null ) } item { diff --git a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt index 961308c..abc3b65 100644 --- a/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt +++ b/android/app/src/main/kotlin/com/novasonic/app/viewmodel/ExploreViewModel.kt @@ -50,6 +50,9 @@ data class ExploreUiState( val albumItem: ItunesItem? = null, // Artist detail (iTunes) 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) val partialAlbum: PartialAlbumState? = null, // Download sheet @@ -168,9 +171,13 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) { _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) { _state.value = _state.value.copy( artistDetail = ItunesArtistState(artist = artist, isLoading = true), + artistAllAlbums = null, ) viewModelScope.launch { val artistId = artist.artistId @@ -195,7 +202,7 @@ class ExploreViewModel(app: Application) : AndroidViewModel(app) { } 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) {