ci: drop push:dev trigger; PR is the gate #25
@@ -9,19 +9,26 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.AssistChip
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -30,10 +37,16 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import com.composables.icons.lucide.ArrowLeft
|
import com.composables.icons.lucide.ArrowLeft
|
||||||
|
import com.composables.icons.lucide.Disc3
|
||||||
|
import com.composables.icons.lucide.LibraryBig
|
||||||
import com.composables.icons.lucide.Lucide
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.Music
|
||||||
|
import com.composables.icons.lucide.Play
|
||||||
import com.composables.icons.lucide.X
|
import com.composables.icons.lucide.X
|
||||||
import com.fabledsword.minstrel.models.RequestRef
|
import com.fabledsword.minstrel.models.RequestRef
|
||||||
import com.fabledsword.minstrel.models.RequestStatus
|
import com.fabledsword.minstrel.models.RequestStatus
|
||||||
|
import com.fabledsword.minstrel.nav.AlbumDetail
|
||||||
|
import com.fabledsword.minstrel.nav.ArtistDetail
|
||||||
import com.fabledsword.minstrel.nav.Requests
|
import com.fabledsword.minstrel.nav.Requests
|
||||||
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
import com.fabledsword.minstrel.shared.widgets.EmptyState
|
||||||
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
|
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
|
||||||
@@ -83,6 +96,7 @@ fun RequestsScreen(
|
|||||||
is RequestsUiState.Success -> RequestList(
|
is RequestsUiState.Success -> RequestList(
|
||||||
rows = s.requests,
|
rows = s.requests,
|
||||||
onCancel = viewModel::cancel,
|
onCancel = viewModel::cancel,
|
||||||
|
onListen = { id -> navController.navigate(id) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,17 +104,30 @@ fun RequestsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RequestList(rows: List<RequestRef>, onCancel: (String) -> Unit) {
|
private fun RequestList(
|
||||||
|
rows: List<RequestRef>,
|
||||||
|
onCancel: (String) -> Unit,
|
||||||
|
onListen: (Any) -> Unit,
|
||||||
|
) {
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
items(items = rows, key = { it.id }) { req ->
|
items(items = rows, key = { it.id }) { req ->
|
||||||
RequestRow(req = req, onCancel = { onCancel(req.id) })
|
RequestRow(
|
||||||
|
req = req,
|
||||||
|
onCancel = { onCancel(req.id) },
|
||||||
|
onListen = req.listenRoute()?.let { route -> { onListen(route) } },
|
||||||
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RequestRow(req: RequestRef, onCancel: () -> Unit) {
|
private fun RequestRow(
|
||||||
|
req: RequestRef,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onListen: (() -> Unit)?,
|
||||||
|
) {
|
||||||
|
var showCancelConfirm by remember { mutableStateOf(false) }
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -108,6 +135,7 @@ private fun RequestRow(req: RequestRef, onCancel: () -> Unit) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
|
KindAvatar(kind = req.kind)
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = req.displayName,
|
text = req.displayName,
|
||||||
@@ -123,6 +151,15 @@ private fun RequestRow(req: RequestRef, onCancel: () -> Unit) {
|
|||||||
StatusPill(status = req.status)
|
StatusPill(status = req.status)
|
||||||
KindPill(label = req.kind)
|
KindPill(label = req.kind)
|
||||||
}
|
}
|
||||||
|
val progress = req.ingestProgressText()
|
||||||
|
if (progress != null) {
|
||||||
|
Text(
|
||||||
|
text = progress,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
if (req.status == RequestStatus.REJECTED && !req.notes.isNullOrEmpty()) {
|
if (req.status == RequestStatus.REJECTED && !req.notes.isNullOrEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = req.notes,
|
text = req.notes,
|
||||||
@@ -134,13 +171,91 @@ private fun RequestRow(req: RequestRef, onCancel: () -> Unit) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (req.status == RequestStatus.PENDING) {
|
when {
|
||||||
TextButton(onClick = onCancel) {
|
onListen != null -> OutlinedButton(onClick = onListen) {
|
||||||
|
Icon(
|
||||||
|
Lucide.Play,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(16.dp).padding(end = 4.dp),
|
||||||
|
)
|
||||||
|
Text("Listen")
|
||||||
|
}
|
||||||
|
req.status == RequestStatus.PENDING -> TextButton(onClick = { showCancelConfirm = true }) {
|
||||||
Icon(Lucide.X, contentDescription = null, modifier = Modifier.padding(end = 4.dp))
|
Icon(Lucide.X, contentDescription = null, modifier = Modifier.padding(end = 4.dp))
|
||||||
Text("Cancel")
|
Text("Cancel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showCancelConfirm) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showCancelConfirm = false },
|
||||||
|
title = { Text("Cancel request?") },
|
||||||
|
text = { Text("Cancel “${req.displayName}”? Lidarr will stop searching for it.") },
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
showCancelConfirm = false
|
||||||
|
onCancel()
|
||||||
|
},
|
||||||
|
) { Text("Cancel request") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showCancelConfirm = false }) { Text("Keep") }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KindAvatar(kind: String) {
|
||||||
|
// Flutter mapping: disc-3 for artist, library-big for album,
|
||||||
|
// music for track. Mirrors lib/requests/requests_screen.dart.
|
||||||
|
val icon = when (kind) {
|
||||||
|
"artist" -> Lucide.Disc3
|
||||||
|
"album" -> Lucide.LibraryBig
|
||||||
|
"track" -> Lucide.Music
|
||||||
|
else -> Lucide.Music
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(28.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Most-specific listenable detail-screen route for a completed
|
||||||
|
* request, or null when nothing's been matched / the status isn't
|
||||||
|
* completed. Track-level matches resolve to AlbumDetail since the
|
||||||
|
* client has no TrackDetail screen.
|
||||||
|
*/
|
||||||
|
private fun RequestRef.listenRoute(): Any? {
|
||||||
|
if (status != RequestStatus.COMPLETED) return null
|
||||||
|
matchedAlbumId?.takeIf { it.isNotEmpty() }?.let { return AlbumDetail(it) }
|
||||||
|
matchedArtistId?.takeIf { it.isNotEmpty() }?.let { return ArtistDetail(it) }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "2 albums · 14 tracks ingested" — null when both counts are zero.
|
||||||
|
* Singularization matches Flutter ("1 album · 1 track ingested").
|
||||||
|
*/
|
||||||
|
private fun RequestRef.ingestProgressText(): String? {
|
||||||
|
if (importedAlbumCount == 0 && importedTrackCount == 0) return null
|
||||||
|
val parts = buildList {
|
||||||
|
if (importedAlbumCount > 0) {
|
||||||
|
add(
|
||||||
|
"$importedAlbumCount ${if (importedAlbumCount == 1) "album" else "albums"}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (importedTrackCount > 0) {
|
||||||
|
add(
|
||||||
|
"$importedTrackCount ${if (importedTrackCount == 1) "track" else "tracks"}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "${parts.joinToString(" · ")} ingested"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user