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