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"
minSdk = 24
targetSdk = 36
versionCode = 43
versionName = "0.12.1"
versionCode = 68
versionName = "0.14.5"
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")

View file

@ -1,7 +1,10 @@
package nl.vonas.mediacluster.tv.ui.components
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -17,7 +20,9 @@ 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.focusProperties
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
@ -47,6 +52,9 @@ fun McButton(
modifier: Modifier = Modifier,
primary: Boolean = false,
focusRequester: FocusRequester? = null,
focusUp: FocusRequester? = null,
focusDown: FocusRequester? = null,
onFocused: (() -> Unit)? = null,
onClick: () -> Unit,
) {
val bg = if (primary) Color.White else Color(0x33FFFFFF)
@ -55,9 +63,21 @@ fun McButton(
val fgFocus = Color.Black
Surface(
onClick = onClick,
modifier = modifier.then(
if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier,
),
modifier = 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)),
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.06f),
glow = ClickableSurfaceDefaults.glow(
@ -116,6 +136,13 @@ fun PosterCard(
width: Dp = 140.dp,
height: Dp = 210.dp,
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) {
(item.positionMs.toFloat() / item.durationMs.toFloat()).coerceIn(0f, 1f)
@ -128,7 +155,7 @@ fun PosterCard(
onClick = if (useKeyHandler) ({}) else onClick,
onLongClick = null,
modifier = modifier
.width(width)
.then(if (fillWidth) Modifier.fillMaxWidth() else Modifier.width(width))
.then(
if (useKeyHandler) {
Modifier.tvShortOrLongPress(onClick = onClick, onLongPress = onLongClick!!)
@ -153,7 +180,10 @@ fun PosterCard(
Box(
Modifier
.fillMaxWidth()
.height(height)
.then(
if (fillWidth) Modifier.aspectRatio(2f / 3f)
else Modifier.height(height)
)
.clip(RoundedCornerShape(4.dp))
.background(Color(0xFF1A1A1A)),
) {
@ -162,13 +192,70 @@ fun PosterCard(
contentDescription = item.title,
modifier = Modifier.fillMaxSize(),
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) {
val barH = if (showCaption) 5.dp else 4.dp
Box(
Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(barH)
.background(Color(0x66FFFFFF)),
)
Box(
Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(progress)
.height(4.dp)
.height(barH)
.background(McColors.Brand),
)
}

View file

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