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
3 changed files with 46 additions and 20 deletions
Showing only changes of commit f3ee182cd7 - Show all commits
@@ -222,7 +222,7 @@ private fun AlbumCover(album: AlbumRef) {
.background(MaterialTheme.colorScheme.surfaceVariant), .background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
if (album.coverUrl.isEmpty()) { if (album.id.isEmpty()) {
Icon( Icon(
imageVector = Lucide.Disc3, imageVector = Lucide.Disc3,
contentDescription = null, contentDescription = null,
@@ -231,7 +231,7 @@ private fun AlbumCover(album: AlbumRef) {
) )
} else { } else {
AsyncImage( AsyncImage(
model = album.coverUrl, model = album.displayCoverUrl,
contentDescription = album.title, contentDescription = album.title,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
@@ -1,5 +1,6 @@
package com.fabledsword.minstrel.player.ui package com.fabledsword.minstrel.player.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -21,10 +22,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music import com.composables.icons.lucide.Music
import com.composables.icons.lucide.Pause import com.composables.icons.lucide.Pause
@@ -41,9 +44,9 @@ private const val COVER_SIZE_DP = 48
* fresh install. Tapping the body navigates to the full * fresh install. Tapping the body navigates to the full
* NowPlayingScreen via [onExpandClick]. * NowPlayingScreen via [onExpandClick].
* *
* Cover art is a placeholder Lucide icon for now — TrackRef carries * Cover comes from TrackRef.coverUrl (derived from albumId via the
* no coverUrl; covers come from the AlbumRef join we'll add in a * BaseUrlInterceptor placeholder); falls back to the Lucide.Music
* later 5.x slice. AsyncImage / Coil wire-up lands then. * icon when the album binding is missing.
*/ */
@Composable @Composable
fun MiniPlayer( fun MiniPlayer(
@@ -69,14 +72,24 @@ fun MiniPlayer(
Box( Box(
modifier = Modifier modifier = Modifier
.size(COVER_SIZE_DP.dp) .size(COVER_SIZE_DP.dp)
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)), .clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
if (track.coverUrl.isEmpty()) {
Icon( Icon(
imageVector = Lucide.Music, imageVector = Lucide.Music,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} else {
AsyncImage(
model = track.coverUrl,
contentDescription = track.title,
modifier = Modifier.size(COVER_SIZE_DP.dp),
contentScale = ContentScale.Crop,
)
}
} }
Spacer(Modifier.width(12.dp)) Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
@@ -1,5 +1,6 @@
package com.fabledsword.minstrel.player.ui package com.fabledsword.minstrel.player.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -24,12 +25,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import com.composables.icons.lucide.ListMusic import com.composables.icons.lucide.ListMusic
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music import com.composables.icons.lucide.Music
@@ -75,7 +78,7 @@ fun NowPlayingScreen(
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
CoverPlaceholder() NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
TrackHeader( TrackHeader(
title = track.title, title = track.title,
@@ -112,21 +115,31 @@ private fun ViewQueueButton(onClick: () -> Unit) {
} }
@Composable @Composable
private fun CoverPlaceholder() { private fun NowPlayingCover(coverUrl: String, contentDescription: String) {
Box( Box(
modifier = Modifier modifier = Modifier
.widthIn(max = COVER_MAX_WIDTH_DP.dp) .widthIn(max = COVER_MAX_WIDTH_DP.dp)
.fillMaxWidth() .fillMaxWidth()
.aspectRatio(1f) .aspectRatio(1f)
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusLg)), .clip(RoundedCornerShape(FabledSwordFlatTokens.radiusLg))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
if (coverUrl.isEmpty()) {
Icon( Icon(
imageVector = Lucide.Music, imageVector = Lucide.Music,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(96.dp), modifier = Modifier.size(96.dp),
) )
} else {
AsyncImage(
model = coverUrl,
contentDescription = contentDescription,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
}
} }
} }