Prefer series poster in continue watching with episode overlay.

Use the series poster instead of episode stills, and show SxE plus title on Verder kijken cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jos Vooges | STH 2026-09-06 00:51:48 +02:00
parent 388962e6d1
commit 4cd4347daf
3 changed files with 96 additions and 9 deletions

View file

@ -20,8 +20,8 @@ android {
applicationId = "nl.vonas.mediacluster.tv" applicationId = "nl.vonas.mediacluster.tv"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 43 versionCode = 68
versionName = "0.12.1" versionName = "0.14.5"
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

@ -1,7 +1,10 @@
package nl.vonas.mediacluster.tv.ui.components package nl.vonas.mediacluster.tv.ui.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
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
@ -17,7 +20,9 @@ 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.focusProperties
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
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.Key
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
@ -47,6 +52,9 @@ fun McButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
primary: Boolean = false, primary: Boolean = false,
focusRequester: FocusRequester? = null, focusRequester: FocusRequester? = null,
focusUp: FocusRequester? = null,
focusDown: FocusRequester? = null,
onFocused: (() -> Unit)? = null,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
val bg = if (primary) Color.White else Color(0x33FFFFFF) val bg = if (primary) Color.White else Color(0x33FFFFFF)
@ -55,9 +63,21 @@ fun McButton(
val fgFocus = Color.Black val fgFocus = Color.Black
Surface( Surface(
onClick = onClick, onClick = onClick,
modifier = modifier.then( modifier = modifier
if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier, .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
), .then(
if (focusUp != null || focusDown != null) {
Modifier.focusProperties {
focusUp?.let { up = it }
focusDown?.let { down = it }
}
} else {
Modifier
},
)
.onFocusChanged { state ->
if (state.isFocused) onFocused?.invoke()
},
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(
@ -116,6 +136,13 @@ fun PosterCard(
width: Dp = 140.dp, width: Dp = 140.dp,
height: Dp = 210.dp, height: Dp = 210.dp,
onLongClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null,
/** Toon episode/subtitle overlay (o.a. Verder kijken). */
showCaption: Boolean = false,
/**
* Grid-modus (Films/Series): vul celbreedte met vaste poster-ratio 2:3
* (zelfde framing als Home), i.p.v. vaste dp die in bredere cellen verkeerd cropt.
*/
fillWidth: Boolean = false,
) { ) {
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)
@ -128,7 +155,7 @@ fun PosterCard(
onClick = if (useKeyHandler) ({}) else onClick, onClick = if (useKeyHandler) ({}) else onClick,
onLongClick = null, onLongClick = null,
modifier = modifier modifier = modifier
.width(width) .then(if (fillWidth) Modifier.fillMaxWidth() else Modifier.width(width))
.then( .then(
if (useKeyHandler) { if (useKeyHandler) {
Modifier.tvShortOrLongPress(onClick = onClick, onLongPress = onLongClick!!) Modifier.tvShortOrLongPress(onClick = onClick, onLongPress = onLongClick!!)
@ -153,7 +180,10 @@ fun PosterCard(
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(height) .then(
if (fillWidth) Modifier.aspectRatio(2f / 3f)
else Modifier.height(height)
)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(Color(0xFF1A1A1A)), .background(Color(0xFF1A1A1A)),
) { ) {
@ -162,13 +192,70 @@ fun PosterCard(
contentDescription = item.title, contentDescription = item.title,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
alignment = Alignment.Center,
) )
if (showCaption && !item.subtitle.isNullOrBlank()) {
val parts = item.subtitle!!.split(" · ", limit = 2)
val epCode = parts.getOrNull(0)
val epTitle = parts.getOrNull(1)
Box(
Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.background(
androidx.compose.ui.graphics.Brush.verticalGradient(
listOf(Color.Transparent, Color(0xEE000000)),
),
)
.padding(horizontal = 8.dp, vertical = 8.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
if (!epCode.isNullOrBlank()) {
Text(
epCode,
color = Color.White,
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
if (!epTitle.isNullOrBlank()) {
Text(
epTitle,
color = Color(0xFFD0D0D0),
fontSize = 10.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else if (epCode.isNullOrBlank()) {
Text(
item.subtitle!!,
color = Color.White,
fontSize = 11.sp,
fontWeight = FontWeight.Medium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
if (progress > 0.02f) { if (progress > 0.02f) {
val barH = if (showCaption) 5.dp else 4.dp
Box(
Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(barH)
.background(Color(0x66FFFFFF)),
)
Box( Box(
Modifier Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.fillMaxWidth(progress) .fillMaxWidth(progress)
.height(4.dp) .height(barH)
.background(McColors.Brand), .background(McColors.Brand),
) )
} }

View file

@ -816,7 +816,7 @@ export class ViewerService {
episode.title ? ` · ${episode.title}` : "" episode.title ? ` · ${episode.title}` : ""
}`, }`,
year: episode.series.year, year: episode.series.year,
posterUrl: episode.stillUrl || episode.series.posterUrl, posterUrl: episode.series.posterUrl || episode.stillUrl,
backdropUrl: episode.series.backdropUrl, backdropUrl: episode.series.backdropUrl,
mediaFileId: file?.id ?? null, mediaFileId: file?.id ?? null,
positionMs: p.positionMs, positionMs: p.positionMs,