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:
parent
0eb0c5d367
commit
4d96940b20
4 changed files with 130 additions and 66 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 = 13
|
versionCode = 14
|
||||||
versionName = "0.7.6"
|
versionName = "0.7.7"
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -521,6 +521,7 @@ private fun HomeScreen(
|
||||||
var home by remember { mutableStateOf<HomePayload?>(null) }
|
var home by remember { mutableStateOf<HomePayload?>(null) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var continueMenu by remember { mutableStateOf<MediaItem?>(null) }
|
var continueMenu by remember { mutableStateOf<MediaItem?>(null) }
|
||||||
|
var menuReady by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val menuFocus = remember { FocusRequester() }
|
val menuFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
|
|
@ -538,8 +539,12 @@ private fun HomeScreen(
|
||||||
|
|
||||||
LaunchedEffect(continueMenu) {
|
LaunchedEffect(continueMenu) {
|
||||||
if (continueMenu != null) {
|
if (continueMenu != null) {
|
||||||
delay(40)
|
menuReady = false
|
||||||
|
delay(500)
|
||||||
|
menuReady = true
|
||||||
runCatching { menuFocus.requestFocus() }
|
runCatching { menuFocus.requestFocus() }
|
||||||
|
} else {
|
||||||
|
menuReady = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -652,11 +657,8 @@ private fun HomeScreen(
|
||||||
Text(menuItem.subtitle!!, color = McColors.TextDim, fontSize = 14.sp)
|
Text(menuItem.subtitle!!, color = McColors.TextDim, fontSize = 14.sp)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
McButton(
|
McButton("Markeren als gezien") {
|
||||||
"Markeren als gezien",
|
if (!menuReady) return@McButton
|
||||||
primary = true,
|
|
||||||
focusRequester = menuFocus,
|
|
||||||
) {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (menuItem.type == "movie") {
|
if (menuItem.type == "movie") {
|
||||||
|
|
@ -670,6 +672,7 @@ private fun HomeScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
McButton("Verbergen") {
|
McButton("Verbergen") {
|
||||||
|
if (!menuReady) return@McButton
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val pid = menuItem.progressId
|
val pid = menuItem.progressId
|
||||||
if (!pid.isNullOrBlank()) {
|
if (!pid.isNullOrBlank()) {
|
||||||
|
|
@ -681,7 +684,7 @@ private fun HomeScreen(
|
||||||
continueMenu = null
|
continueMenu = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
McButton("Annuleren") { continueMenu = null }
|
McButton("Annuleren", focusRequester = menuFocus) { continueMenu = null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ 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.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.padding
|
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.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
|
||||||
|
|
@ -24,6 +26,7 @@ import androidx.compose.runtime.setValue
|
||||||
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.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.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
|
@ -93,26 +96,34 @@ fun SearchScreen(
|
||||||
"567890",
|
"567890",
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Row(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(McColors.Bg)
|
.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)
|
// Links: toetsenbord
|
||||||
Spacer(Modifier.height(8.dp))
|
Column(
|
||||||
|
Modifier
|
||||||
|
.width(400.dp)
|
||||||
|
.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,
|
if (query.isBlank()) "Typ een titel…" else query,
|
||||||
color = if (query.isBlank()) McColors.TextDim else McColors.Accent,
|
color = if (query.isBlank()) McColors.TextDim else McColors.Accent,
|
||||||
fontSize = 22.sp,
|
fontSize = 20.sp,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
rows.forEach { row ->
|
rows.forEach { row ->
|
||||||
Row(
|
Row(
|
||||||
Modifier.fillMaxWidth().padding(bottom = 8.dp),
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
) {
|
) {
|
||||||
row.forEach { ch ->
|
row.forEach { ch ->
|
||||||
McButton(ch.toString()) { scheduleSearch(query + ch) }
|
McButton(ch.toString()) { scheduleSearch(query + ch) }
|
||||||
|
|
@ -126,13 +137,19 @@ fun SearchScreen(
|
||||||
}
|
}
|
||||||
McButton("Leeg") { scheduleSearch("") }
|
McButton("Leeg") { scheduleSearch("") }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(20.dp))
|
// Rechts: resultaten
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight(),
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
when {
|
when {
|
||||||
busy -> "Zoeken…"
|
busy -> "Zoeken…"
|
||||||
error != null -> error!!
|
error != null -> error!!
|
||||||
query.isBlank() -> "Resultaten verschijnen hier"
|
query.isBlank() -> "Resultaten"
|
||||||
results.isEmpty() -> "Niets gevonden"
|
results.isEmpty() -> "Niets gevonden"
|
||||||
else -> "${results.size} resultaten"
|
else -> "${results.size} resultaten"
|
||||||
},
|
},
|
||||||
|
|
@ -141,18 +158,18 @@ fun SearchScreen(
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 140.dp),
|
columns = GridCells.Adaptive(minSize = 120.dp),
|
||||||
state = gridState,
|
state = gridState,
|
||||||
contentPadding = PaddingValues(bottom = 40.dp),
|
contentPadding = PaddingValues(bottom = 40.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
items(results, key = { "${it.type}:${it.id}" }) { item ->
|
items(results, key = { "${it.type}:${it.id}" }) { item ->
|
||||||
PosterCard(
|
PosterCard(
|
||||||
item = item,
|
item = item,
|
||||||
width = 140.dp,
|
width = 120.dp,
|
||||||
height = 210.dp,
|
height = 180.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)
|
||||||
},
|
},
|
||||||
|
|
@ -161,3 +178,4 @@ fun SearchScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,21 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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.mutableLongStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
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.layout.ContentScale
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PosterCard(
|
fun PosterCard(
|
||||||
|
|
@ -89,10 +123,19 @@ fun PosterCard(
|
||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val useKeyHandler = onLongClick != null
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = if (useKeyHandler) ({}) else onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = null,
|
||||||
modifier = modifier.width(width),
|
modifier = modifier
|
||||||
|
.width(width)
|
||||||
|
.then(
|
||||||
|
if (useKeyHandler) {
|
||||||
|
Modifier.tvShortOrLongPress(onClick = onClick, onLongPress = onLongClick!!)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
|
||||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.14f),
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.14f),
|
||||||
border = ClickableSurfaceDefaults.border(
|
border = ClickableSurfaceDefaults.border(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue