Fix TV continue-watching long-press menu and Netflix-style search layout.

Long-press no longer auto-selects the first action; search shows keyboard left and results right.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-01 03:04:44 +02:00
parent 0eb0c5d367
commit 4d96940b20
4 changed files with 130 additions and 66 deletions

View file

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

View file

@ -521,6 +521,7 @@ private fun HomeScreen(
var home by remember { mutableStateOf<HomePayload?>(null) }
var error by remember { mutableStateOf<String?>(null) }
var continueMenu by remember { mutableStateOf<MediaItem?>(null) }
var menuReady by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val menuFocus = remember { FocusRequester() }
@ -538,8 +539,12 @@ private fun HomeScreen(
LaunchedEffect(continueMenu) {
if (continueMenu != null) {
delay(40)
menuReady = false
delay(500)
menuReady = true
runCatching { menuFocus.requestFocus() }
} else {
menuReady = false
}
}
@ -652,11 +657,8 @@ private fun HomeScreen(
Text(menuItem.subtitle!!, color = McColors.TextDim, fontSize = 14.sp)
}
Spacer(Modifier.height(4.dp))
McButton(
"Markeren als gezien",
primary = true,
focusRequester = menuFocus,
) {
McButton("Markeren als gezien") {
if (!menuReady) return@McButton
scope.launch {
runCatching {
if (menuItem.type == "movie") {
@ -670,6 +672,7 @@ private fun HomeScreen(
}
}
McButton("Verbergen") {
if (!menuReady) return@McButton
scope.launch {
val pid = menuItem.progressId
if (!pid.isNullOrBlank()) {
@ -681,7 +684,7 @@ private fun HomeScreen(
continueMenu = null
}
}
McButton("Annuleren") { continueMenu = null }
McButton("Annuleren", focusRequester = menuFocus) { continueMenu = null }
}
}
}

View file

@ -7,10 +7,12 @@ 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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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
@ -24,6 +26,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.ExperimentalTvMaterial3Api
@ -93,70 +96,85 @@ fun SearchScreen(
"567890",
)
Column(
Row(
Modifier
.fillMaxSize()
.background(McColors.Bg)
.padding(36.dp),
.padding(horizontal = 36.dp, vertical = 24.dp),
horizontalArrangement = Arrangement.spacedBy(32.dp),
) {
Text("Zoeken", color = Color.White, fontSize = 28.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text(
if (query.isBlank()) "Typ een titel…" else query,
color = if (query.isBlank()) McColors.TextDim else McColors.Accent,
fontSize = 22.sp,
fontWeight = FontWeight.SemiBold,
)
Spacer(Modifier.height(16.dp))
rows.forEach { row ->
Row(
Modifier.fillMaxWidth().padding(bottom = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
row.forEach { ch ->
McButton(ch.toString()) { scheduleSearch(query + ch) }
// Links: toetsenbord
Column(
Modifier
.width(400.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,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
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) }
}
}
}
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
McButton("Spatie") { scheduleSearch("$query ") }
McButton("Wis") {
if (query.isNotEmpty()) scheduleSearch(query.dropLast(1))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
McButton("Spatie") { scheduleSearch("$query ") }
McButton("Wis") {
if (query.isNotEmpty()) scheduleSearch(query.dropLast(1))
}
McButton("Leeg") { scheduleSearch("") }
}
McButton("Leeg") { scheduleSearch("") }
}
Spacer(Modifier.height(20.dp))
Text(
when {
busy -> "Zoeken…"
error != null -> error!!
query.isBlank() -> "Resultaten verschijnen hier"
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 = 140.dp),
state = gridState,
contentPadding = PaddingValues(bottom = 40.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize(),
// Rechts: resultaten
Column(
Modifier
.weight(1f)
.fillMaxHeight(),
) {
items(results, key = { "${it.type}:${it.id}" }) { item ->
PosterCard(
item = item,
width = 140.dp,
height = 210.dp,
onClick = {
if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id)
},
)
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(),
) {
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)
},
)
}
}
}
}

View file

@ -9,12 +9,21 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@ -73,6 +82,31 @@ fun McButton(
}
}
private fun Key.isTvSelectKey(): Boolean =
this == Key.DirectionCenter || this == Key.Enter || this == Key.NumPadEnter
@Composable
private fun Modifier.tvShortOrLongPress(
onClick: () -> Unit,
onLongPress: () -> Unit,
): Modifier {
var downAt by remember { mutableLongStateOf(0L) }
return this.onPreviewKeyEvent { ev ->
when {
ev.type == KeyEventType.KeyDown && ev.key.isTvSelectKey() -> {
downAt = System.currentTimeMillis()
false
}
ev.type == KeyEventType.KeyUp && ev.key.isTvSelectKey() -> {
val held = System.currentTimeMillis() - downAt
if (held >= 450) onLongPress() else onClick()
true
}
else -> false
}
}
}
@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun PosterCard(
@ -89,10 +123,19 @@ fun PosterCard(
0f
}
val useKeyHandler = onLongClick != null
Surface(
onClick = onClick,
onLongClick = onLongClick,
modifier = modifier.width(width),
onClick = if (useKeyHandler) ({}) else onClick,
onLongClick = null,
modifier = modifier
.width(width)
.then(
if (useKeyHandler) {
Modifier.tvShortOrLongPress(onClick = onClick, onLongPress = onLongClick!!)
} else {
Modifier
},
),
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.14f),
border = ClickableSurfaceDefaults.border(