From 04d6a38234d2845cbda18903348a758f6d449356 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Tue, 1 Sep 2026 00:25:43 +0200 Subject: [PATCH] Tighten TV back navigation, continue-watching actions, and detail focus. Long-press continue items to mark watched or hide, scroll-back before exit, confirm quit on home, and focus Play by default. Co-authored-by: Cursor --- apps/android-tv/app/build.gradle.kts | 4 +- .../vonas/mediacluster/tv/ui/BrowseScreen.kt | 17 +- .../java/nl/vonas/mediacluster/tv/ui/McNav.kt | 451 ++++++++++++------ .../vonas/mediacluster/tv/ui/PlayerScreen.kt | 7 +- .../vonas/mediacluster/tv/ui/SearchScreen.kt | 18 +- .../tv/ui/components/Components.kt | 24 +- apps/master-api/src/viewer/service.ts | 8 +- 7 files changed, 371 insertions(+), 158 deletions(-) diff --git a/apps/android-tv/app/build.gradle.kts b/apps/android-tv/app/build.gradle.kts index a92734a..4c08cae 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 = 12 - versionName = "0.7.5" + versionCode = 13 + versionName = "0.7.6" 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/BrowseScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt index 9b6ba9d..1b68dae 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/BrowseScreen.kt @@ -3,7 +3,6 @@ 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 @@ -47,6 +46,7 @@ fun BrowseScreen( catalogId: String, titleHint: String? = null, onBack: (() -> Unit)? = null, + onAtTopBack: (() -> Unit)? = null, onOpenMovie: (String) -> Unit, onOpenSeries: (String) -> Unit, onPlayContinue: (( @@ -109,8 +109,19 @@ fun BrowseScreen( } } - if (onBack != null) { - BackHandler(onBack = onBack) + val handleBack = onBack != null || onAtTopBack != null + if (handleBack) { + BackHandler { + val scrolled = gridState.firstVisibleItemIndex > 0 || + gridState.firstVisibleItemScrollOffset > 0 + if (scrolled) { + scope.launch { gridState.animateScrollToItem(0) } + } else if (onBack != null) { + onBack() + } else { + onAtTopBack?.invoke() + } + } } Column( diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt index 3d7afc4..37268f2 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/McNav.kt @@ -1,5 +1,6 @@ package nl.vonas.mediacluster.tv.ui +import android.app.Activity import android.os.Build import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -19,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -202,7 +204,11 @@ private fun MainHub( ) { var tab by remember { mutableStateOf(MainTab.Home) } var navFocused by remember { mutableStateOf(false) } + var exitConfirm by remember { mutableStateOf(false) } val navFocus = remember { FocusRequester() } + val homeListState = rememberLazyListState() + val hubScope = rememberCoroutineScope() + val activity = LocalContext.current as? Activity var shelfPrefs by remember { mutableStateOf>(emptyList()) } var movieCatalog by remember { mutableStateOf("movies") } var seriesCatalog by remember { mutableStateOf("series") } @@ -252,13 +258,40 @@ private fun MainHub( else -> null } - BackHandler(enabled = !navFocused && tab != MainTab.Home) { - tab = MainTab.Home + fun focusTopNav() { navFocused = true + exitConfirm = false + hubScope.launch { + delay(40) + runCatching { navFocus.requestFocus() } + } } - BackHandler(enabled = !navFocused && tab == MainTab.Home) { - navFocused = true - runCatching { navFocus.requestFocus() } + + // Exit-dialog: terug annuleert + BackHandler(enabled = exitConfirm) { + exitConfirm = false + } + // Home + top-nav: vraag om afsluiten + BackHandler(enabled = tab == MainTab.Home && navFocused && !exitConfirm) { + exitConfirm = true + } + // Home content: eerst scroll omhoog, anders top-nav + BackHandler(enabled = tab == MainTab.Home && !navFocused && !exitConfirm) { + if (homeListState.firstVisibleItemIndex > 0 || homeListState.firstVisibleItemScrollOffset > 0) { + hubScope.launch { homeListState.animateScrollToItem(0) } + } else { + focusTopNav() + } + } + // Films/Series/Zoeken: BrowseScreen handelt scroll→top af; hier alleen top-nav → Home + BackHandler(enabled = navFocused && tab != MainTab.Home && !exitConfirm) { + tab = MainTab.Home + focusTopNav() + } + // Settings (geen eigen scroll-back): terug naar home + BackHandler(enabled = tab == MainTab.Settings && !navFocused && !exitConfirm) { + tab = MainTab.Home + focusTopNav() } LaunchedEffect(navFocused) { @@ -268,83 +301,124 @@ private fun MainHub( } } - Column(Modifier.fillMaxSize().background(McColors.Bg)) { - Box(Modifier.focusRequester(navFocus)) { - TopNavBar( - selected = tab, - onSelect = { - tab = it - navFocused = true - when (it) { - MainTab.Movies -> { - if (movieSubId == "__search__") { - movieSubId = "movies" - movieCatalog = "movies" - } - } - MainTab.Series -> { - if (seriesSubId == "__search__") { - seriesSubId = "series" - seriesCatalog = "series" - } - } - else -> Unit - } - }, - subItems = subItems, - selectedSubId = selectedSubId, - onSelectSub = { item -> - if (item.id == "__search__") { - tab = MainTab.Search - navFocused = true - } else { - when (tab) { + Box(Modifier.fillMaxSize().background(McColors.Bg)) { + Column(Modifier.fillMaxSize()) { + Box(Modifier.focusRequester(navFocus)) { + TopNavBar( + selected = tab, + onSelect = { + tab = it + exitConfirm = false + focusTopNav() + when (it) { MainTab.Movies -> { - movieSubId = item.id - movieCatalog = item.catalogId + if (movieSubId == "__search__") { + movieSubId = "movies" + movieCatalog = "movies" + } } MainTab.Series -> { - seriesSubId = item.id - seriesCatalog = item.catalogId + if (seriesSubId == "__search__") { + seriesSubId = "series" + seriesCatalog = "series" + } } else -> Unit } - } - }, - ) + }, + subItems = subItems, + selectedSubId = selectedSubId, + onSelectSub = { item -> + if (item.id == "__search__") { + tab = MainTab.Search + focusTopNav() + } else { + when (tab) { + MainTab.Movies -> { + movieSubId = item.id + movieCatalog = item.catalogId + } + MainTab.Series -> { + seriesSubId = item.id + seriesCatalog = item.catalogId + } + else -> Unit + } + } + }, + ) + } + when (tab) { + MainTab.Home -> HomeScreen( + api = api, + listState = homeListState, + onContentFocused = { navFocused = false }, + onOpenMovie = onOpenMovie, + onOpenSeries = onOpenSeries, + onSeeAll = onBrowse, + onPlay = onPlay, + ) + MainTab.Movies -> BrowseScreen( + api = api, + catalogId = movieCatalog, + titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films", + onAtTopBack = { focusTopNav() }, + onOpenMovie = onOpenMovie, + onOpenSeries = onOpenSeries, + ) + MainTab.Series -> BrowseScreen( + api = api, + catalogId = seriesCatalog, + titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series", + onAtTopBack = { focusTopNav() }, + onOpenMovie = onOpenMovie, + onOpenSeries = onOpenSeries, + ) + MainTab.Search -> SearchScreen( + api = api, + onOpenMovie = onOpenMovie, + onOpenSeries = onOpenSeries, + onAtTopBack = { focusTopNav() }, + ) + MainTab.Settings -> SettingsScreen( + api = api, + onBack = { + tab = MainTab.Home + focusTopNav() + }, + onLogout = onLogout, + ) + } } - when (tab) { - MainTab.Home -> HomeScreen( - api = api, - onOpenMovie = onOpenMovie, - onOpenSeries = onOpenSeries, - onSeeAll = onBrowse, - onPlay = onPlay, - ) - MainTab.Movies -> BrowseScreen( - api = api, - catalogId = movieCatalog, - titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films", - onOpenMovie = onOpenMovie, - onOpenSeries = onOpenSeries, - ) - MainTab.Series -> BrowseScreen( - api = api, - catalogId = seriesCatalog, - titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series", - onOpenMovie = onOpenMovie, - onOpenSeries = onOpenSeries, - ) - MainTab.Search -> SearchScreen( - api = api, - onOpenMovie = onOpenMovie, - onOpenSeries = onOpenSeries, - ) - MainTab.Settings -> SettingsScreen( - api = api, - onBack = { tab = MainTab.Home }, - onLogout = onLogout, - ) + + if (exitConfirm) { + Box( + Modifier + .fillMaxSize() + .background(Color(0xCC000000)), + contentAlignment = Alignment.Center, + ) { + Column( + Modifier + .width(420.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF1A1A1A)) + .padding(28.dp), + verticalArrangement = Arrangement.spacedBy(14.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("VMC afsluiten?", color = Color.White, fontSize = 22.sp, fontWeight = FontWeight.Bold) + Text( + "Weet je zeker dat je de app wilt sluiten?", + color = McColors.TextDim, + fontSize = 15.sp, + ) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + McButton("Annuleren") { exitConfirm = false } + McButton("Afsluiten", primary = true) { activity?.finish() } + } + } + } } } } @@ -437,6 +511,8 @@ private fun LinkScreen(api: ApiClient, store: SessionStore, onLinked: () -> Unit @Composable private fun HomeScreen( api: ApiClient, + listState: androidx.compose.foundation.lazy.LazyListState, + onContentFocused: () -> Unit, onOpenMovie: (String) -> Unit, onOpenSeries: (String) -> Unit, onSeeAll: (catalogId: String, title: String) -> Unit, @@ -444,92 +520,170 @@ private fun HomeScreen( ) { var home by remember { mutableStateOf(null) } var error by remember { mutableStateOf(null) } + var continueMenu by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + val menuFocus = remember { FocusRequester() } + + suspend fun reloadHome() { + home = api.home() + } LaunchedEffect(Unit) { try { - home = api.home() + reloadHome() } catch (e: Exception) { error = e.message } } + LaunchedEffect(continueMenu) { + if (continueMenu != null) { + delay(40) + runCatching { menuFocus.requestFocus() } + } + } + val spotlight = home?.spotlight.orEmpty().ifEmpty { home?.shelves?.flatMap { it.items }?.distinctBy { "${it.type}:${it.id}" }?.take(10).orEmpty() } - LazyColumn( - Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 56.dp), - verticalArrangement = Arrangement.spacedBy(28.dp), - ) { - item { - SpotlightBillboard( - items = spotlight, - onPlay = { item -> - when { - !item.mediaFileId.isNullOrBlank() -> onPlay( - item.mediaFileId!!, - item.id.takeIf { item.type == "movie" }, - item.id.takeIf { item.type == "episode" }, - null, - if (item.type == "episode") "EPISODE" else "MOVIE", - item.positionMs, - ) - item.type == "movie" -> onOpenMovie(item.id) - else -> onOpenSeries(item.id) - } - }, - onOpen = { item -> - if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) - }, - ) - } - if (error != null) { - item { Text(error!!, color = McColors.Danger, modifier = Modifier.padding(horizontal = 40.dp)) } - } - val continueItems = home?.continueWatching.orEmpty() - if (continueItems.isNotEmpty()) { - val preview = continueItems.take(5) + Box(Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 56.dp), + verticalArrangement = Arrangement.spacedBy(28.dp), + ) { item { - ShelfBlock( - title = "Verder kijken", - items = preview, - onSeeAll = if (continueItems.size > 5) { - { onSeeAll("continue-watching", "Verder kijken") } - } else { - null - }, - onClick = { item -> + SpotlightBillboard( + items = spotlight, + onPlay = { item -> + onContentFocused() when { !item.mediaFileId.isNullOrBlank() -> onPlay( item.mediaFileId!!, item.id.takeIf { item.type == "movie" }, item.id.takeIf { item.type == "episode" }, - item.seriesId, + null, if (item.type == "episode") "EPISODE" else "MOVIE", item.positionMs, ) item.type == "movie" -> onOpenMovie(item.id) - item.type == "episode" && !item.seriesId.isNullOrBlank() -> onOpenSeries(item.seriesId!!) else -> onOpenSeries(item.id) } }, + onOpen = { item -> + onContentFocused() + if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) + }, + ) + } + if (error != null) { + item { Text(error!!, color = McColors.Danger, modifier = Modifier.padding(horizontal = 40.dp)) } + } + val continueItems = home?.continueWatching.orEmpty() + if (continueItems.isNotEmpty()) { + val preview = continueItems.take(5) + item { + ShelfBlock( + title = "Verder kijken", + items = preview, + onSeeAll = if (continueItems.size > 5) { + { onSeeAll("continue-watching", "Verder kijken") } + } else { + null + }, + onClick = { item -> + onContentFocused() + when { + !item.mediaFileId.isNullOrBlank() -> onPlay( + item.mediaFileId!!, + item.id.takeIf { item.type == "movie" }, + item.id.takeIf { item.type == "episode" }, + item.seriesId, + if (item.type == "episode") "EPISODE" else "MOVIE", + item.positionMs, + ) + item.type == "movie" -> onOpenMovie(item.id) + item.type == "episode" && !item.seriesId.isNullOrBlank() -> onOpenSeries(item.seriesId!!) + else -> onOpenSeries(item.id) + } + }, + onLongClick = { item -> continueMenu = item }, + ) + } + } + items(home?.shelves.orEmpty()) { shelf -> + val catalogId = when (shelf.slug) { + "recent-movies", "recent-series", "4k-movies" -> shelf.slug + else -> shelf.slug.ifBlank { shelf.id } + } + ShelfBlock( + title = shelf.title, + items = shelf.items, + onSeeAll = { onSeeAll(catalogId, shelf.title) }, + onClick = { item -> + onContentFocused() + if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) + }, ) } } - items(home?.shelves.orEmpty()) { shelf -> - val catalogId = when (shelf.slug) { - "recent-movies", "recent-series", "4k-movies" -> shelf.slug - else -> shelf.slug.ifBlank { shelf.id } + + val menuItem = continueMenu + if (menuItem != null) { + BackHandler { continueMenu = null } + Box( + Modifier + .fillMaxSize() + .background(Color(0xCC000000)), + contentAlignment = Alignment.Center, + ) { + Column( + Modifier + .width(440.dp) + .clip(RoundedCornerShape(12.dp)) + .background(Color(0xFF1A1A1A)) + .padding(28.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text(menuItem.title, color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.Bold, maxLines = 2) + if (!menuItem.subtitle.isNullOrBlank()) { + Text(menuItem.subtitle!!, color = McColors.TextDim, fontSize = 14.sp) + } + Spacer(Modifier.height(4.dp)) + McButton( + "Markeren als gezien", + primary = true, + focusRequester = menuFocus, + ) { + scope.launch { + runCatching { + if (menuItem.type == "movie") { + api.markWatched(movieId = menuItem.id) + } else { + api.markWatched(episodeId = menuItem.id) + } + reloadHome() + }.onFailure { error = it.message } + continueMenu = null + } + } + McButton("Verbergen") { + scope.launch { + val pid = menuItem.progressId + if (!pid.isNullOrBlank()) { + runCatching { + api.dismissContinue(pid) + reloadHome() + }.onFailure { error = it.message } + } + continueMenu = null + } + } + McButton("Annuleren") { continueMenu = null } + } } - ShelfBlock( - title = shelf.title, - items = shelf.items, - onSeeAll = { onSeeAll(catalogId, shelf.title) }, - onClick = { item -> - if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) - }, - ) } } } @@ -677,6 +831,7 @@ private fun ShelfBlock( items: List, onSeeAll: (() -> Unit)?, onClick: (MediaItem) -> Unit, + onLongClick: ((MediaItem) -> Unit)? = null, ) { Column(Modifier.padding(start = 48.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { Row( @@ -699,7 +854,11 @@ private fun ShelfBlock( contentPadding = PaddingValues(end = 48.dp), ) { items(items, key = { "${it.type}:${it.id}:${it.progressId}" }) { item -> - PosterCard(item, onClick = { onClick(item) }) + PosterCard( + item = item, + onClick = { onClick(item) }, + onLongClick = onLongClick?.let { handler -> { handler(item) } }, + ) } } } @@ -720,6 +879,7 @@ private fun MovieDetailScreen( var files by remember { mutableStateOf>>(emptyList()) } var error by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() + val playFocus = remember { FocusRequester() } LaunchedEffect(movieId) { try { @@ -751,6 +911,13 @@ private fun MovieDetailScreen( } } + LaunchedEffect(files) { + if (files.isNotEmpty()) { + delay(80) + runCatching { playFocus.requestFocus() } + } + } + BackHandler(onBack = onBack) Box(Modifier.fillMaxSize().background(McColors.Bg)) { AsyncImage(model = backdrop, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) @@ -767,7 +934,7 @@ private fun MovieDetailScreen( if (error != null) Text(error!!, color = McColors.Danger) Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { files.firstOrNull()?.let { (id, _) -> - McButton("Afspelen", primary = true) { onPlay(id, title, 0) } + McButton("Afspelen", primary = true, focusRequester = playFocus) { onPlay(id, title, 0) } } McButton("Als gekeken") { scope.launch { @@ -801,6 +968,7 @@ private fun SeriesDetailScreen( var backdrop by remember { mutableStateOf(null) } var episodes by remember { mutableStateOf>>(emptyList()) } var error by remember { mutableStateOf(null) } + val firstEpFocus = remember { FocusRequester() } LaunchedEffect(seriesId) { try { @@ -829,6 +997,13 @@ private fun SeriesDetailScreen( } } + LaunchedEffect(episodes) { + if (episodes.isNotEmpty()) { + delay(80) + runCatching { firstEpFocus.requestFocus() } + } + } + BackHandler(onBack = onBack) Box(Modifier.fillMaxSize().background(McColors.Bg)) { AsyncImage(model = backdrop, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, alpha = 0.35f) @@ -843,8 +1018,12 @@ private fun SeriesDetailScreen( Spacer(Modifier.height(16.dp)) Text("Afleveringen", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) } - items(episodes) { (id, label, fileId) -> - McButton(label) { onPlay(fileId, id, "$title · $label") } + itemsIndexed(episodes) { index, (id, label, fileId) -> + McButton( + label, + primary = index == 0, + focusRequester = if (index == 0) firstEpFocus else null, + ) { onPlay(fileId, id, "$title · $label") } } } } diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/PlayerScreen.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/PlayerScreen.kt index e9b968a..95a3ce2 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/PlayerScreen.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/PlayerScreen.kt @@ -194,8 +194,11 @@ fun PlayerScreen( when { menu == "os" -> menu = "text" menu != null -> menu = null - controls -> onBack() - else -> showControls() + controls -> { + menu = null + controls = false + } + else -> onBack() } } 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 0a60e43..8191530 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 @@ -1,5 +1,6 @@ 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.Column @@ -13,8 +14,8 @@ import androidx.compose.foundation.layout.padding 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.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -42,6 +43,7 @@ fun SearchScreen( api: ApiClient, onOpenMovie: (String) -> Unit, onOpenSeries: (String) -> Unit, + onAtTopBack: (() -> Unit)? = null, ) { var query by remember { mutableStateOf("") } var results by remember { mutableStateOf>(emptyList()) } @@ -49,6 +51,19 @@ fun SearchScreen( var error by remember { mutableStateOf(null) } val scope = rememberCoroutineScope() var searchJob by remember { mutableStateOf(null) } + val gridState = rememberLazyGridState() + + if (onAtTopBack != null) { + BackHandler { + val scrolled = gridState.firstVisibleItemIndex > 0 || + gridState.firstVisibleItemScrollOffset > 0 + if (scrolled) { + scope.launch { gridState.animateScrollToItem(0) } + } else { + onAtTopBack() + } + } + } fun scheduleSearch(next: String) { query = next @@ -127,6 +142,7 @@ fun SearchScreen( 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), diff --git a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt index 7764da7..831b995 100644 --- a/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt +++ b/apps/android-tv/app/src/main/java/nl/vonas/mediacluster/tv/ui/components/Components.kt @@ -12,6 +12,8 @@ import androidx.compose.runtime.Composable 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.layout.ContentScale import androidx.compose.ui.text.font.FontWeight @@ -35,6 +37,7 @@ fun McButton( label: String, modifier: Modifier = Modifier, primary: Boolean = false, + focusRequester: FocusRequester? = null, onClick: () -> Unit, ) { val bg = if (primary) Color.White else Color(0x33FFFFFF) @@ -43,7 +46,9 @@ fun McButton( val fgFocus = Color.Black Surface( onClick = onClick, - modifier = modifier, + modifier = modifier.then( + if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier, + ), shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)), scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f), glow = ClickableSurfaceDefaults.glow( @@ -76,6 +81,7 @@ fun PosterCard( modifier: Modifier = Modifier, width: Dp = 140.dp, height: Dp = 210.dp, + onLongClick: (() -> Unit)? = null, ) { val progress = if (item.durationMs > 0) { (item.positionMs.toFloat() / item.durationMs.toFloat()).coerceIn(0f, 1f) @@ -85,6 +91,7 @@ fun PosterCard( Surface( onClick = onClick, + onLongClick = onLongClick, modifier = modifier.width(width), shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)), scale = ClickableSurfaceDefaults.scale(focusedScale = 1.14f), @@ -117,17 +124,10 @@ fun PosterCard( Box( Modifier .align(Alignment.BottomStart) - .fillMaxWidth() - .height(3.dp) - .background(Color(0x55FFFFFF)), - ) { - Box( - Modifier - .fillMaxWidth(progress) - .height(3.dp) - .background(McColors.Accent), - ) - } + .fillMaxWidth(progress) + .height(4.dp) + .background(McColors.Brand), + ) } } } diff --git a/apps/master-api/src/viewer/service.ts b/apps/master-api/src/viewer/service.ts index 66c999b..89b1e37 100644 --- a/apps/master-api/src/viewer/service.ts +++ b/apps/master-api/src/viewer/service.ts @@ -757,7 +757,9 @@ export class ViewerService { ? { hiddenFromContinue: body.hiddenFromContinue } : autoComplete ? { hiddenFromContinue: true } - : {}), + : positionMs > 30_000 + ? { hiddenFromContinue: false } + : {}), }, }); } @@ -791,7 +793,9 @@ export class ViewerService { ? { hiddenFromContinue: body.hiddenFromContinue } : autoComplete ? { hiddenFromContinue: true } - : {}), + : positionMs > 30_000 + ? { hiddenFromContinue: false } + : {}), }, }); }