From 47dcbafa87cc7b0eec3496bdd1260d8451196437 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 1 Sep 2026 03:43:13 +0200 Subject: [PATCH] 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 --- apps/android-tv/app/build.gradle.kts | 4 +- .../vonas/mediacluster/tv/ui/SearchScreen.kt | 259 ++++++++++++++---- 2 files changed, 201 insertions(+), 62 deletions(-) diff --git a/apps/android-tv/app/build.gradle.kts b/apps/android-tv/app/build.gradle.kts index b3b30c3..09a9d5f 100644 --- a/apps/android-tv/app/build.gradle.kts +++ b/apps/android-tv/app/build.gradle.kts @@ -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") diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt index 6bc4569..e45c77a 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/SearchScreen.kt @@ -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), - ) { - row.forEach { ch -> - McButton(ch.toString()) { scheduleSearch(query + ch) } + Box( + Modifier + .fillMaxWidth() + .heightIn(min = 44.dp) + .background(Color(0x22FFFFFF), RoundedCornerShape(6.dp)) + .padding(horizontal = 14.dp, vertical = 10.dp), + contentAlignment = Alignment.CenterStart, + ) { + 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") { - if (query.isNotEmpty()) scheduleSearch(query.dropLast(1)) - } - McButton("Leeg") { scheduleSearch("") } + + 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)) + }, + ) + SearchKey( + label = "Wis", + modifier = Modifier + .weight(1f) + .height(40.dp), + onClick = { scheduleSearch("") }, + ) } } @@ -145,37 +205,116 @@ fun SearchScreen( .weight(1f) .fillMaxHeight(), ) { - Text( - when { - busy -> "Zoeken…" - error != null -> error!! - query.isBlank() -> "Resultaten" - results.isEmpty() -> "Niets gevonden" - else -> "${results.size} resultaten" - }, - color = if (error != null) McColors.Danger else McColors.TextDim, - fontSize = 14.sp, - ) - Spacer(Modifier.height(12.dp)) - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 120.dp), - state = gridState, - contentPadding = PaddingValues(bottom = 40.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalArrangement = Arrangement.spacedBy(14.dp), - modifier = Modifier.fillMaxSize(), + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, ) { - items(results, key = { "${it.type}:${it.id}" }) { item -> - PosterCard( - item = item, - width = 120.dp, - height = 180.dp, - onClick = { - if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) - }, + Text( + when { + busy -> "Zoeken…" + 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, + ) + 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 = 110.dp), + state = gridState, + contentPadding = PaddingValues(bottom = 24.dp, end = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier.fillMaxSize(), + ) { + items(results, key = { "${it.type}:${it.id}" }) { item -> + PosterCard( + item = item, + width = 110.dp, + height = 165.dp, + onClick = { + if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) + }, + ) + } + } + } + } + } +} + +@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, + ) } } }