feat(android): TrackActions kebab on NowPlaying with hideQueueActions

NowPlaying gains the 7-item TrackActions menu in its bottom action
row alongside the View Queue button. hideQueueActions=true suppresses
Play next / Add to queue since the menu's track IS the playing one.
Go to album / artist pops NowPlaying first (it's a full-screen
overlay) before navigating, mirroring Flutter's shell-route hook.

Adds a thin Scaffold around the screen body so the TrackActions
transient messages have a SnackbarHost to surface in.

MiniPlayer kebab still deferred — needs shell-level snackbar
plumbing that lives outside this slice's scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 12:24:13 -04:00
parent 965ec412d1
commit 3f00006b74
@@ -17,11 +17,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -40,8 +45,12 @@ import com.composables.icons.lucide.Pause
import com.composables.icons.lucide.Play import com.composables.icons.lucide.Play
import com.composables.icons.lucide.SkipBack import com.composables.icons.lucide.SkipBack
import com.composables.icons.lucide.SkipForward import com.composables.icons.lucide.SkipForward
import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.Queue import com.fabledsword.minstrel.nav.Queue
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
import com.fabledsword.minstrel.theme.LocalActionColors import com.fabledsword.minstrel.theme.LocalActionColors
@@ -61,8 +70,15 @@ private const val SECONDS_PER_MINUTE = 60L
fun NowPlayingScreen( fun NowPlayingScreen(
navController: NavHostController, navController: NavHostController,
viewModel: PlayerViewModel = hiltViewModel(), viewModel: PlayerViewModel = hiltViewModel(),
trackActionsViewModel: TrackActionsViewModel = hiltViewModel(),
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
trackActionsViewModel.transientMessages.collect { msg ->
snackbarHostState.showSnackbar(msg)
}
}
val track = state.currentTrack val track = state.currentTrack
if (track == null) { if (track == null) {
EmptyState( EmptyState(
@@ -71,35 +87,59 @@ fun NowPlayingScreen(
) )
return return
} }
Column( Scaffold(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize() snackbarHost = { SnackbarHost(snackbarHostState) },
.padding(horizontal = 24.dp), containerColor = MaterialTheme.colorScheme.background,
verticalArrangement = Arrangement.Center, ) { inner ->
horizontalAlignment = Alignment.CenterHorizontally, Column(
) { modifier = Modifier
NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title) .fillMaxSize()
Spacer(Modifier.height(24.dp)) .padding(inner)
TrackHeader( .padding(horizontal = 24.dp),
title = track.title, verticalArrangement = Arrangement.Center,
artist = track.artistName, horizontalAlignment = Alignment.CenterHorizontally,
album = track.albumTitle, ) {
) NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
ScrubberRow( TrackHeader(
positionMs = state.positionMs, title = track.title,
durationMs = state.durationMs, artist = track.artistName,
onSeek = viewModel::seekTo, album = track.albumTitle,
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(24.dp))
TransportRow( ScrubberRow(
isPlaying = state.isPlaying, positionMs = state.positionMs,
onPrev = viewModel::skipToPrevious, durationMs = state.durationMs,
onPlayPause = { if (state.isPlaying) viewModel.pause() else viewModel.play() }, onSeek = viewModel::seekTo,
onNext = viewModel::skipToNext, )
) Spacer(Modifier.height(8.dp))
Spacer(Modifier.height(16.dp)) TransportRow(
ViewQueueButton(onClick = { navController.navigate(Queue) }) isPlaying = state.isPlaying,
onPrev = viewModel::skipToPrevious,
onPlayPause = { if (state.isPlaying) viewModel.pause() else viewModel.play() },
onNext = viewModel::skipToNext,
)
Spacer(Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ViewQueueButton(onClick = { navController.navigate(Queue) })
TrackActionsButton(
track = track,
hideQueueActions = true,
onNavigateToAlbum = { id ->
navController.popBackStack()
navController.navigate(AlbumDetail(id))
},
onNavigateToArtist = { id ->
navController.popBackStack()
navController.navigate(ArtistDetail(id))
},
)
}
}
} }
} }