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"
|
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")
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
@ -179,3 +279,42 @@ 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue