diff --git a/apps/android-tv/app/build.gradle.kts b/apps/android-tv/app/build.gradle.kts index e30c29f..905e4f2 100644 --- a/apps/android-tv/app/build.gradle.kts +++ b/apps/android-tv/app/build.gradle.kts @@ -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") 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 b9ceb81..a2ae59d 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 @@ -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), ) } diff --git a/apps/master-api/src/viewer/service.ts b/apps/master-api/src/viewer/service.ts index 11d32c3..d75bdea 100644 --- a/apps/master-api/src/viewer/service.ts +++ b/apps/master-api/src/viewer/service.ts @@ -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,