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 3f00006b74 - Show all commits
@@ -17,11 +17,16 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -40,8 +45,12 @@ import com.composables.icons.lucide.Pause
import com.composables.icons.lucide.Play
import com.composables.icons.lucide.SkipBack
import com.composables.icons.lucide.SkipForward
import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.Queue
import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsButton
import com.fabledsword.minstrel.shared.widgets.track_actions.TrackActionsViewModel
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
import com.fabledsword.minstrel.theme.LocalActionColors
@@ -61,8 +70,15 @@ private const val SECONDS_PER_MINUTE = 60L
fun NowPlayingScreen(
navController: NavHostController,
viewModel: PlayerViewModel = hiltViewModel(),
trackActionsViewModel: TrackActionsViewModel = hiltViewModel(),
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
trackActionsViewModel.transientMessages.collect { msg ->
snackbarHostState.showSnackbar(msg)
}
}
val track = state.currentTrack
if (track == null) {
EmptyState(
@@ -71,35 +87,59 @@ fun NowPlayingScreen(
)
return
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title)
Spacer(Modifier.height(24.dp))
TrackHeader(
title = track.title,
artist = track.artistName,
album = track.albumTitle,
)
Spacer(Modifier.height(24.dp))
ScrubberRow(
positionMs = state.positionMs,
durationMs = state.durationMs,
onSeek = viewModel::seekTo,
)
Spacer(Modifier.height(8.dp))
TransportRow(
isPlaying = state.isPlaying,
onPrev = viewModel::skipToPrevious,
onPlayPause = { if (state.isPlaying) viewModel.pause() else viewModel.play() },
onNext = viewModel::skipToNext,
)
Spacer(Modifier.height(16.dp))
ViewQueueButton(onClick = { navController.navigate(Queue) })
Scaffold(
modifier = Modifier.fillMaxSize(),
snackbarHost = { SnackbarHost(snackbarHostState) },
containerColor = MaterialTheme.colorScheme.background,
) { inner ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(inner)
.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title)
Spacer(Modifier.height(24.dp))
TrackHeader(
title = track.title,
artist = track.artistName,
album = track.albumTitle,
)
Spacer(Modifier.height(24.dp))
ScrubberRow(
positionMs = state.positionMs,
durationMs = state.durationMs,
onSeek = viewModel::seekTo,
)
Spacer(Modifier.height(8.dp))
TransportRow(
isPlaying = state.isPlaying,
onPrev = viewModel::skipToPrevious,
onPlayPause = { if (state.isPlaying) viewModel.pause() else viewModel.play() },
onNext = viewModel::skipToNext,
)
Spacer(Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
ViewQueueButton(onClick = { navController.navigate(Queue) })
TrackActionsButton(
track = track,
hideQueueActions = true,
onNavigateToAlbum = { id ->
navController.popBackStack()
navController.navigate(AlbumDetail(id))
},
onNavigateToArtist = { id ->
navController.popBackStack()
navController.navigate(ArtistDetail(id))
},
)
}
}
}
}