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 <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-01 00:25:43 +02:00
parent 7e624dd452
commit 04d6a38234
7 changed files with 371 additions and 158 deletions

View file

@ -12,8 +12,8 @@ android {
applicationId = "nl.vonas.mediacluster.tv" applicationId = "nl.vonas.mediacluster.tv"
minSdk = 24 minSdk = 24
targetSdk = 35 targetSdk = 35
versionCode = 12 versionCode = 13
versionName = "0.7.5" versionName = "0.7.6"
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")

View file

@ -3,7 +3,6 @@ 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
@ -47,6 +46,7 @@ fun BrowseScreen(
catalogId: String, catalogId: String,
titleHint: String? = null, titleHint: String? = null,
onBack: (() -> Unit)? = null, onBack: (() -> Unit)? = null,
onAtTopBack: (() -> Unit)? = null,
onOpenMovie: (String) -> Unit, onOpenMovie: (String) -> Unit,
onOpenSeries: (String) -> Unit, onOpenSeries: (String) -> Unit,
onPlayContinue: (( onPlayContinue: ((
@ -109,8 +109,19 @@ fun BrowseScreen(
} }
} }
if (onBack != null) { val handleBack = onBack != null || onAtTopBack != null
BackHandler(onBack = onBack) 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( Column(

View file

@ -1,5 +1,6 @@
package nl.vonas.mediacluster.tv.ui package nl.vonas.mediacluster.tv.ui
import android.app.Activity
import android.os.Build import android.os.Build
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background 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.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
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.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -202,7 +204,11 @@ private fun MainHub(
) { ) {
var tab by remember { mutableStateOf(MainTab.Home) } var tab by remember { mutableStateOf(MainTab.Home) }
var navFocused by remember { mutableStateOf(false) } var navFocused by remember { mutableStateOf(false) }
var exitConfirm by remember { mutableStateOf(false) }
val navFocus = remember { FocusRequester() } val navFocus = remember { FocusRequester() }
val homeListState = rememberLazyListState()
val hubScope = rememberCoroutineScope()
val activity = LocalContext.current as? Activity
var shelfPrefs by remember { mutableStateOf<List<ShelfPref>>(emptyList()) } var shelfPrefs by remember { mutableStateOf<List<ShelfPref>>(emptyList()) }
var movieCatalog by remember { mutableStateOf("movies") } var movieCatalog by remember { mutableStateOf("movies") }
var seriesCatalog by remember { mutableStateOf("series") } var seriesCatalog by remember { mutableStateOf("series") }
@ -252,14 +258,41 @@ private fun MainHub(
else -> null else -> null
} }
BackHandler(enabled = !navFocused && tab != MainTab.Home) { fun focusTopNav() {
tab = MainTab.Home
navFocused = true
}
BackHandler(enabled = !navFocused && tab == MainTab.Home) {
navFocused = true navFocused = true
exitConfirm = false
hubScope.launch {
delay(40)
runCatching { navFocus.requestFocus() } 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) { LaunchedEffect(navFocused) {
if (navFocused) { if (navFocused) {
@ -268,13 +301,15 @@ private fun MainHub(
} }
} }
Column(Modifier.fillMaxSize().background(McColors.Bg)) { Box(Modifier.fillMaxSize().background(McColors.Bg)) {
Column(Modifier.fillMaxSize()) {
Box(Modifier.focusRequester(navFocus)) { Box(Modifier.focusRequester(navFocus)) {
TopNavBar( TopNavBar(
selected = tab, selected = tab,
onSelect = { onSelect = {
tab = it tab = it
navFocused = true exitConfirm = false
focusTopNav()
when (it) { when (it) {
MainTab.Movies -> { MainTab.Movies -> {
if (movieSubId == "__search__") { if (movieSubId == "__search__") {
@ -296,7 +331,7 @@ private fun MainHub(
onSelectSub = { item -> onSelectSub = { item ->
if (item.id == "__search__") { if (item.id == "__search__") {
tab = MainTab.Search tab = MainTab.Search
navFocused = true focusTopNav()
} else { } else {
when (tab) { when (tab) {
MainTab.Movies -> { MainTab.Movies -> {
@ -316,6 +351,8 @@ private fun MainHub(
when (tab) { when (tab) {
MainTab.Home -> HomeScreen( MainTab.Home -> HomeScreen(
api = api, api = api,
listState = homeListState,
onContentFocused = { navFocused = false },
onOpenMovie = onOpenMovie, onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries, onOpenSeries = onOpenSeries,
onSeeAll = onBrowse, onSeeAll = onBrowse,
@ -325,6 +362,7 @@ private fun MainHub(
api = api, api = api,
catalogId = movieCatalog, catalogId = movieCatalog,
titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films", titleHint = movieSubs.firstOrNull { it.id == movieSubId }?.label ?: "Films",
onAtTopBack = { focusTopNav() },
onOpenMovie = onOpenMovie, onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries, onOpenSeries = onOpenSeries,
) )
@ -332,6 +370,7 @@ private fun MainHub(
api = api, api = api,
catalogId = seriesCatalog, catalogId = seriesCatalog,
titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series", titleHint = seriesSubs.firstOrNull { it.id == seriesSubId }?.label ?: "Series",
onAtTopBack = { focusTopNav() },
onOpenMovie = onOpenMovie, onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries, onOpenSeries = onOpenSeries,
) )
@ -339,14 +378,49 @@ private fun MainHub(
api = api, api = api,
onOpenMovie = onOpenMovie, onOpenMovie = onOpenMovie,
onOpenSeries = onOpenSeries, onOpenSeries = onOpenSeries,
onAtTopBack = { focusTopNav() },
) )
MainTab.Settings -> SettingsScreen( MainTab.Settings -> SettingsScreen(
api = api, api = api,
onBack = { tab = MainTab.Home }, onBack = {
tab = MainTab.Home
focusTopNav()
},
onLogout = onLogout, 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() }
}
}
}
}
}
} }
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
@ -437,6 +511,8 @@ private fun LinkScreen(api: ApiClient, store: SessionStore, onLinked: () -> Unit
@Composable @Composable
private fun HomeScreen( private fun HomeScreen(
api: ApiClient, api: ApiClient,
listState: androidx.compose.foundation.lazy.LazyListState,
onContentFocused: () -> Unit,
onOpenMovie: (String) -> Unit, onOpenMovie: (String) -> Unit,
onOpenSeries: (String) -> Unit, onOpenSeries: (String) -> Unit,
onSeeAll: (catalogId: String, title: String) -> Unit, onSeeAll: (catalogId: String, title: String) -> Unit,
@ -444,21 +520,37 @@ 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) }
val scope = rememberCoroutineScope()
val menuFocus = remember { FocusRequester() }
suspend fun reloadHome() {
home = api.home()
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
try { try {
home = api.home() reloadHome()
} catch (e: Exception) { } catch (e: Exception) {
error = e.message error = e.message
} }
} }
LaunchedEffect(continueMenu) {
if (continueMenu != null) {
delay(40)
runCatching { menuFocus.requestFocus() }
}
}
val spotlight = home?.spotlight.orEmpty().ifEmpty { val spotlight = home?.spotlight.orEmpty().ifEmpty {
home?.shelves?.flatMap { it.items }?.distinctBy { "${it.type}:${it.id}" }?.take(10).orEmpty() home?.shelves?.flatMap { it.items }?.distinctBy { "${it.type}:${it.id}" }?.take(10).orEmpty()
} }
Box(Modifier.fillMaxSize()) {
LazyColumn( LazyColumn(
Modifier.fillMaxSize(), state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 56.dp), contentPadding = PaddingValues(bottom = 56.dp),
verticalArrangement = Arrangement.spacedBy(28.dp), verticalArrangement = Arrangement.spacedBy(28.dp),
) { ) {
@ -466,6 +558,7 @@ private fun HomeScreen(
SpotlightBillboard( SpotlightBillboard(
items = spotlight, items = spotlight,
onPlay = { item -> onPlay = { item ->
onContentFocused()
when { when {
!item.mediaFileId.isNullOrBlank() -> onPlay( !item.mediaFileId.isNullOrBlank() -> onPlay(
item.mediaFileId!!, item.mediaFileId!!,
@ -480,6 +573,7 @@ private fun HomeScreen(
} }
}, },
onOpen = { item -> onOpen = { item ->
onContentFocused()
if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id)
}, },
) )
@ -500,6 +594,7 @@ private fun HomeScreen(
null null
}, },
onClick = { item -> onClick = { item ->
onContentFocused()
when { when {
!item.mediaFileId.isNullOrBlank() -> onPlay( !item.mediaFileId.isNullOrBlank() -> onPlay(
item.mediaFileId!!, item.mediaFileId!!,
@ -514,6 +609,7 @@ private fun HomeScreen(
else -> onOpenSeries(item.id) else -> onOpenSeries(item.id)
} }
}, },
onLongClick = { item -> continueMenu = item },
) )
} }
} }
@ -527,11 +623,69 @@ private fun HomeScreen(
items = shelf.items, items = shelf.items,
onSeeAll = { onSeeAll(catalogId, shelf.title) }, onSeeAll = { onSeeAll(catalogId, shelf.title) },
onClick = { item -> onClick = { item ->
onContentFocused()
if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.id) if (item.type == "movie") onOpenMovie(item.id) else onOpenSeries(item.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 }
}
}
}
}
} }
@OptIn(ExperimentalTvMaterial3Api::class) @OptIn(ExperimentalTvMaterial3Api::class)
@ -677,6 +831,7 @@ private fun ShelfBlock(
items: List<MediaItem>, items: List<MediaItem>,
onSeeAll: (() -> Unit)?, onSeeAll: (() -> Unit)?,
onClick: (MediaItem) -> Unit, onClick: (MediaItem) -> Unit,
onLongClick: ((MediaItem) -> Unit)? = null,
) { ) {
Column(Modifier.padding(start = 48.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) { Column(Modifier.padding(start = 48.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
Row( Row(
@ -699,7 +854,11 @@ private fun ShelfBlock(
contentPadding = PaddingValues(end = 48.dp), contentPadding = PaddingValues(end = 48.dp),
) { ) {
items(items, key = { "${it.type}:${it.id}:${it.progressId}" }) { item -> 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<List<Pair<String, String>>>(emptyList()) } var files by remember { mutableStateOf<List<Pair<String, String>>>(emptyList()) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val playFocus = remember { FocusRequester() }
LaunchedEffect(movieId) { LaunchedEffect(movieId) {
try { try {
@ -751,6 +911,13 @@ private fun MovieDetailScreen(
} }
} }
LaunchedEffect(files) {
if (files.isNotEmpty()) {
delay(80)
runCatching { playFocus.requestFocus() }
}
}
BackHandler(onBack = onBack) BackHandler(onBack = onBack)
Box(Modifier.fillMaxSize().background(McColors.Bg)) { Box(Modifier.fillMaxSize().background(McColors.Bg)) {
AsyncImage(model = backdrop, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) 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) if (error != null) Text(error!!, color = McColors.Danger)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
files.firstOrNull()?.let { (id, _) -> 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") { McButton("Als gekeken") {
scope.launch { scope.launch {
@ -801,6 +968,7 @@ private fun SeriesDetailScreen(
var backdrop by remember { mutableStateOf<String?>(null) } var backdrop by remember { mutableStateOf<String?>(null) }
var episodes by remember { mutableStateOf<List<Triple<String, String, String>>>(emptyList()) } var episodes by remember { mutableStateOf<List<Triple<String, String, String>>>(emptyList()) }
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
val firstEpFocus = remember { FocusRequester() }
LaunchedEffect(seriesId) { LaunchedEffect(seriesId) {
try { try {
@ -829,6 +997,13 @@ private fun SeriesDetailScreen(
} }
} }
LaunchedEffect(episodes) {
if (episodes.isNotEmpty()) {
delay(80)
runCatching { firstEpFocus.requestFocus() }
}
}
BackHandler(onBack = onBack) BackHandler(onBack = onBack)
Box(Modifier.fillMaxSize().background(McColors.Bg)) { Box(Modifier.fillMaxSize().background(McColors.Bg)) {
AsyncImage(model = backdrop, contentDescription = null, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop, alpha = 0.35f) 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)) Spacer(Modifier.height(16.dp))
Text("Afleveringen", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) Text("Afleveringen", color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
} }
items(episodes) { (id, label, fileId) -> itemsIndexed(episodes) { index, (id, label, fileId) ->
McButton(label) { onPlay(fileId, id, "$title · $label") } McButton(
label,
primary = index == 0,
focusRequester = if (index == 0) firstEpFocus else null,
) { onPlay(fileId, id, "$title · $label") }
} }
} }
} }

View file

@ -194,8 +194,11 @@ fun PlayerScreen(
when { when {
menu == "os" -> menu = "text" menu == "os" -> menu = "text"
menu != null -> menu = null menu != null -> menu = null
controls -> onBack() controls -> {
else -> showControls() menu = null
controls = false
}
else -> onBack()
} }
} }

View file

@ -1,5 +1,6 @@
package nl.vonas.mediacluster.tv.ui package nl.vonas.mediacluster.tv.ui
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.Column 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.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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
@ -42,6 +43,7 @@ fun SearchScreen(
api: ApiClient, api: ApiClient,
onOpenMovie: (String) -> Unit, onOpenMovie: (String) -> Unit,
onOpenSeries: (String) -> Unit, onOpenSeries: (String) -> Unit,
onAtTopBack: (() -> Unit)? = null,
) { ) {
var query by remember { mutableStateOf("") } var query by remember { mutableStateOf("") }
var results by remember { mutableStateOf<List<MediaItem>>(emptyList()) } var results by remember { mutableStateOf<List<MediaItem>>(emptyList()) }
@ -49,6 +51,19 @@ fun SearchScreen(
var error by remember { mutableStateOf<String?>(null) } var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var searchJob by remember { mutableStateOf<Job?>(null) } var searchJob by remember { mutableStateOf<Job?>(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) { fun scheduleSearch(next: String) {
query = next query = next
@ -127,6 +142,7 @@ fun SearchScreen(
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 140.dp), columns = GridCells.Adaptive(minSize = 140.dp),
state = gridState,
contentPadding = PaddingValues(bottom = 40.dp), contentPadding = PaddingValues(bottom = 40.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),

View file

@ -12,6 +12,8 @@ import androidx.compose.runtime.Composable
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.graphics.Color import androidx.compose.ui.graphics.Color
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
@ -35,6 +37,7 @@ fun McButton(
label: String, label: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
primary: Boolean = false, primary: Boolean = false,
focusRequester: FocusRequester? = null,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val bg = if (primary) Color.White else Color(0x33FFFFFF) val bg = if (primary) Color.White else Color(0x33FFFFFF)
@ -43,7 +46,9 @@ fun McButton(
val fgFocus = Color.Black val fgFocus = Color.Black
Surface( Surface(
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier.then(
if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier,
),
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)), shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(4.dp)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f), scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
glow = ClickableSurfaceDefaults.glow( glow = ClickableSurfaceDefaults.glow(
@ -76,6 +81,7 @@ fun PosterCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
width: Dp = 140.dp, width: Dp = 140.dp,
height: Dp = 210.dp, height: Dp = 210.dp,
onLongClick: (() -> Unit)? = null,
) { ) {
val progress = if (item.durationMs > 0) { val progress = if (item.durationMs > 0) {
(item.positionMs.toFloat() / item.durationMs.toFloat()).coerceIn(0f, 1f) (item.positionMs.toFloat() / item.durationMs.toFloat()).coerceIn(0f, 1f)
@ -85,6 +91,7 @@ fun PosterCard(
Surface( Surface(
onClick = onClick, onClick = onClick,
onLongClick = onLongClick,
modifier = modifier.width(width), modifier = modifier.width(width),
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),
@ -117,18 +124,11 @@ fun PosterCard(
Box( Box(
Modifier Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.fillMaxWidth()
.height(3.dp)
.background(Color(0x55FFFFFF)),
) {
Box(
Modifier
.fillMaxWidth(progress) .fillMaxWidth(progress)
.height(3.dp) .height(4.dp)
.background(McColors.Accent), .background(McColors.Brand),
) )
} }
} }
} }
} }
}

View file

@ -757,6 +757,8 @@ export class ViewerService {
? { hiddenFromContinue: body.hiddenFromContinue } ? { hiddenFromContinue: body.hiddenFromContinue }
: autoComplete : autoComplete
? { hiddenFromContinue: true } ? { hiddenFromContinue: true }
: positionMs > 30_000
? { hiddenFromContinue: false }
: {}), : {}),
}, },
}); });
@ -791,6 +793,8 @@ export class ViewerService {
? { hiddenFromContinue: body.hiddenFromContinue } ? { hiddenFromContinue: body.hiddenFromContinue }
: autoComplete : autoComplete
? { hiddenFromContinue: true } ? { hiddenFromContinue: true }
: positionMs > 30_000
? { hiddenFromContinue: false }
: {}), : {}),
}, },
}); });