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:
parent
7e624dd452
commit
04d6a38234
7 changed files with 371 additions and 158 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<List<ShelfPref>>(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<HomePayload?>(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) {
|
||||
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<MediaItem>,
|
||||
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<List<Pair<String, String>>>(emptyList()) }
|
||||
var error by remember { mutableStateOf<String?>(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<String?>(null) }
|
||||
var episodes by remember { mutableStateOf<List<Triple<String, String, String>>>(emptyList()) }
|
||||
var error by remember { mutableStateOf<String?>(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") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<List<MediaItem>>(emptyList()) }
|
||||
|
|
@ -49,6 +51,19 @@ fun SearchScreen(
|
|||
var error by remember { mutableStateOf<String?>(null) }
|
||||
val scope = rememberCoroutineScope()
|
||||
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) {
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue