From 235839b696fccadb659470f3f8276b8036368a45 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 15 Jul 2026 13:02:32 -0400 Subject: [PATCH 1/4] =?UTF-8?q?feat(player):=20heart/like=20button=20in=20?= =?UTF-8?q?queue=20view=20(web=20+=20android)=20=E2=80=94=20#1596?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full-screen player's queue ("up next") track rows were the one track-list surface missing the like heart that TrackRow/PlaylistTrackRow (web) and playlist/album/artist detail (Android) already carried. Web: render the shared in QueueTrackRow between the row body and the remove button (serves both the /now-playing aside and the mobile QueueDrawer, same component). LikeButton already stops click propagation so it won't trigger play-on-click. Android: PlayerViewModel now exposes likedTrackIds (set-based, the same idiom as the detail VMs) + toggleLikeTrack; QueueScreen threads liked/onToggleLike through QueueList → QueueRow, which renders the shared LikeButton after the duration. Liked state stays sourced from LikesRepository by track.id — no TrackRef data-model change. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../minstrel/player/ui/PlayerViewModel.kt | 29 +++++++++++++++++++ .../minstrel/player/ui/QueueScreen.kt | 17 ++++++++++- web/src/lib/components/QueueTrackRow.svelte | 3 ++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt index d7a4952a..e20a3407 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/PlayerViewModel.kt @@ -1,11 +1,16 @@ package com.fabledsword.minstrel.player.ui import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.fabledsword.minstrel.likes.data.LikesRepository import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerUiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -22,11 +27,26 @@ import javax.inject.Inject @HiltViewModel class PlayerViewModel @Inject constructor( private val controller: PlayerController, + private val likes: LikesRepository, ) : ViewModel() { val uiState: StateFlow = controller.uiState val dropEvents: SharedFlow = controller.dropEvents + /** + * Reactive set of liked track ids — the set-based pattern the queue + * (and playlist/album/artist detail) uses to color each row's + * [com.fabledsword.minstrel.shared.widgets.LikeButton] without a + * per-row Flow subscription. + */ + val likedTrackIds: StateFlow> = + likes.observeLikedTrackIds() + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(SHARE_STOP_TIMEOUT_MS), + initialValue = emptySet(), + ) + fun play() = controller.play() fun pause() = controller.pause() fun seekTo(positionMs: Long) = controller.seekTo(positionMs) @@ -35,4 +55,13 @@ class PlayerViewModel @Inject constructor( fun seekToIndex(index: Int) = controller.seekToIndex(index) fun toggleShuffle() = controller.toggleShuffle() fun cycleRepeat() = controller.cycleRepeat() + + fun toggleLikeTrack(trackId: String) { + val desired = trackId !in likedTrackIds.value + viewModelScope.launch { + likes.toggleLike(LikesRepository.ENTITY_TRACK, trackId, desired) + } + } } + +private const val SHARE_STOP_TIMEOUT_MS = 5_000L diff --git a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt index 71e2f64d..683c430e 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/player/ui/QueueScreen.kt @@ -36,6 +36,7 @@ import com.composables.icons.lucide.Volume2 import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.shared.formatDuration import com.fabledsword.minstrel.shared.widgets.EmptyState +import com.fabledsword.minstrel.shared.widgets.LikeButton @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -44,6 +45,7 @@ fun QueueScreen( viewModel: PlayerViewModel = hiltViewModel(), ) { val state by viewModel.uiState.collectAsStateWithLifecycle() + val likedTrackIds by viewModel.likedTrackIds.collectAsStateWithLifecycle() Scaffold( modifier = Modifier.fillMaxSize(), topBar = { @@ -67,7 +69,9 @@ fun QueueScreen( QueueList( tracks = state.queue, currentIndex = state.queueIndex, + likedTrackIds = likedTrackIds, onJumpTo = viewModel::seekToIndex, + onToggleLike = viewModel::toggleLikeTrack, ) } } @@ -78,14 +82,18 @@ fun QueueScreen( private fun QueueList( tracks: List, currentIndex: Int, + likedTrackIds: Set, onJumpTo: (Int) -> Unit, + onToggleLike: (String) -> Unit, ) { LazyColumn(modifier = Modifier.fillMaxSize()) { itemsIndexed(items = tracks, key = { _, track -> track.id }) { index, track -> QueueRow( track = track, isCurrent = index == currentIndex, + liked = track.id in likedTrackIds, onClick = { onJumpTo(index) }, + onToggleLike = { onToggleLike(track.id) }, ) HorizontalDivider() } @@ -93,7 +101,13 @@ private fun QueueList( } @Composable -private fun QueueRow(track: TrackRef, isCurrent: Boolean, onClick: () -> Unit) { +private fun QueueRow( + track: TrackRef, + isCurrent: Boolean, + liked: Boolean, + onClick: () -> Unit, + onToggleLike: () -> Unit, +) { val highlight = if (isCurrent) { MaterialTheme.colorScheme.primary.copy(alpha = HIGHLIGHT_ALPHA) } else { @@ -142,6 +156,7 @@ private fun QueueRow(track: TrackRef, isCurrent: Boolean, onClick: () -> Unit) { color = MaterialTheme.colorScheme.onSurfaceVariant, ) } + LikeButton(liked = liked, onToggle = onToggleLike) } } diff --git a/web/src/lib/components/QueueTrackRow.svelte b/web/src/lib/components/QueueTrackRow.svelte index 6580ab9c..d6d043d4 100644 --- a/web/src/lib/components/QueueTrackRow.svelte +++ b/web/src/lib/components/QueueTrackRow.svelte @@ -4,6 +4,7 @@ import type { TrackRef } from '$lib/api/types'; import { playFromQueueIndex, removeFromQueue, moveQueueItem } from '$lib/player/store.svelte'; import { offsetToDelta } from './queue-row-math'; + import LikeButton from './LikeButton.svelte'; let { track, index, isCurrent } = $props<{ track: TrackRef; @@ -80,6 +81,8 @@
{track.artist_name}
+ +