feat(android): MiniPlayer TrackActions kebab + shell-level snackbar

Closes the deferred MiniPlayer follow-up from the TrackActions slice.
MiniPlayer gains a TrackActionsButton next to play/pause with
hideQueueActions=true (the playing track is the queue entry itself).

Shell architecture: ShellScaffold now takes navController + owns a
shell-scoped SnackbarHost backed by a TrackActionsViewModel
hiltViewModel() at the shell level. Snackbars triggered by the
MiniPlayer's kebab surface there; per-screen kebabs continue to
flow through each screen's own Scaffold SnackbarHost (independent
collectors so the two never compete).

All 14 ShellScaffold call sites in MinstrelNavGraph updated to pass
navController; mechanical sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 18:08:20 -04:00
parent c9bf0479ec
commit 4a3215cc07
3 changed files with 54 additions and 17 deletions
@@ -60,42 +60,42 @@ private fun NavGraphBuilder.inShellTopLevel(
expandPlayer: () -> Unit,
) {
composable<Home> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
HomeScreen(navController = navController)
}
}
composable<Library> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
LibraryScreen(navController = navController)
}
}
composable<Search> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
SearchScreen(navController = navController)
}
}
composable<Discover> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
DiscoverScreen(navController = navController)
}
}
composable<Playlists> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
PlaylistsListScreen(navController = navController)
}
}
composable<Settings> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
SettingsScreen(navController = navController)
}
}
composable<Admin> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminLandingScreen(navController = navController)
}
}
composable<Requests> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
RequestsScreen(navController = navController)
}
}
@@ -106,32 +106,32 @@ private fun NavGraphBuilder.inShellDetail(
expandPlayer: () -> Unit,
) {
composable<AlbumDetail> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AlbumDetailScreen(navController = navController)
}
}
composable<ArtistDetail> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
ArtistDetailScreen(navController = navController)
}
}
composable<PlaylistDetail> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
PlaylistDetailScreen(navController = navController)
}
}
composable<AdminRequests> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminRequestsScreen(navController = navController)
}
}
composable<AdminQuarantine> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminQuarantineScreen(navController = navController)
}
}
composable<AdminUsers> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminUsersScreen(navController = navController)
}
}
@@ -32,6 +32,7 @@ import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music
import com.composables.icons.lucide.Pause
import com.composables.icons.lucide.Play
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
private const val MINI_BAR_HEIGHT_DP = 64
@@ -77,6 +78,8 @@ private fun MiniCover(coverUrl: String, contentDescription: String) {
@Composable
fun MiniPlayer(
onExpandClick: () -> Unit,
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: PlayerViewModel = hiltViewModel(),
) {
@@ -122,6 +125,14 @@ fun MiniPlayer(
tint = MaterialTheme.colorScheme.onSurface,
)
}
// hideQueueActions=true: the playing track is itself the
// queue entry, so Play next / Add to queue would be silly.
TrackActionsButton(
track = track,
hideQueueActions = true,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
}
}
@@ -4,14 +4,26 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.player.ui.MiniPlayer
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
/**
* Outer shell wrapper for "in-app" routes — banners on top
* (conditional, none implemented yet), routed screen filling the
* middle, MiniPlayer pinned at the bottom (auto-hides when no track).
* A shell-scoped SnackbarHost above the MiniPlayer surfaces transient
* messages emitted by the MiniPlayer's TrackActionsButton (snackbars
* for in-screen kebabs continue to live on each screen's own Scaffold).
*
* Mirrors the Flutter `_ShellWithPlayerBar` (lib/shared/routing.dart).
*
@@ -21,15 +33,24 @@ import com.fabledsword.minstrel.player.ui.MiniPlayer
* intentionally do NOT — they fill the entire screen, often with a
* back-only AppBar.
*
* `onExpandPlayer` is the navigate-to-NowPlaying callback; passed
* through to MiniPlayer so it doesn't have to know the nav graph.
* [onExpandPlayer] is the navigate-to-NowPlaying callback passed
* through to MiniPlayer's tap-handler. [navController] is needed by
* the MiniPlayer kebab for go-to-album / go-to-artist nav.
*/
@Composable
fun ShellScaffold(
navController: NavHostController,
onExpandPlayer: () -> Unit,
modifier: Modifier = Modifier,
trackActionsViewModel: TrackActionsViewModel = hiltViewModel(),
content: @Composable () -> Unit,
) {
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
trackActionsViewModel.transientMessages.collect { msg ->
snackbarHostState.showSnackbar(msg)
}
}
Column(modifier = modifier.fillMaxSize()) {
// Banners (VersionTooOld, UpdateBanner) live here once those
// mechanisms exist; for now the slot is empty so the layout
@@ -37,6 +58,11 @@ fun ShellScaffold(
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
content()
}
MiniPlayer(onExpandClick = onExpandPlayer)
SnackbarHost(hostState = snackbarHostState)
MiniPlayer(
onExpandClick = onExpandPlayer,
onNavigateToAlbum = { id -> navController.navigate(AlbumDetail(id)) },
onNavigateToArtist = { id -> navController.navigate(ArtistDetail(id)) },
)
}
}