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 54 additions and 17 deletions
Showing only changes of commit 4a3215cc07 - Show all commits
@@ -60,42 +60,42 @@ private fun NavGraphBuilder.inShellTopLevel(
expandPlayer: () -> Unit,
) {
composable<Home> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
HomeScreen(navController = navController)
}
}
composable<Library> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
LibraryScreen(navController = navController)
}
}
composable<Search> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
SearchScreen(navController = navController)
}
}
composable<Discover> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
DiscoverScreen(navController = navController)
}
}
composable<Playlists> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
PlaylistsListScreen(navController = navController)
}
}
composable<Settings> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
SettingsScreen(navController = navController)
}
}
composable<Admin> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminLandingScreen(navController = navController)
}
}
composable<Requests> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
RequestsScreen(navController = navController)
}
}
@@ -106,32 +106,32 @@ private fun NavGraphBuilder.inShellDetail(
expandPlayer: () -> Unit,
) {
composable<AlbumDetail> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AlbumDetailScreen(navController = navController)
}
}
composable<ArtistDetail> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
ArtistDetailScreen(navController = navController)
}
}
composable<PlaylistDetail> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
PlaylistDetailScreen(navController = navController)
}
}
composable<AdminRequests> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminRequestsScreen(navController = navController)
}
}
composable<AdminQuarantine> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminQuarantineScreen(navController = navController)
}
}
composable<AdminUsers> {
ShellScaffold(onExpandPlayer = expandPlayer) {
ShellScaffold(navController = navController, onExpandPlayer = expandPlayer) {
AdminUsersScreen(navController = navController)
}
}
@@ -32,6 +32,7 @@ import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music
import com.composables.icons.lucide.Pause
import com.composables.icons.lucide.Play
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsButton
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
private const val MINI_BAR_HEIGHT_DP = 64
@@ -77,6 +78,8 @@ private fun MiniCover(coverUrl: String, contentDescription: String) {
@Composable
fun MiniPlayer(
onExpandClick: () -> Unit,
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
modifier: Modifier = Modifier,
viewModel: PlayerViewModel = hiltViewModel(),
) {
@@ -122,6 +125,14 @@ fun MiniPlayer(
tint = MaterialTheme.colorScheme.onSurface,
)
}
// hideQueueActions=true: the playing track is itself the
// queue entry, so Play next / Add to queue would be silly.
TrackActionsButton(
track = track,
hideQueueActions = true,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
}
}
@@ -4,14 +4,26 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.player.ui.MiniPlayer
import com.fabledsword.minstrel.shared.widgets.trackactions.TrackActionsViewModel
/**
* Outer shell wrapper for "in-app" routes — banners on top
* (conditional, none implemented yet), routed screen filling the
* middle, MiniPlayer pinned at the bottom (auto-hides when no track).
* A shell-scoped SnackbarHost above the MiniPlayer surfaces transient
* messages emitted by the MiniPlayer's TrackActionsButton (snackbars
* for in-screen kebabs continue to live on each screen's own Scaffold).
*
* Mirrors the Flutter `_ShellWithPlayerBar` (lib/shared/routing.dart).
*
@@ -21,15 +33,24 @@ import com.fabledsword.minstrel.player.ui.MiniPlayer
* intentionally do NOT — they fill the entire screen, often with a
* back-only AppBar.
*
* `onExpandPlayer` is the navigate-to-NowPlaying callback; passed
* through to MiniPlayer so it doesn't have to know the nav graph.
* [onExpandPlayer] is the navigate-to-NowPlaying callback passed
* through to MiniPlayer's tap-handler. [navController] is needed by
* the MiniPlayer kebab for go-to-album / go-to-artist nav.
*/
@Composable
fun ShellScaffold(
navController: NavHostController,
onExpandPlayer: () -> Unit,
modifier: Modifier = Modifier,
trackActionsViewModel: TrackActionsViewModel = hiltViewModel(),
content: @Composable () -> Unit,
) {
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
trackActionsViewModel.transientMessages.collect { msg ->
snackbarHostState.showSnackbar(msg)
}
}
Column(modifier = modifier.fillMaxSize()) {
// Banners (VersionTooOld, UpdateBanner) live here once those
// mechanisms exist; for now the slot is empty so the layout
@@ -37,6 +58,11 @@ fun ShellScaffold(
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
content()
}
MiniPlayer(onExpandClick = onExpandPlayer)
SnackbarHost(hostState = snackbarHostState)
MiniPlayer(
onExpandClick = onExpandPlayer,
onNavigateToAlbum = { id -> navController.navigate(AlbumDetail(id)) },
onNavigateToArtist = { id -> navController.navigate(ArtistDetail(id)) },
)
}
}