feat(android): now-playing row highlight on track surfaces
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<HistoryEntry>,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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<String>,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
+12
-1
@@ -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<String>,
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -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<TrackRef>,
|
||||
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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user