feat(subsonic): getUser + envelope-shaped 404 for /rest/* #11

Merged
bvandeusen merged 262 commits from dev into main 2026-04-19 23:13:40 -04:00
2 changed files with 37 additions and 5 deletions
Showing only changes of commit cd1c054f62 - Show all commits
@@ -81,6 +81,7 @@ fun AdminRequestsScreen(
) )
is AdminRequestsUiState.Success -> RequestList( is AdminRequestsUiState.Success -> RequestList(
rows = s.rows, rows = s.rows,
usernames = s.usernames,
onApprove = viewModel::approve, onApprove = viewModel::approve,
onReject = viewModel::reject, onReject = viewModel::reject,
) )
@@ -92,12 +93,20 @@ fun AdminRequestsScreen(
@Composable @Composable
private fun RequestList( private fun RequestList(
rows: List<RequestRef>, rows: List<RequestRef>,
usernames: Map<String, String>,
onApprove: (String) -> Unit, onApprove: (String) -> Unit,
onReject: (String) -> Unit, onReject: (String) -> Unit,
) { ) {
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) {
items(items = rows, key = { it.id }) { req -> items(items = rows, key = { it.id }) { req ->
AdminRequestRow(req = req, onApprove = { onApprove(req.id) }, onReject = { onReject(req.id) }) AdminRequestRow(
req = req,
requester = usernames[req.userId]
?: req.userId.takeIf { it.isNotEmpty() }?.take(8)
?: "unknown",
onApprove = { onApprove(req.id) },
onReject = { onReject(req.id) },
)
HorizontalDivider() HorizontalDivider()
} }
} }
@@ -106,6 +115,7 @@ private fun RequestList(
@Composable @Composable
private fun AdminRequestRow( private fun AdminRequestRow(
req: RequestRef, req: RequestRef,
requester: String,
onApprove: () -> Unit, onApprove: () -> Unit,
onReject: () -> Unit, onReject: () -> Unit,
) { ) {
@@ -122,6 +132,13 @@ private fun AdminRequestRow(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
Text(
text = "Requested by $requester",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
AssistChip( AssistChip(
onClick = {}, onClick = {},
@@ -3,10 +3,13 @@ package com.fabledsword.minstrel.admin.ui
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.fabledsword.minstrel.admin.data.AdminRequestsRepository import com.fabledsword.minstrel.admin.data.AdminRequestsRepository
import com.fabledsword.minstrel.admin.data.AdminUsersRepository
import com.fabledsword.minstrel.events.EventsStream import com.fabledsword.minstrel.events.EventsStream
import com.fabledsword.minstrel.models.RequestRef import com.fabledsword.minstrel.models.RequestRef
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@@ -19,13 +22,17 @@ private val RELEVANT_EVENT_KINDS = setOf("request.status_changed")
sealed interface AdminRequestsUiState { sealed interface AdminRequestsUiState {
data object Loading : AdminRequestsUiState data object Loading : AdminRequestsUiState
data object Empty : AdminRequestsUiState data object Empty : AdminRequestsUiState
data class Success(val rows: List<RequestRef>) : AdminRequestsUiState data class Success(
val rows: List<RequestRef>,
val usernames: Map<String, String>,
) : AdminRequestsUiState
data class Error(val message: String) : AdminRequestsUiState data class Error(val message: String) : AdminRequestsUiState
} }
@HiltViewModel @HiltViewModel
class AdminRequestsViewModel @Inject constructor( class AdminRequestsViewModel @Inject constructor(
private val repository: AdminRequestsRepository, private val repository: AdminRequestsRepository,
private val usersRepository: AdminUsersRepository,
private val eventsStream: EventsStream, private val eventsStream: EventsStream,
) : ViewModel() { ) : ViewModel() {
@@ -44,11 +51,19 @@ class AdminRequestsViewModel @Inject constructor(
fun refresh(): Job = viewModelScope.launch { fun refresh(): Job = viewModelScope.launch {
internal.value = AdminRequestsUiState.Loading internal.value = AdminRequestsUiState.Loading
try { try {
val rows = repository.list() val (rows, usernames) = coroutineScope {
val requestsJob = async { repository.list() }
val usernamesJob = async {
runCatching {
usersRepository.list().associate { it.id to it.username }
}.getOrDefault(emptyMap())
}
requestsJob.await() to usernamesJob.await()
}
internal.value = if (rows.isEmpty()) { internal.value = if (rows.isEmpty()) {
AdminRequestsUiState.Empty AdminRequestsUiState.Empty
} else { } else {
AdminRequestsUiState.Success(rows) AdminRequestsUiState.Success(rows = rows, usernames = usernames)
} }
} catch ( } catch (
@Suppress("TooGenericExceptionCaught") e: Throwable, @Suppress("TooGenericExceptionCaught") e: Throwable,
@@ -75,7 +90,7 @@ class AdminRequestsViewModel @Inject constructor(
internal.value = if (filtered.isEmpty()) { internal.value = if (filtered.isEmpty()) {
AdminRequestsUiState.Empty AdminRequestsUiState.Empty
} else { } else {
AdminRequestsUiState.Success(filtered) AdminRequestsUiState.Success(rows = filtered, usernames = before.usernames)
} }
} }
viewModelScope.launch { viewModelScope.launch {