package dev.stave.ui.random
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import dev.stave.ui.components.staveFocusable
import dev.stave.ui.theme.OnMuted
import dev.stave.ui.theme.OnPrimary
import dev.stave.ui.theme.SurfaceRaised
import dev.stave.ui.theme.adaptiveScreenPaddingHorizontal
import dev.stave.ui.theme.adaptiveScreenPaddingVertical
import dev.stave.ui.theme.isCompactHeight
import dev.stave.ui.theme.staveInterFamily
import dev.stave.ui.theme.withInterDigits
private val CHOICES = listOf(10, 25, 50, 100)
/**
* Picker for the "Random" feature. User picks a count and a type (albums or tracks).
* Works with both UPnP servers and local files — whichever source is active.
*/
@Composable
fun RandomPickerScreen(
onPickAlbums: (count: Int) -> Unit,
onPickTracks: (count: Int) -> Unit,
onBack: () -> Unit,
) {
BackHandler(onBack = onBack)
// Maintainer, 2026-10-29: phone landscape (~245dp usable) showed only the first count
// button per column. On compact height the screen scrolls and the header spacing
// tightens; TV keeps the non-scrolling layout (its weight spacer pins the gramophone
// to the bottom, which a scrolling Column would ignore).
val compactHeight = isCompactHeight()
val scroll = rememberScrollState()
Column(
Modifier
.fillMaxSize()
.then(if (compactHeight) Modifier.verticalScroll(scroll) else Modifier)
.padding(horizontal = adaptiveScreenPaddingHorizontal(), vertical = adaptiveScreenPaddingVertical())
) {
Text("Random", style = MaterialTheme.typography.displaySmall, color = OnPrimary)
Text(
"Pick a count to pull a random sample from the library.",
style = MaterialTheme.typography.bodyMedium,
color = OnMuted,
)
// Maintainer, 2026-10-29: 12dp on compact height (phone landscape), 28dp elsewhere.
Spacer(Modifier.height(if (compactHeight) 12.dp else 28.dp))
// Side-by-side columns. On phone portrait the 16dp gap and
// 1.5f weight on the left column prevent text clipping.
val compact = dev.stave.ui.theme.isCompactWidth()
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(if (compact) 16.dp else 40.dp),
) {
PickerColumn(
modifier = Modifier.weight(if (compact) 1.5f else 1f),
header = "Albums",
subtitle = "Browse a\nshelf of\nrandom albums",
onPick = onPickAlbums,
)
PickerColumn(
modifier = Modifier.weight(1f),
header = "Tracks",
subtitle = "Queue up\nand play\nimmediately",
onPick = onPickTracks,
)
}
Spacer(Modifier.weight(1f))
Spacer(Modifier.height(32.dp))
// Gramophone branding at the bottom, matching the Ready to Party dialog
Box(
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
contentAlignment = Alignment.Center,
) {
// ╔══════════════════════════════════════════════════════════════╗
// ║ DO NOT TOUCH THIS GRAMOPHONE. ║
// ║ ║
// ║ This AsyncImage — the drawable reference, the size, the ║
// ║ contentDescription, the surrounding Box — is the canonical ║
// ║ reference rendering of the glowing gramophone. the maintainer has ║
// ║ declared it perfect and will make whoever touches it pay a ║
// ║ severe price. That applies to humans, agents, refactors, ║
// ║ IDE auto-import cleanups, "while I was nearby" tidy-ups, ║
// ║ and anything else that looks like a casual edit. ║
// ║ ║
// ║ Do not change the drawable. Do not resize. Do not wrap it. ║
// ║ Do not "upgrade" it to a higher-resolution variant. Do not ║
// ║ layer effects on top. Do not move it. Do not rename it. ║
// ║ If you think this needs to change, you are wrong, and you ║
// ║ need to ask the maintainer first. ║
// ╚══════════════════════════════════════════════════════════════╝
coil3.compose.AsyncImage(
model = dev.stave.R.drawable.earhorn_gramophone_glow,
contentDescription = null,
modifier = Modifier.size(160.dp),
)
}
}
}
@Composable
private fun PickerColumn(
modifier: Modifier = Modifier,
header: String,
subtitle: String,
onPick: (count: Int) -> Unit,
) {
Column(
modifier,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(withInterDigits(header), style = MaterialTheme.typography.titleLarge, color = OnPrimary)
// Maintainer, 2026-10-29: on compact height the subtitle drops minLines 3→1 and its
// hard line breaks become spaces (otherwise the "\n"s still force 3 lines); the
// landscape columns are wide enough for one line. Other sizes unchanged.
val compactHeight = isCompactHeight()
Text(
if (compactHeight) subtitle.replace('\n', ' ') else subtitle,
style = MaterialTheme.typography.bodySmall,
color = OnMuted,
minLines = if (compactHeight) 1 else 3,
maxLines = 3,
)
Spacer(Modifier.height(4.dp))
CHOICES.forEach { count ->
CountButton(count) { onPick(count) }
}
}
}
@Composable
private fun CountButton(count: Int, onClick: () -> Unit) {
val interaction = remember { MutableInteractionSource() }
Box(
Modifier
.fillMaxWidth()
.background(SurfaceRaised)
.staveFocusable(interaction, cornerRadiusDp = 6)
.clickable(interaction, indication = null) { onClick() }
.padding(horizontal = 20.dp, vertical = 12.dp),
contentAlignment = Alignment.CenterStart,
) {
Text(
count.toString(),
style = MaterialTheme.typography.titleMedium.copy(fontFamily = staveInterFamily()),
color = OnPrimary,
)
}
}