feat(android): playbar everywhere, song-search context menu, offline tab loads offline
- MiniPlayer now shows on every screen (album/artist/playlist detail), not just top-level tabs; the tab bar stays top-level-only. - Local library song search results get the 3-dot context menu (add to playlist, favorite, queue, download, …) via onShowContextMenu. - Offline tab is fed by the local Room DB and no longer waits on the online library: it stays usable while the library is loading or the server is unreachable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
03046cd631
commit
419f80568c
3 files changed
+44
-34
No files matched your search
@@ -155,41 +155,43 @@ fun AppNavigation() {
|
|||||||
containerColor = NsBackground,
|
containerColor = NsBackground,
|
||||||
modifier = Modifier.background(NsBackground).background(bgGlow),
|
modifier = Modifier.background(NsBackground).background(bgGlow),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (isTopLevel) {
|
// MiniPlayer shows on EVERY screen (incl. album/artist/playlist detail) so playback
|
||||||
Column {
|
// is always reachable; the tab bar stays top-level-only.
|
||||||
// ABS audio routes through MusicService — one notification, one mini player.
|
Column {
|
||||||
// When an ABS book is loaded, tapping the mini player opens the ABS overlay
|
// ABS audio routes through MusicService — one notification, one mini player.
|
||||||
// instead of the music NowPlaying screen.
|
// When an ABS book is loaded, tapping the mini player opens the ABS overlay
|
||||||
val hasAbsItem = absState.item != null
|
// instead of the music NowPlaying screen.
|
||||||
val showMiniPlayer = playerState.isPlaying ||
|
val hasAbsItem = absState.item != null
|
||||||
playerState.currentSong != null ||
|
val showMiniPlayer = playerState.isPlaying ||
|
||||||
(lastPlayedSong != null && !showNowPlaying)
|
playerState.currentSong != null ||
|
||||||
AnimatedVisibility(
|
(lastPlayedSong != null && !showNowPlaying)
|
||||||
visible = showMiniPlayer,
|
AnimatedVisibility(
|
||||||
enter = slideInVertically { it } + fadeIn(tween(220)),
|
visible = showMiniPlayer,
|
||||||
exit = slideOutVertically { it } + fadeOut(tween(180)),
|
enter = slideInVertically { it } + fadeIn(tween(220)),
|
||||||
) {
|
exit = slideOutVertically { it } + fadeOut(tween(180)),
|
||||||
MiniPlayer(
|
) {
|
||||||
state = playerState,
|
MiniPlayer(
|
||||||
coverUrl = coverUrl(playerState.currentSong?.coverArt) ?: playerState.artworkUri,
|
state = playerState,
|
||||||
onTap = { if (hasAbsItem) showAbsPlayer = true else showNowPlaying = true },
|
coverUrl = coverUrl(playerState.currentSong?.coverArt) ?: playerState.artworkUri,
|
||||||
onTogglePlay = { if (hasAbsItem) absPlayerController.togglePlay() else player.togglePlay() },
|
onTap = { if (hasAbsItem) showAbsPlayer = true else showNowPlaying = true },
|
||||||
onSkipPrev = { if (hasAbsItem) absPlayerController.prevChapter() else player.skipPrev() },
|
onTogglePlay = { if (hasAbsItem) absPlayerController.togglePlay() else player.togglePlay() },
|
||||||
onSkipNext = { if (hasAbsItem) absPlayerController.nextChapter() else player.skipNext() },
|
onSkipPrev = { if (hasAbsItem) absPlayerController.prevChapter() else player.skipPrev() },
|
||||||
lastSong = if (playerState.currentSong == null) lastPlayedSong else null,
|
onSkipNext = { if (hasAbsItem) absPlayerController.nextChapter() else player.skipNext() },
|
||||||
lastSongCoverUrl = lastPlayedSong?.coverArtId?.let { coverUrl(it) },
|
lastSong = if (playerState.currentSong == null) lastPlayedSong else null,
|
||||||
onPlayLast = {
|
lastSongCoverUrl = lastPlayedSong?.coverArtId?.let { coverUrl(it) },
|
||||||
lastPlayedSong?.let { ls ->
|
onPlayLast = {
|
||||||
scope.launch {
|
lastPlayedSong?.let { ls ->
|
||||||
runCatching {
|
scope.launch {
|
||||||
val song = repo.getSong(ls.id)
|
runCatching {
|
||||||
player.playSongs(listOf(song), 0)
|
val song = repo.getSong(ls.id)
|
||||||
}
|
player.playSongs(listOf(song), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
},
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
if (isTopLevel) {
|
||||||
NavigationBar(containerColor = NavBg, tonalElevation = 0.dp) {
|
NavigationBar(containerColor = NavBg, tonalElevation = 0.dp) {
|
||||||
navItems.forEach { dest ->
|
navItems.forEach { dest ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
@@ -272,6 +274,7 @@ fun AppNavigation() {
|
|||||||
onSongPlay = { songs, idx -> player.playSongs(songs, idx) },
|
onSongPlay = { songs, idx -> player.playSongs(songs, idx) },
|
||||||
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,
|
||||||
onOpenImporter = { navController.navigate("importer") },
|
onOpenImporter = { navController.navigate("importer") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ fun ExploreScreen(
|
|||||||
onSongPlay: (List<com.novasonic.app.api.model.Song>, Int) -> Unit,
|
onSongPlay: (List<com.novasonic.app.api.model.Song>, Int) -> Unit,
|
||||||
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,
|
||||||
onOpenImporter: () -> Unit = {},
|
onOpenImporter: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val vm: ExploreViewModel = viewModel()
|
val vm: ExploreViewModel = viewModel()
|
||||||
@@ -214,6 +215,7 @@ fun ExploreScreen(
|
|||||||
onSongPlay = onSongPlay,
|
onSongPlay = onSongPlay,
|
||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
onArtistClick = onArtistClick,
|
onArtistClick = onArtistClick,
|
||||||
|
onShowContextMenu = onShowContextMenu,
|
||||||
)
|
)
|
||||||
ExploreTab.GLOBAL -> GlobalExploreContent(
|
ExploreTab.GLOBAL -> GlobalExploreContent(
|
||||||
state = state,
|
state = state,
|
||||||
@@ -238,6 +240,7 @@ private fun LibrarySearchContent(
|
|||||||
onSongPlay: (List<com.novasonic.app.api.model.Song>, Int) -> Unit,
|
onSongPlay: (List<com.novasonic.app.api.model.Song>, Int) -> Unit,
|
||||||
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,
|
||||||
) {
|
) {
|
||||||
// Reuse SearchScreen's ViewModel
|
// Reuse SearchScreen's ViewModel
|
||||||
val vm: com.novasonic.app.viewmodel.SearchViewModel = viewModel()
|
val vm: com.novasonic.app.viewmodel.SearchViewModel = viewModel()
|
||||||
@@ -315,7 +318,9 @@ private fun LibrarySearchContent(
|
|||||||
song = song,
|
song = song,
|
||||||
coverUrl = coverUrl(song.coverArt),
|
coverUrl = coverUrl(song.coverArt),
|
||||||
isPlaying = playerState.currentSong?.id == song.id,
|
isPlaying = playerState.currentSong?.id == song.id,
|
||||||
|
starred = song.starred != null,
|
||||||
onPlay = { onSongPlay(songs, idx) },
|
onPlay = { onSongPlay(songs, idx) },
|
||||||
|
onMore = onShowContextMenu?.let { cb -> { cb(song) } },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,7 +108,9 @@ fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isLoading) {
|
// The Offline tab is fed by the local Room DB (offlineGroups) and must stay usable even while
|
||||||
|
// the online library is still loading or unreachable — only gate the network-backed tabs.
|
||||||
|
if (state.isLoading && state.tab != LibraryTab.OFFLINE) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
CircularProgressIndicator(color = NsAccent)
|
CircularProgressIndicator(color = NsAccent)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user