From 04ece65a07e7e816b7f5bc878f2abfc7b1b1fb32 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 27 May 2026 22:26:15 -0400 Subject: [PATCH] =?UTF-8?q?feat(android):=20Home=20Most-Played=20plays=20t?= =?UTF-8?q?he=20section=20+=20NowPlaying=20LikeButton=20(audit=20v3=20?= =?UTF-8?q?=C2=A74.7=20+=20=C2=A74.15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §4.7 — The Most-played tiles on Home navigated to the track's album (a leftover TODO from before the player API existed). Now tapping a tile replaces the queue with the whole Most-played section and starts at the tapped index, via HomeViewModel.playMostPlayed → PlayerController.setQueue(source = "home:most_played"). Mirrors Flutter's _resolveSectionTracks where Home sections are playable units. §4.15 — NowPlaying had no inline like affordance — users had to open the kebab to like the current track. Added a LikeButton at the head of BottomActionsRow, wired to TrackActionsViewModel.isLikedFlow / toggleLike (same pattern as MiniPlayer). NowPlayingBody now collects the like state and threads it down. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../minstrel/home/ui/HomeScreen.kt | 33 +++++++++++++++---- .../minstrel/player/ui/NowPlayingScreen.kt | 10 ++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt index e9c4f84c..77164cde 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/home/ui/HomeScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -106,12 +107,28 @@ sealed interface HomeUiState { class HomeViewModel @Inject constructor( private val homeRepository: HomeRepository, private val playlistsRepository: PlaylistsRepository, + private val player: com.fabledsword.minstrel.player.PlayerController, ) : ViewModel() { init { refresh() } + /** + * Tap on a Most-played tile: replace the queue with the section's + * tracks and start at the tapped index. Mirrors Flutter's + * `_resolveSectionTracks` — Home's section is itself the playable + * unit, not a list of album-detail links. + */ + fun playMostPlayed(tracks: List, startIndex: Int) { + if (tracks.isEmpty()) return + player.setQueue( + tracks = tracks, + initialIndex = startIndex.coerceIn(0, tracks.lastIndex), + source = "home:most_played", + ) + } + /** * Pulls both /home/index and the playlists list. Returns the Job * for the combined refresh so a pull-to-refresh wrapper can await @@ -205,6 +222,7 @@ fun HomeScreen( onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) }, onArtistClick = { id -> navController.navigate(ArtistDetail(id)) }, onPlaylistClick = { id -> navController.navigate(PlaylistDetail(id)) }, + onMostPlayedTap = viewModel::playMostPlayed, ) } } @@ -262,6 +280,7 @@ private fun HomeSuccessContent( onAlbumClick: (String) -> Unit, onArtistClick: (String) -> Unit, onPlaylistClick: (String) -> Unit, + onMostPlayedTap: (List, Int) -> Unit, ) { LazyColumn( modifier = Modifier.fillMaxSize(), @@ -300,7 +319,7 @@ private fun HomeSuccessContent( } item { if (sections.mostPlayedTracks.isNotEmpty()) { - MostPlayedRow(sections.mostPlayedTracks, onAlbumClick) + MostPlayedRow(sections.mostPlayedTracks, onMostPlayedTap) } else { EmptySection( title = "Most played", @@ -406,18 +425,18 @@ private fun ArtistsRow( /** * Most-played section. Renders compact track tiles (cover + title + artist). - * Tapping a tile navigates to that track's album for now; the proper - * "play the section as a queue" wiring lands once the player API - * exposes a play-list-from-IDs entry point in Phase 7. + * Tapping a tile replaces the queue with the entire section starting + * at that tile — mirrors Flutter's `_resolveSectionTracks` behaviour + * where Home sections are themselves playable units. */ @Composable private fun MostPlayedRow( tracks: List, - onAlbumClick: (String) -> Unit, + onTap: (List, Int) -> Unit, ) { HorizontalScrollRow(title = "Most played") { - items(items = tracks, key = { it.id }) { track -> - CompactTrackTile(track = track, onClick = { onAlbumClick(track.albumId) }) + itemsIndexed(items = tracks, key = { _, t -> t.id }) { index, track -> + CompactTrackTile(track = track, onClick = { onTap(tracks, index) }) } } } 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 0697bd27..8c3bb295 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 @@ -68,6 +68,7 @@ import com.fabledsword.minstrel.nav.HERO_KEY_NOW_PLAYING_COVER import com.fabledsword.minstrel.nav.LocalAnimatedContentScope import com.fabledsword.minstrel.nav.LocalSharedTransitionScope import com.fabledsword.minstrel.nav.Queue +import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel import com.fabledsword.minstrel.theme.FabledSwordFlatTokens @@ -138,6 +139,7 @@ fun NowPlayingScreen( track = track, navController = navController, viewModel = viewModel, + trackActionsViewModel = trackActionsViewModel, ) } } @@ -208,7 +210,10 @@ private fun NowPlayingBody( track: com.fabledsword.minstrel.models.TrackRef, navController: NavHostController, viewModel: PlayerViewModel, + trackActionsViewModel: TrackActionsViewModel, ) { + val isLiked by trackActionsViewModel.isLikedFlow(track.id) + .collectAsStateWithLifecycle(initialValue = false) Column( modifier = Modifier .fillMaxSize() @@ -240,6 +245,8 @@ private fun NowPlayingBody( track = track, shuffleEnabled = state.shuffleEnabled, repeatMode = state.repeatMode, + isLiked = isLiked, + onToggleLike = { trackActionsViewModel.toggleLike(track.id, isLiked) }, onToggleShuffle = viewModel::toggleShuffle, onCycleRepeat = viewModel::cycleRepeat, ) @@ -252,6 +259,8 @@ private fun BottomActionsRow( track: com.fabledsword.minstrel.models.TrackRef, shuffleEnabled: Boolean, repeatMode: RepeatMode, + isLiked: Boolean, + onToggleLike: () -> Unit, onToggleShuffle: () -> Unit, onCycleRepeat: () -> Unit, ) { @@ -261,6 +270,7 @@ private fun BottomActionsRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { + LikeButton(liked = isLiked, onToggle = onToggleLike) IconButton(onClick = onToggleShuffle) { Icon( imageVector = Lucide.Shuffle,