Fit full compact search keyboard beside results on TV.

Replace oversized McButtons with a 6-column key grid so the whole keyboard stays visible Netflix-style.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-01 03:43:13 +02:00
parent 8b3fb1b856
commit 47dcbafa87
2 changed files with 201 additions and 62 deletions

View file

@ -12,8 +12,8 @@ android {
applicationId = "nl.vonas.mediacluster.tv"
minSdk = 24
targetSdk = 35
versionCode = 14
versionName = "0.7.7"
versionCode = 15
versionName = "0.7.8"
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")

View file

@ -3,43 +3,60 @@ package nl.vonas.mediacluster.tv.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Glow
import androidx.tv.material3.Surface
import androidx.tv.material3.Text
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import nl.vonas.mediacluster.tv.data.ApiClient
import nl.vonas.mediacluster.tv.data.MediaItem
import nl.vonas.mediacluster.tv.ui.components.McButton
import nl.vonas.mediacluster.tv.ui.components.PosterCard
import nl.vonas.mediacluster.tv.ui.theme.McColors
private val KEYBOARD_CHARS = listOf(
"A", "B", "C", "D", "E", "F",
"G", "H", "I", "J", "K", "L",
"M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X",
"Y", "Z", "1", "2", "3", "4",
"5", "6", "7", "8", "9", "0",
)
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun SearchScreen(
@ -72,7 +89,7 @@ fun SearchScreen(
query = next
searchJob?.cancel()
searchJob = scope.launch {
delay(250)
delay(220)
if (next.isBlank()) {
results = emptyList()
return@launch
@ -89,53 +106,96 @@ fun SearchScreen(
}
}
val rows = listOf(
"ABCDEFGHIJ",
"KLMNOPQRST",
"UVWXYZ1234",
"567890",
)
Row(
Modifier
.fillMaxSize()
.background(McColors.Bg)
.padding(horizontal = 36.dp, vertical = 24.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp),
.padding(start = 28.dp, end = 28.dp, top = 8.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(28.dp),
) {
// Links: toetsenbord
// Links: compact toetsenbord (volledig zichtbaar)
Column(
Modifier
.width(400.dp)
.width(340.dp)
.fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text("Zoeken", color = Color.White, fontSize = 26.sp, fontWeight = FontWeight.Bold)
Text(
if (query.isBlank()) "Typ een titel…" else query,
color = if (query.isBlank()) McColors.TextDim else McColors.Accent,
"Zoeken",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
fontWeight = FontWeight.Bold,
)
Spacer(Modifier.height(8.dp))
rows.forEach { row ->
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
modifier = Modifier.padding(bottom = 4.dp),
Box(
Modifier
.fillMaxWidth()
.heightIn(min = 44.dp)
.background(Color(0x22FFFFFF), RoundedCornerShape(6.dp))
.padding(horizontal = 14.dp, vertical = 10.dp),
contentAlignment = Alignment.CenterStart,
) {
row.forEach { ch ->
McButton(ch.toString()) { scheduleSearch(query + ch) }
Text(
text = if (query.isBlank()) "Typ een titel…" else query,
color = if (query.isBlank()) McColors.TextDim else Color.White,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
Spacer(Modifier.height(12.dp))
// 6×6 letter/cijfer-grid — toetsen schalen mee via weight
Column(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(5.dp),
) {
KEYBOARD_CHARS.chunked(6).forEach { rowKeys ->
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
rowKeys.forEach { key ->
SearchKey(
label = key,
modifier = Modifier
.weight(1f)
.aspectRatio(1.15f),
onClick = { scheduleSearch(query + key) },
)
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
McButton("Spatie") { scheduleSearch("$query ") }
McButton("Wis") {
}
Spacer(Modifier.height(8.dp))
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
SearchKey(
label = "Spatie",
modifier = Modifier
.weight(2f)
.height(40.dp),
onClick = { scheduleSearch("$query ") },
)
SearchKey(
label = "⌫",
modifier = Modifier
.weight(1f)
.height(40.dp),
onClick = {
if (query.isNotEmpty()) scheduleSearch(query.dropLast(1))
}
McButton("Leeg") { scheduleSearch("") }
},
)
SearchKey(
label = "Wis",
modifier = Modifier
.weight(1f)
.height(40.dp),
onClick = { scheduleSearch("") },
)
}
}
@ -144,23 +204,63 @@ fun SearchScreen(
Modifier
.weight(1f)
.fillMaxHeight(),
) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
when {
busy -> "Zoeken…"
error != null -> error!!
error != null -> "Fout bij zoeken"
query.isBlank() -> "Resultaten"
results.isEmpty() -> "Niets gevonden"
else -> "${results.size} resultaten"
},
color = if (error != null) McColors.Danger else McColors.TextDim,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
)
Spacer(Modifier.height(12.dp))
if (query.isNotBlank() && !busy) {
Text(
"“$query”",
color = Color(0xFF808080),
fontSize = 13.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(start = 12.dp),
)
}
}
if (error != null) {
Text(
error!!,
color = McColors.Danger,
fontSize = 12.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(top = 4.dp),
)
}
Spacer(Modifier.height(10.dp))
if (query.isBlank() && results.isEmpty()) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
"Begin links met typen",
color = Color(0xFF5A5A5A),
fontSize = 15.sp,
)
}
} else {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 120.dp),
columns = GridCells.Adaptive(minSize = 110.dp),
state = gridState,
contentPadding = PaddingValues(bottom = 40.dp),
contentPadding = PaddingValues(bottom = 24.dp, end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.fillMaxSize(),
@ -168,8 +268,8 @@ fun SearchScreen(
items(results, key = { "${it.type}:${it.id}" }) { item ->
PosterCard(
item = item,
width = 120.dp,
height = 180.dp,
width = 110.dp,
height = 165.dp,
onClick = {
if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id)
},
@ -178,4 +278,43 @@ fun SearchScreen(
}
}
}
}
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
private fun SearchKey(
label: String,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
modifier = modifier,
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.08f),
glow = ClickableSurfaceDefaults.glow(
focusedGlow = Glow(elevationColor = Color.White.copy(alpha = 0.2f), elevation = 4.dp),
),
colors = ClickableSurfaceDefaults.colors(
containerColor = Color(0xFF1C1C1C),
focusedContainerColor = Color.White,
contentColor = Color.White,
focusedContentColor = Color.Black,
),
) {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = label,
color = Color.Unspecified,
fontSize = if (label.length == 1) 15.sp else 12.sp,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Center,
maxLines = 1,
)
}
}
}