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:
parent
8b3fb1b856
commit
47dcbafa87
2 changed files with 201 additions and 62 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue