feat: M3 weighted shuffle v1 — real /api/radio with scoring #21

Merged
bvandeusen merged 262 commits from dev into main 2026-04-27 11:00:29 -04:00
Showing only changes of commit 1ef5cd5b7a - Show all commits
@@ -4,6 +4,9 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -27,6 +30,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
@@ -52,6 +56,11 @@ private const val MINI_BAR_TONAL_ELEVATION_DP = 4
private const val COVER_SIZE_DP = 48
private const val SCRUBBER_ROW_HEIGHT_DP = 4
// Upward flick speed (dp/s) that expands the bar into NowPlaying.
// Mirrors player_bar.dart's 200 px/s threshold; expressed in dp and
// converted via density so the gesture feels the same across screens.
private const val SWIPE_UP_VELOCITY_DP = 200
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
private fun MiniCover(coverUrl: String, contentDescription: String) {
@@ -126,10 +135,22 @@ fun MiniPlayer(
val isLiked by trackActionsViewModel.isLikedFlow(track.id)
.collectAsStateWithLifecycle(initialValue = false)
// Swipe up anywhere on the bar to expand into the full player —
// mirrors player_bar.dart. We only act on a clear upward flick
// (negative velocity past the threshold) so a slow tap-with-jitter
// doesn't accidentally open the screen. The horizontal seek slider
// keeps its own gestures; a vertical draggable only claims
// vertical-dominant drags.
val flickThresholdPx = with(LocalDensity.current) { SWIPE_UP_VELOCITY_DP.dp.toPx() }
Surface(
modifier = modifier
.fillMaxWidth()
.height(MINI_BAR_HEIGHT_DP.dp),
.height(MINI_BAR_HEIGHT_DP.dp)
.draggable(
orientation = Orientation.Vertical,
state = rememberDraggableState { },
onDragStopped = { velocity -> if (velocity < -flickThresholdPx) onExpandClick() },
),
color = MaterialTheme.colorScheme.surface,
tonalElevation = MINI_BAR_TONAL_ELEVATION_DP.dp,
) {