From c29d1e0b3df8cfe986440dc14a534fe5e0f5b0a5 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 26 May 2026 22:04:59 -0400 Subject: [PATCH] feat(android): now-playing row highlight on track surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit v2 priority #9. Track rows on AlbumDetail, PlaylistDetail, LikedTab, HistoryTab, and SearchScreen now render the currently- playing track's title in accent (primary) color so the user can spot "this is what's playing" while scrolling a list. Pattern: each screen pulls a PlayerViewModel via hiltViewModel(), reads state.currentTrack?.id, threads it through to its track row composable as nowPlaying: Boolean. Row picks titleColor based on the flag. No new infrastructure — just a thread-through. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/history/ui/HistoryTab.kt | 14 ++++++++++- .../minstrel/library/ui/AlbumDetailScreen.kt | 13 +++++++++- .../fabledsword/minstrel/likes/ui/LikedTab.kt | 14 ++++++++++- .../playlists/ui/PlaylistDetailScreen.kt | 13 +++++++++- .../minstrel/search/ui/SearchScreen.kt | 25 +++++++++++++++++-- 5 files changed, 73 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt index a5711036..3019d419 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/history/ui/HistoryTab.kt @@ -100,8 +100,11 @@ fun HistoryTab( onNavigateToAlbum: (String) -> Unit = {}, onNavigateToArtist: (String) -> Unit = {}, viewModel: HistoryTabViewModel = hiltViewModel(), + playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + val playerState by playerViewModel.uiState.collectAsStateWithLifecycle() + val playingTrackId = playerState.currentTrack?.id PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { when (val s = state) { HistoryUiState.Loading -> LoadingCentered() @@ -115,6 +118,7 @@ fun HistoryTab( ) is HistoryUiState.Success -> HistoryList( entries = s.entries, + playingTrackId = playingTrackId, onPlay = viewModel::playTrack, onNavigateToAlbum = onNavigateToAlbum, onNavigateToArtist = onNavigateToArtist, @@ -126,6 +130,7 @@ fun HistoryTab( @Composable private fun HistoryList( entries: List, + playingTrackId: String?, onPlay: (TrackRef) -> Unit, onNavigateToAlbum: (String) -> Unit, onNavigateToArtist: (String) -> Unit, @@ -134,6 +139,7 @@ private fun HistoryList( items(items = entries, key = { it.id }) { entry -> HistoryRow( entry = entry, + nowPlaying = entry.track.id == playingTrackId, onClick = { onPlay(entry.track) }, onNavigateToAlbum = onNavigateToAlbum, onNavigateToArtist = onNavigateToArtist, @@ -146,10 +152,16 @@ private fun HistoryList( @Composable private fun HistoryRow( entry: HistoryEntry, + nowPlaying: Boolean, onClick: () -> Unit, onNavigateToAlbum: (String) -> Unit, onNavigateToArtist: (String) -> Unit, ) { + val titleColor = if (nowPlaying) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } Row( modifier = Modifier .fillMaxWidth() @@ -163,7 +175,7 @@ private fun HistoryRow( Text( text = entry.track.title, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, + color = titleColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt index b49aceea..0eb18fad 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/ui/AlbumDetailScreen.kt @@ -67,6 +67,7 @@ fun AlbumDetailScreen( navController: NavHostController, viewModel: AlbumDetailViewModel = hiltViewModel(), trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), + playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val snackbarHostState = remember { SnackbarHostState() } @@ -102,10 +103,12 @@ fun AlbumDetailScreen( is AlbumDetailUiState.Success -> { val albumLiked by viewModel.albumLiked.collectAsStateWithLifecycle() val likedTrackIds by viewModel.likedTrackIds.collectAsStateWithLifecycle() + val playerState by playerViewModel.uiState.collectAsStateWithLifecycle() AlbumBody( detail = s.detail, albumLiked = albumLiked, likedTrackIds = likedTrackIds, + playingTrackId = playerState.currentTrack?.id, onPlayAll = { viewModel.play(startTrackId = null) }, onShuffleAll = viewModel::shuffle, onTrackClick = { id -> viewModel.play(startTrackId = id) }, @@ -130,6 +133,7 @@ private fun AlbumBody( detail: AlbumDetailRef, albumLiked: Boolean, likedTrackIds: Set, + playingTrackId: String?, onPlayAll: () -> Unit, onShuffleAll: () -> Unit, onTrackClick: (String) -> Unit, @@ -156,6 +160,7 @@ private fun AlbumBody( TrackRow( track = track, liked = track.id in likedTrackIds, + nowPlaying = track.id == playingTrackId, onClick = { onTrackClick(track.id) }, onToggleLike = { onToggleTrackLike(track.id) }, onNavigateToAlbum = onNavigateToAlbum, @@ -274,11 +279,17 @@ private fun trackCountLine(album: AlbumRef): String { private fun TrackRow( track: TrackRef, liked: Boolean, + nowPlaying: Boolean, onClick: () -> Unit, onToggleLike: () -> Unit, onNavigateToAlbum: (String) -> Unit, onNavigateToArtist: (String) -> Unit, ) { + val titleColor = if (nowPlaying) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } Row( modifier = Modifier .fillMaxWidth() @@ -296,7 +307,7 @@ private fun TrackRow( Text( text = track.title, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, + color = titleColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt index a3faed34..18db9030 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/likes/ui/LikedTab.kt @@ -124,8 +124,11 @@ class LikedTabViewModel @Inject constructor( fun LikedTab( navController: NavHostController, viewModel: LikedTabViewModel = hiltViewModel(), + playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + val playerState by playerViewModel.uiState.collectAsStateWithLifecycle() + val playingTrackId = playerState.currentTrack?.id PullToRefreshScaffold(onRefresh = { viewModel.refresh().join() }) { when (val s = state) { LikedTabUiState.Loading -> LoadingCentered() @@ -140,6 +143,7 @@ fun LikedTab( ) is LikedTabUiState.Success -> LikedContent( sections = s.sections, + playingTrackId = playingTrackId, onArtistClick = { id -> navController.navigate(ArtistDetail(id)) }, onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) }, onTrackClick = { index -> @@ -155,6 +159,7 @@ fun LikedTab( @Composable private fun LikedContent( sections: LikedSections, + playingTrackId: String?, onArtistClick: (String) -> Unit, onAlbumClick: (String) -> Unit, onTrackClick: (Int) -> Unit, @@ -196,6 +201,7 @@ private fun LikedContent( itemsIndexed(items = sections.tracks, key = { _, t -> "trk-${t.id}" }) { index, track -> LikedTrackRow( track = track, + nowPlaying = track.id == playingTrackId, onClick = { onTrackClick(index) }, onNavigateToAlbum = onNavigateToAlbum, onNavigateToArtist = onNavigateToArtist, @@ -232,10 +238,16 @@ private fun SectionHeader(label: String, count: Int) { @Composable private fun LikedTrackRow( track: TrackRef, + nowPlaying: Boolean, onClick: () -> Unit, onNavigateToAlbum: (String) -> Unit, onNavigateToArtist: (String) -> Unit, ) { + val titleColor = if (nowPlaying) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } Row( modifier = Modifier .fillMaxWidth() @@ -249,7 +261,7 @@ private fun LikedTrackRow( Text( text = track.title, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, + color = titleColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt index 34e7c3e8..9129d521 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/playlists/ui/PlaylistDetailScreen.kt @@ -197,6 +197,7 @@ fun PlaylistDetailScreen( navController: NavHostController, viewModel: PlaylistDetailViewModel = hiltViewModel(), trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), + playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsState() val snackbarHostState = remember { SnackbarHostState() } @@ -231,9 +232,11 @@ fun PlaylistDetailScreen( is PlaylistDetailUiState.Error -> ErrorBlock(s.message, viewModel::refresh) is PlaylistDetailUiState.Success -> { val likedTrackIds by viewModel.likedTrackIds.collectAsState() + val playerState by playerViewModel.uiState.collectAsState() PlaylistDetailBody( detail = s.detail, likedTrackIds = likedTrackIds, + playingTrackId = playerState.currentTrack?.id, onPlayAll = { viewModel.play(s.detail.tracks, startTrackId = null) }, onShuffleAll = { viewModel.play(s.detail.tracks.shuffled(), startTrackId = null) @@ -262,6 +265,7 @@ private fun currentTitle(state: PlaylistDetailUiState): String = when (state) { private fun PlaylistDetailBody( detail: PlaylistDetailRef, likedTrackIds: Set, + playingTrackId: String?, onPlayAll: () -> Unit, onShuffleAll: () -> Unit, onTrackClick: (String) -> Unit, @@ -279,6 +283,7 @@ private fun PlaylistDetailBody( TrackRow( row = row, liked = row.trackId != null && row.trackId in likedTrackIds, + nowPlaying = row.trackId != null && row.trackId == playingTrackId, onClick = { row.trackId?.let(onTrackClick) }, onToggleLike = { row.trackId?.let(onToggleTrackLike) }, onNavigateToAlbum = onNavigateToAlbum, @@ -389,6 +394,7 @@ private fun PlaylistCover(playlist: PlaylistRef) { private fun TrackRow( row: PlaylistTrackRef, liked: Boolean, + nowPlaying: Boolean, onClick: () -> Unit, onToggleLike: () -> Unit, onNavigateToAlbum: (String) -> Unit, @@ -396,6 +402,11 @@ private fun TrackRow( ) { val enabled = row.isAvailable val rowAlpha = if (enabled) 1f else UNAVAILABLE_ALPHA + val baseTitleColor = if (nowPlaying) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } Row( modifier = Modifier .fillMaxWidth() @@ -412,7 +423,7 @@ private fun TrackRow( Text( text = row.title, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = rowAlpha), + color = baseTitleColor.copy(alpha = rowAlpha), maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt index 05619f2b..e548a11d 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/search/ui/SearchScreen.kt @@ -66,8 +66,11 @@ fun SearchScreen( navController: NavHostController, viewModel: SearchViewModel = hiltViewModel(), trackActionsViewModel: TrackActionsViewModel = hiltViewModel(), + playerViewModel: com.fabledsword.minstrel.player.ui.PlayerViewModel = hiltViewModel(), ) { val state by viewModel.state.collectAsStateWithLifecycle() + val playerState by playerViewModel.uiState.collectAsStateWithLifecycle() + val playingTrackId = playerState.currentTrack?.id val focusRequester = remember { FocusRequester() } val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(Unit) { focusRequester.requestFocus() } @@ -107,6 +110,7 @@ fun SearchScreen( Box(modifier = Modifier.fillMaxSize().padding(inner)) { ResultsPane( state = state.results, + playingTrackId = playingTrackId, onArtistClick = { id -> navController.navigate(ArtistDetail(id)) }, onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) }, onTrackPlay = viewModel::playTrack, @@ -144,6 +148,7 @@ private fun SearchField( @Composable private fun ResultsPane( state: SearchResultsState, + playingTrackId: String?, onArtistClick: (String) -> Unit, onAlbumClick: (String) -> Unit, onTrackPlay: (TrackRef) -> Unit, @@ -160,6 +165,7 @@ private fun ResultsPane( } else { ResultsList( response = state.response, + playingTrackId = playingTrackId, onArtistClick = onArtistClick, onAlbumClick = onAlbumClick, onTrackPlay = onTrackPlay, @@ -174,6 +180,7 @@ private fun ResultsPane( @Composable private fun ResultsList( response: SearchResponseRef, + playingTrackId: String?, onArtistClick: (String) -> Unit, onAlbumClick: (String) -> Unit, onTrackPlay: (TrackRef) -> Unit, @@ -186,7 +193,13 @@ private fun ResultsList( ) { artistsSection(response.artists, onArtistClick) albumsSection(response.albums, onAlbumClick) - tracksSection(response.tracks, onTrackPlay, onNavigateToAlbum, onNavigateToArtist) + tracksSection( + response.tracks, + playingTrackId, + onTrackPlay, + onNavigateToAlbum, + onNavigateToArtist, + ) } } @@ -224,6 +237,7 @@ private fun LazyListScope.albumsSection( private fun LazyListScope.tracksSection( tracks: List, + playingTrackId: String?, onTrackPlay: (TrackRef) -> Unit, onNavigateToAlbum: (String) -> Unit, onNavigateToArtist: (String) -> Unit, @@ -233,6 +247,7 @@ private fun LazyListScope.tracksSection( items(items = tracks, key = { "trk-${it.id}" }) { track -> TrackResultRow( track = track, + nowPlaying = track.id == playingTrackId, onClick = { onTrackPlay(track) }, onNavigateToAlbum = onNavigateToAlbum, onNavigateToArtist = onNavigateToArtist, @@ -244,10 +259,16 @@ private fun LazyListScope.tracksSection( @Composable private fun TrackResultRow( track: TrackRef, + nowPlaying: Boolean, onClick: () -> Unit, onNavigateToAlbum: (String) -> Unit, onNavigateToArtist: (String) -> Unit, ) { + val titleColor = if (nowPlaying) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + } Row( modifier = Modifier .fillMaxWidth() @@ -261,7 +282,7 @@ private fun TrackResultRow( Text( text = track.title, style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurface, + color = titleColor, maxLines = 1, overflow = TextOverflow.Ellipsis, )