From 3f00006b74d5bebb0b6f8bf119e7e52a20695014 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 26 May 2026 12:24:13 -0400 Subject: [PATCH] feat(android): TrackActions kebab on NowPlaying with hideQueueActions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../minstrel/player/ui/NowPlayingScreen.kt | 98 +++++++++++++------ 1 file changed, 69 insertions(+), 29 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt index 587cb278..05bbd02e 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/NowPlayingScreen.kt @@ -17,11 +17,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.SkipBack 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.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.LocalActionColors @@ -61,8 +70,15 @@ private const val SECONDS_PER_MINUTE = 60L fun NowPlayingScreen( navController: NavHostController, viewModel: PlayerViewModel = hiltViewModel(), + trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { + trackActionsViewModel.transientMessages.collect { msg -> + snackbarHostState.showSnackbar(msg) + } + } val track = state.currentTrack if (track == null) { EmptyState( @@ -71,35 +87,59 @@ fun NowPlayingScreen( ) return } - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 24.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title) - Spacer(Modifier.height(24.dp)) - TrackHeader( - title = track.title, - artist = track.artistName, - album = track.albumTitle, - ) - Spacer(Modifier.height(24.dp)) - ScrubberRow( - positionMs = state.positionMs, - durationMs = state.durationMs, - onSeek = viewModel::seekTo, - ) - Spacer(Modifier.height(8.dp)) - TransportRow( - isPlaying = state.isPlaying, - onPrev = viewModel::skipToPrevious, - onPlayPause = { if (state.isPlaying) viewModel.pause() else viewModel.play() }, - onNext = viewModel::skipToNext, - ) - Spacer(Modifier.height(16.dp)) - ViewQueueButton(onClick = { navController.navigate(Queue) }) + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { SnackbarHost(snackbarHostState) }, + containerColor = MaterialTheme.colorScheme.background, + ) { inner -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(inner) + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title) + Spacer(Modifier.height(24.dp)) + TrackHeader( + title = track.title, + artist = track.artistName, + album = track.albumTitle, + ) + Spacer(Modifier.height(24.dp)) + ScrubberRow( + positionMs = state.positionMs, + durationMs = state.durationMs, + onSeek = viewModel::seekTo, + ) + Spacer(Modifier.height(8.dp)) + TransportRow( + 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)) + }, + ) + } + } } }