feat(android): Home Most-Played plays the section + NowPlaying LikeButton (audit v3 §4.7 + §4.15)
§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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<com.fabledsword.minstrel.models.TrackRef>, 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<TrackRef>, 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<TrackRef>,
|
||||
onAlbumClick: (String) -> Unit,
|
||||
onTap: (List<TrackRef>, 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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user