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 90 additions and 59 deletions
Showing only changes of commit c816490061 - Show all commits
@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions") // Compose screen + private helper composables and dialogs
package com.fabledsword.minstrel.admin.ui
import androidx.compose.foundation.layout.Arrangement
@@ -70,64 +72,21 @@ fun AdminUsersScreen(
}
}
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Admin · Users") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Lucide.ArrowLeft, contentDescription = "Back")
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = AdminUsers::class.qualifiedName,
)
},
)
AdminUsersScaffold(
navController = navController,
usersState = state,
invitesState = invitesState,
onRefresh = {
viewModel.refresh().join()
invitesViewModel.refresh()
},
) { inner ->
PullToRefreshScaffold(
onRefresh = {
viewModel.refresh().join()
invitesViewModel.refresh()
},
modifier = Modifier.fillMaxSize().padding(inner),
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { SectionHeader("Users") }
usersSection(
state = state,
onSetAdmin = viewModel::setAdmin,
onSetAutoApprove = viewModel::setAutoApprove,
onAskResetPassword = { resetPasswordFor = it },
onAskDelete = { deleteCandidate = it },
)
item { HorizontalDivider() }
item {
SectionHeader(
label = "Invites",
trailing = {
TextButton(onClick = { showGenerateInvite = true }) {
Icon(
Lucide.Plus,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Text("Generate")
}
},
)
}
invitesSection(
state = invitesState,
onRevoke = invitesViewModel::revoke,
)
}
}
}
onSetAdmin = viewModel::setAdmin,
onSetAutoApprove = viewModel::setAutoApprove,
onAskResetPassword = { resetPasswordFor = it },
onAskDelete = { deleteCandidate = it },
onAskGenerateInvite = { showGenerateInvite = true },
onRevokeInvite = invitesViewModel::revoke,
)
PendingDialogs(
resetPasswordFor = resetPasswordFor,
@@ -155,6 +114,74 @@ fun AdminUsersScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AdminUsersScaffold(
navController: NavHostController,
usersState: AdminUsersUiState,
invitesState: AdminInvitesUiState,
onRefresh: suspend () -> Unit,
onSetAdmin: (String, Boolean) -> Unit,
onSetAutoApprove: (String, Boolean) -> Unit,
onAskResetPassword: (AdminUserRef) -> Unit,
onAskDelete: (AdminUserRef) -> Unit,
onAskGenerateInvite: () -> Unit,
onRevokeInvite: (String) -> Unit,
) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text("Admin · Users") },
navigationIcon = {
IconButton(onClick = { navController.popBackStack() }) {
Icon(Lucide.ArrowLeft, contentDescription = "Back")
}
},
actions = {
MainAppBarActions(
navController = navController,
currentRouteName = AdminUsers::class.qualifiedName,
)
},
)
},
) { inner ->
PullToRefreshScaffold(
onRefresh = onRefresh,
modifier = Modifier.fillMaxSize().padding(inner),
) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item { SectionHeader("Users") }
usersSection(
state = usersState,
onSetAdmin = onSetAdmin,
onSetAutoApprove = onSetAutoApprove,
onAskResetPassword = onAskResetPassword,
onAskDelete = onAskDelete,
)
item { HorizontalDivider() }
item {
SectionHeader(
label = "Invites",
trailing = {
TextButton(onClick = onAskGenerateInvite) {
Icon(
Lucide.Plus,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
Text("Generate")
}
},
)
}
invitesSection(state = invitesState, onRevoke = onRevokeInvite)
}
}
}
}
@Composable
private fun SectionHeader(label: String, trailing: (@Composable () -> Unit)? = null) {
Row(
@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions") // Compose screen + private helper composables
package com.fabledsword.minstrel.playlists.ui
import androidx.compose.foundation.background
@@ -197,8 +199,8 @@ class PlaylistDetailViewModel @Inject constructor(
fun regenerate() {
val playlist = (internal.value as? PlaylistDetailUiState.Success)
?.detail?.playlist ?: return
val variant = playlist.systemVariant ?: return
if (!playlist.refreshable) return
val variant = playlist.systemVariant
?.takeIf { playlist.refreshable } ?: return
viewModelScope.launch {
runCatching { repository.refreshSystemPlaylist(variant) }
.onSuccess { newId ->
@@ -1,3 +1,5 @@
@file:Suppress("TooManyFunctions") // Compose screen + private helper composables
package com.fabledsword.minstrel.requests.ui
import androidx.compose.foundation.layout.Arrangement