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" applicationId = "nl.vonas.mediacluster.tv"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 14 versionCode = 15
versionName = "0.7.7" versionName = "0.7.8"
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"") buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
ndk { ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a") 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.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
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.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Glow
import androidx.tv.material3.Surface
import androidx.tv.material3.Text import androidx.tv.material3.Text
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import nl.vonas.mediacluster.tv.data.ApiClient import nl.vonas.mediacluster.tv.data.ApiClient
import nl.vonas.mediacluster.tv.data.MediaItem 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.components.PosterCard
import nl.vonas.mediacluster.tv.ui.theme.McColors 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) @OptIn(ExperimentalTvMaterial3Api::class)
@Composable @Composable
fun SearchScreen( fun SearchScreen(
@ -72,7 +89,7 @@ fun SearchScreen(
query = next query = next
searchJob?.cancel() searchJob?.cancel()
searchJob = scope.launch { searchJob = scope.launch {
delay(250) delay(220)
if (next.isBlank()) { if (next.isBlank()) {
results = emptyList() results = emptyList()
return@launch return@launch
@ -89,53 +106,96 @@ fun SearchScreen(
} }
} }
val rows = listOf(
"ABCDEFGHIJ",
"KLMNOPQRST",
"UVWXYZ1234",
"567890",
)
Row( Row(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background(McColors.Bg) .background(McColors.Bg)
.padding(horizontal = 36.dp, vertical = 24.dp), .padding(start = 28.dp, end = 28.dp, top = 8.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp), horizontalArrangement = Arrangement.spacedBy(28.dp),
) { ) {
// Links: toetsenbord // Links: compact toetsenbord (volledig zichtbaar)
Column( Column(
Modifier Modifier
.width(400.dp) .width(340.dp)
.fillMaxHeight(), .fillMaxHeight(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Text("Zoeken", color = Color.White, fontSize = 26.sp, fontWeight = FontWeight.Bold)
Text( Text(
if (query.isBlank()) "Typ een titel…" else query, "Zoeken",
color = if (query.isBlank()) McColors.TextDim else McColors.Accent, color = Color.White,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Bold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
rows.forEach { row -> Box(
Row( Modifier
horizontalArrangement = Arrangement.spacedBy(6.dp), .fillMaxWidth()
modifier = Modifier.padding(bottom = 4.dp), .heightIn(min = 44.dp)
.background(Color(0x22FFFFFF), RoundedCornerShape(6.dp))
.padding(horizontal = 14.dp, vertical = 10.dp),
contentAlignment = Alignment.CenterStart,
) { ) {
row.forEach { ch -> Text(
McButton(ch.toString()) { scheduleSearch(query + ch) } 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)) 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 Modifier
.weight(1f) .weight(1f)
.fillMaxHeight(), .fillMaxHeight(),
) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
when { when {
busy -> "Zoeken…" busy -> "Zoeken…"
error != null -> error!! error != null -> "Fout bij zoeken"
query.isBlank() -> "Resultaten" query.isBlank() -> "Resultaten"
results.isEmpty() -> "Niets gevonden" results.isEmpty() -> "Niets gevonden"
else -> "${results.size} resultaten" else -> "${results.size} resultaten"
}, },
color = if (error != null) McColors.Danger else McColors.TextDim, color = if (error != null) McColors.Danger else McColors.TextDim,
fontSize = 14.sp, 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( LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 120.dp), columns = GridCells.Adaptive(minSize = 110.dp),
state = gridState, state = gridState,
contentPadding = PaddingValues(bottom = 40.dp), contentPadding = PaddingValues(bottom = 24.dp, end = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -168,8 +268,8 @@ fun SearchScreen(
items(results, key = { "${it.type}:${it.id}" }) { item -> items(results, key = { "${it.type}:${it.id}" }) { item ->
PosterCard( PosterCard(
item = item, item = item,
width = 120.dp, width = 110.dp,
height = 180.dp, height = 165.dp,
onClick = { onClick = {
if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) 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,
)
}
}
} }