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:
@@ -60,42 +60,42 @@ private fun NavGraphBuilder.inShellTopLevel(
|
|||||||
expandPlayer: () -> Unit,
|
expandPlayer: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composable<Home> {
|
composable<Home> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
HomeScreen(navController = navController)
|
HomeScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Library> {
|
composable<Library> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
LibraryScreen(navController = navController)
|
LibraryScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Search> {
|
composable<Search> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
SearchScreen(navController = navController)
|
SearchScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Discover> {
|
composable<Discover> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
DiscoverScreen(navController = navController)
|
DiscoverScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Playlists> {
|
composable<Playlists> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
PlaylistsListScreen(navController = navController)
|
PlaylistsListScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Settings> {
|
composable<Settings> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
SettingsScreen(navController = navController)
|
SettingsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Admin> {
|
composable<Admin> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
AdminLandingScreen(navController = navController)
|
AdminLandingScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<Requests> {
|
composable<Requests> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
RequestsScreen(navController = navController)
|
RequestsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,32 +106,32 @@ private fun NavGraphBuilder.inShellDetail(
|
|||||||
expandPlayer: () -> Unit,
|
expandPlayer: () -> Unit,
|
||||||
) {
|
) {
|
||||||
composable<AlbumDetail> {
|
composable<AlbumDetail> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
AlbumDetailScreen(navController = navController)
|
AlbumDetailScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<ArtistDetail> {
|
composable<ArtistDetail> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
ArtistDetailScreen(navController = navController)
|
ArtistDetailScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<PlaylistDetail> {
|
composable<PlaylistDetail> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
PlaylistDetailScreen(navController = navController)
|
PlaylistDetailScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<AdminRequests> {
|
composable<AdminRequests> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
AdminRequestsScreen(navController = navController)
|
AdminRequestsScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<AdminQuarantine> {
|
composable<AdminQuarantine> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
AdminQuarantineScreen(navController = navController)
|
AdminQuarantineScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable<AdminUsers> {
|
composable<AdminUsers> {
|
||||||
ShellScaffold(onExpandPlayer = expandPlayer) {
|
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
|
||||||
AdminUsersScreen(navController = navController)
|
AdminUsersScreen(navController = navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import com.composables.icons.lucide.Lucide
|
|||||||
import com.composables.icons.lucide.Music
|
import com.composables.icons.lucide.Music
|
||||||
import com.composables.icons.lucide.Pause
|
import com.composables.icons.lucide.Pause
|
||||||
import com.composables.icons.lucide.Play
|
import com.composables.icons.lucide.Play
|
||||||
|
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
|
||||||
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||||
|
|
||||||
private const val MINI_BAR_HEIGHT_DP = 64
|
private const val MINI_BAR_HEIGHT_DP = 64
|
||||||
@@ -77,6 +78,8 @@ private fun MiniCover(coverUrl: String, contentDescription: String) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun MiniPlayer(
|
fun MiniPlayer(
|
||||||
onExpandClick: () -> Unit,
|
onExpandClick: () -> Unit,
|
||||||
|
onNavigateToAlbum: (String) -> Unit,
|
||||||
|
onNavigateToArtist: (String) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: PlayerViewModel = hiltViewModel(),
|
viewModel: PlayerViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
@@ -122,6 +125,14 @@ fun MiniPlayer(
|
|||||||
tint = MaterialTheme.colorScheme.onSurface,
|
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.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.player.ui.MiniPlayer
|
||||||
|
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outer shell wrapper for "in-app" routes — banners on top
|
* Outer shell wrapper for "in-app" routes — banners on top
|
||||||
* (conditional, none implemented yet), routed screen filling the
|
* (conditional, none implemented yet), routed screen filling the
|
||||||
* middle, MiniPlayer pinned at the bottom (auto-hides when no track).
|
* 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).
|
* 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
|
* intentionally do NOT — they fill the entire screen, often with a
|
||||||
* back-only AppBar.
|
* back-only AppBar.
|
||||||
*
|
*
|
||||||
* `onExpandPlayer` is the navigate-to-NowPlaying callback; passed
|
* [onExpandPlayer] is the navigate-to-NowPlaying callback passed
|
||||||
* through to MiniPlayer so it doesn't have to know the nav graph.
|
* through to MiniPlayer's tap-handler. [navController] is needed by
|
||||||
|
* the MiniPlayer kebab for go-to-album / go-to-artist nav.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ShellScaffold(
|
fun ShellScaffold(
|
||||||
|
navController: NavHostController,
|
||||||
onExpandPlayer: () -> Unit,
|
onExpandPlayer: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
trackActionsViewModel: TrackActionsViewModel = hiltViewModel(),
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
trackActionsViewModel.transientMessages.collect { msg ->
|
||||||
|
snackbarHostState.showSnackbar(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
Column(modifier = modifier.fillMaxSize()) {
|
||||||
// Banners (VersionTooOld, UpdateBanner) live here once those
|
// Banners (VersionTooOld, UpdateBanner) live here once those
|
||||||
// mechanisms exist; for now the slot is empty so the layout
|
// mechanisms exist; for now the slot is empty so the layout
|
||||||
@@ -37,6 +58,11 @@ fun ShellScaffold(
|
|||||||
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
MiniPlayer(onExpandClick = onExpandPlayer)
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
|
MiniPlayer(
|
||||||
|
onExpandClick = onExpandPlayer,
|
||||||
|
onNavigateToAlbum = { id -> navController.navigate(AlbumDetail(id)) },
|
||||||
|
onNavigateToArtist = { id -> navController.navigate(ArtistDetail(id)) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user