polish Events detail and stop hero jump on provider chips
This commit is contained in:
parent
5115682427
commit
929a2b1137
2 changed files with 252 additions and 298 deletions
|
|
@ -20,8 +20,8 @@ android {
|
|||
applicationId = "nl.vonas.mediacluster.tv"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 120
|
||||
versionName = "0.14.57"
|
||||
versionCode = 121
|
||||
versionName = "0.14.58"
|
||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@ package nl.vonas.mediacluster.tv.ui
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.gestures.BringIntoViewSpec
|
||||
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
|
||||
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
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -20,10 +25,12 @@ import androidx.compose.foundation.layout.width
|
|||
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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -37,7 +44,6 @@ import androidx.compose.runtime.snapshotFlow
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
|
|
@ -87,7 +93,20 @@ private val StreamLabelRegex = Regex("""^stream\s*\d+""", RegexOption.IGNORE_CAS
|
|||
private const val HeroRotateMs = 7_000L
|
||||
private const val HeroFadeMs = 850
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||
/**
|
||||
* Provider chips zitten onder de hero-spacer. Standaard BringIntoView zou de LazyColumn
|
||||
* scrollen → collapsePx springt. We willen focus op chips zonder hero-beweging.
|
||||
*/
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private val ChipsNoScrollBringIntoViewSpec = object : BringIntoViewSpec {
|
||||
override fun calculateScrollDistance(
|
||||
offset: Float,
|
||||
size: Float,
|
||||
containerSize: Float,
|
||||
): Float = 0f
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTvMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun EventsScreen(
|
||||
api: ApiClient,
|
||||
|
|
@ -197,9 +216,6 @@ fun EventsScreen(
|
|||
if (showProviderChips) idx++
|
||||
idx
|
||||
}
|
||||
val chipsListIndex = remember(showProviderChips) {
|
||||
if (!showProviderChips) -1 else 1 // after spacer
|
||||
}
|
||||
|
||||
fun expandHeroFully() {
|
||||
if (listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0) {
|
||||
|
|
@ -233,13 +249,8 @@ fun EventsScreen(
|
|||
scope.launch {
|
||||
if (!showProviderChips) return@launch
|
||||
if (!awaitReady({ chipsFocusReady })) return@launch
|
||||
runCatching {
|
||||
if (chipsListIndex >= 0) {
|
||||
listState.animateScrollToItem(chipsListIndex)
|
||||
delay(40)
|
||||
}
|
||||
chipsFocus.requestFocus()
|
||||
}
|
||||
// Geen animateScrollToItem: dat collapst de hero via collapsePx.
|
||||
runCatching { chipsFocus.requestFocus() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -380,6 +391,9 @@ fun EventsScreen(
|
|||
}
|
||||
if (showProviderChips) {
|
||||
item(key = "providers") {
|
||||
CompositionLocalProvider(
|
||||
LocalBringIntoViewSpec provides ChipsNoScrollBringIntoViewSpec,
|
||||
) {
|
||||
ProviderChips(
|
||||
providers = providers,
|
||||
selected = providerFilter,
|
||||
|
|
@ -392,10 +406,8 @@ fun EventsScreen(
|
|||
focusUp = chipsUpTarget,
|
||||
focusDown = chipsDownTarget,
|
||||
onFocusReady = { chipsFocusReady = it },
|
||||
onFocus = {
|
||||
onContentFocused()
|
||||
if (showHero) expandHeroFully()
|
||||
},
|
||||
// Alleen content-focus melden — géén expand/scroll (hero jump).
|
||||
onFocus = onContentFocused,
|
||||
onFocusDownFallback = if (chipsDownTarget == null) {
|
||||
({ focusFirstRail() })
|
||||
} else {
|
||||
|
|
@ -404,6 +416,7 @@ fun EventsScreen(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (live.isNotEmpty()) {
|
||||
item(key = "live") {
|
||||
EventRail(
|
||||
|
|
@ -462,9 +475,8 @@ fun EventDetailScreen(
|
|||
var nowMs by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
||||
val playFocus = remember { FocusRequester() }
|
||||
val backFocus = remember { FocusRequester() }
|
||||
val galleryFocus = remember { FocusRequester() }
|
||||
val listState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
val bodyFocus = remember { FocusRequester() }
|
||||
val bodyScroll = rememberScrollState()
|
||||
|
||||
LaunchedEffect(eventId) {
|
||||
runCatching {
|
||||
|
|
@ -492,34 +504,6 @@ fun EventDetailScreen(
|
|||
BackHandler(onBack = onBack)
|
||||
|
||||
val ev = event
|
||||
val gallery = remember(ev) {
|
||||
ev?.images?.distinct()?.filter {
|
||||
it != ev.imageLandscape && it != ev.logoUrl
|
||||
}?.take(12).orEmpty()
|
||||
}
|
||||
val hasGallery = gallery.isNotEmpty()
|
||||
// Lazy items: 0=hero, 1=cta, 2?=desc, 3?=audio, last=gallery
|
||||
val galleryListIndex = remember(ev, hasGallery) {
|
||||
if (!hasGallery || ev == null) -1
|
||||
else {
|
||||
var idx = 2 // after hero + cta
|
||||
if (!ev.description.isNullOrBlank() || !ev.subtitle.isNullOrBlank()) idx++
|
||||
if (ev.audioLanguages.isNotEmpty()) idx++
|
||||
idx
|
||||
}
|
||||
}
|
||||
|
||||
fun scrollToGallery() {
|
||||
if (galleryListIndex < 0) return
|
||||
scope.launch {
|
||||
runCatching {
|
||||
listState.animateScrollToItem(galleryListIndex)
|
||||
delay(60)
|
||||
galleryFocus.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.fillMaxSize().background(McColors.Bg)) {
|
||||
when {
|
||||
ev == null && error == null -> {
|
||||
|
|
@ -537,18 +521,12 @@ fun EventDetailScreen(
|
|||
}
|
||||
}
|
||||
else -> {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 40.dp),
|
||||
) {
|
||||
item(key = "hero") {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(420.dp)
|
||||
.clip(RectangleShape),
|
||||
) {
|
||||
val showSubtitle = !ev.subtitle.isNullOrBlank() &&
|
||||
!ev.subtitle.equals(ev.name, ignoreCase = true)
|
||||
val showDescription = !ev.description.isNullOrBlank()
|
||||
val hasFocusableBody = showSubtitle || showDescription
|
||||
|
||||
// Full-bleed landscape backdrop
|
||||
AsyncImage(
|
||||
model = eventArtUrl(ev),
|
||||
contentDescription = ev.name,
|
||||
|
|
@ -556,74 +534,90 @@ fun EventDetailScreen(
|
|||
alignment = Alignment.TopCenter,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
// Strong horizontal + vertical scrims (SpotlightHero / MovieDetail)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Modifier.fillMaxSize().background(
|
||||
Brush.horizontalGradient(
|
||||
0.0f to Color(0xF20B0B0B),
|
||||
0.40f to Color(0xCC0B0B0B),
|
||||
0.72f to Color(0x550B0B0B),
|
||||
1.0f to Color(0x180B0B0B),
|
||||
),
|
||||
),
|
||||
)
|
||||
Box(
|
||||
Modifier.fillMaxSize().background(
|
||||
Brush.verticalGradient(
|
||||
listOf(
|
||||
Color(0x44000000),
|
||||
Color.Transparent,
|
||||
Color(0x880B0B0B),
|
||||
McColors.Bg,
|
||||
),
|
||||
0.0f to Color(0x66000000),
|
||||
0.12f to Color.Transparent,
|
||||
0.45f to Color.Transparent,
|
||||
0.72f to Color(0xAA0B0B0B),
|
||||
1.0f to Color(0xFF0B0B0B),
|
||||
),
|
||||
),
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(horizontal = HomeChrome.EdgeInset, vertical = 28.dp)
|
||||
.fillMaxWidth(0.72f),
|
||||
) {
|
||||
StatusBadge(ev, nowMs)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
ev.name,
|
||||
color = McColors.Text,
|
||||
fontSize = 34.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 40.sp,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val meta = metaLine(ev)
|
||||
if (meta.isNotBlank()) {
|
||||
Text(
|
||||
meta,
|
||||
color = McColors.TextDim,
|
||||
fontSize = 15.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
Text(
|
||||
timeWindowLabel(ev),
|
||||
color = McColors.Text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
ev.logoUrl?.let { logo ->
|
||||
|
||||
ev.logoUrl?.takeIf { it.isNotBlank() }?.let { logo ->
|
||||
AsyncImage(
|
||||
model = logo,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 24.dp, end = HomeChrome.EdgeInset)
|
||||
.height(44.dp)
|
||||
.width(110.dp),
|
||||
.padding(top = HomeChrome.BadgeTop, end = HomeChrome.EdgeInset)
|
||||
.height(48.dp)
|
||||
.width(120.dp),
|
||||
contentScale = ContentScale.Fit,
|
||||
alignment = Alignment.CenterEnd,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(0.62f)
|
||||
.padding(top = HomeChrome.BadgeTop)
|
||||
.padding(
|
||||
start = HomeChrome.EdgeInset,
|
||||
end = HomeChrome.EdgeInset,
|
||||
bottom = 36.dp,
|
||||
)
|
||||
.verticalScroll(bodyScroll),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
StatusBadge(ev, nowMs)
|
||||
Text(
|
||||
ev.name,
|
||||
color = Color.White,
|
||||
fontSize = 40.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
lineHeight = 46.sp,
|
||||
)
|
||||
val meta = detailMetaLine(ev)
|
||||
if (meta.isNotBlank()) {
|
||||
Text(
|
||||
meta,
|
||||
color = Color(0xFFCFCFCF),
|
||||
fontSize = 15.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
val window = detailTimeWindowLabel(ev, nowMs)
|
||||
if (window.isNotBlank()) {
|
||||
Text(
|
||||
window,
|
||||
color = McColors.Text,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
item(key = "cta") {
|
||||
|
||||
Spacer(Modifier.height(6.dp))
|
||||
|
||||
Row(
|
||||
Modifier.padding(horizontal = HomeChrome.EdgeInset),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (ev.playable) {
|
||||
|
|
@ -631,7 +625,7 @@ fun EventDetailScreen(
|
|||
"Kijk live",
|
||||
primary = true,
|
||||
focusRequester = playFocus,
|
||||
onFocusDownFallback = if (hasGallery) ({ scrollToGallery() }) else null,
|
||||
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
||||
onClick = { onPlay(ev.id) },
|
||||
)
|
||||
} else {
|
||||
|
|
@ -640,144 +634,76 @@ fun EventDetailScreen(
|
|||
primary = false,
|
||||
enabled = false,
|
||||
focusRequester = playFocus,
|
||||
onFocusDownFallback = if (hasGallery) ({ scrollToGallery() }) else null,
|
||||
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
McDetailButton(
|
||||
"Terug",
|
||||
focusRequester = backFocus,
|
||||
onFocusDownFallback = if (hasGallery) ({ scrollToGallery() }) else null,
|
||||
focusDown = bodyFocus.takeIf { hasFocusableBody },
|
||||
onClick = onBack,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!ev.description.isNullOrBlank() || !ev.subtitle.isNullOrBlank()) {
|
||||
item(key = "desc") {
|
||||
|
||||
if (hasFocusableBody) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(HomeChrome.EdgeInset)
|
||||
.padding(top = 22.dp),
|
||||
.focusRequester(bodyFocus)
|
||||
.focusable()
|
||||
.focusProperties { up = playFocus }
|
||||
.padding(top = 4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (!ev.eventType.isNullOrBlank() || !ev.subtitle.isNullOrBlank()) {
|
||||
if (showSubtitle) {
|
||||
Text(
|
||||
listOfNotNull(ev.eventType, ev.subtitle)
|
||||
.joinToString(" · "),
|
||||
ev.subtitle!!,
|
||||
color = McColors.TextDim,
|
||||
fontSize = 15.sp,
|
||||
fontSize = 14.sp,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
if (!ev.description.isNullOrBlank()) {
|
||||
if (showDescription) {
|
||||
Text(
|
||||
ev.description!!,
|
||||
color = McColors.Text,
|
||||
color = Color(0xFFE8E8E8),
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
lineHeight = 26.sp,
|
||||
maxLines = 8,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ev.audioLanguages.isNotEmpty()) {
|
||||
item(key = "audio") {
|
||||
Column(Modifier.padding(HomeChrome.EdgeInset).padding(top = 18.dp)) {
|
||||
Text("Audio", color = McColors.TextDim, fontSize = 13.sp)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(ev.audioLanguages) { lang ->
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(McColors.Surface)
|
||||
.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(lang.uppercase(), color = McColors.Text, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasGallery) {
|
||||
item(key = "gallery") {
|
||||
Column(Modifier.padding(top = 28.dp)) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
"Beelden",
|
||||
color = McColors.Text,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
modifier = Modifier.padding(horizontal = HomeChrome.EdgeInset),
|
||||
"Audio",
|
||||
color = Color(0xFF8F8F8F),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
letterSpacing = 0.6.sp,
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = HomeChrome.EdgeInset),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
itemsIndexed(gallery) { index, url ->
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Surface(
|
||||
onClick = {},
|
||||
modifier = Modifier
|
||||
.width(220.dp)
|
||||
.height(124.dp)
|
||||
.then(
|
||||
if (index == 0) {
|
||||
Modifier.focusRequester(galleryFocus)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.focusProperties {
|
||||
up = playFocus
|
||||
}
|
||||
.onFocusChanged {
|
||||
focused = it.isFocused
|
||||
if (it.isFocused) {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
listState.animateScrollToItem(
|
||||
galleryListIndex.coerceAtLeast(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scale = ClickableSurfaceDefaults.scale(focusedScale = 1.05f),
|
||||
glow = ClickableSurfaceDefaults.glow(
|
||||
focusedGlow = Glow(
|
||||
elevationColor = McColors.Accent,
|
||||
elevation = 6.dp,
|
||||
),
|
||||
),
|
||||
colors = ClickableSurfaceDefaults.colors(
|
||||
containerColor = McColors.Surface,
|
||||
focusedContainerColor = McColors.Surface,
|
||||
),
|
||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(10.dp)),
|
||||
) {
|
||||
AsyncImage(
|
||||
model = url,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
if (focused) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ev.audioLanguages.take(8).forEach { lang ->
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0x22000000)),
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(Color(0x33FFFFFF))
|
||||
.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
) {
|
||||
Text(
|
||||
lang.uppercase(),
|
||||
color = Color(0xFFDDDDDD),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(40.dp))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
item(key = "spacer") { Spacer(Modifier.height(40.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1290,7 +1216,7 @@ private fun McDetailButton(
|
|||
focusDown: FocusRequester? = null,
|
||||
onFocus: (() -> Unit)? = null,
|
||||
onFocusChanged: ((Boolean) -> Unit)? = null,
|
||||
/** When gallery/rail is off-screen, scroll+focus before default D-pad move. */
|
||||
/** When next down target is off-screen, scroll+focus before default D-pad move. */
|
||||
onFocusDownFallback: (() -> Unit)? = null,
|
||||
) {
|
||||
val mod = Modifier
|
||||
|
|
@ -1412,6 +1338,25 @@ private fun timeWindowLabel(event: ScheduleEvent): String {
|
|||
return "${start.format(TimeFmt)} – ${stop.format(TimeFmt)}"
|
||||
}
|
||||
|
||||
/** Detail: HH:mm–HH:mm met Vandaag/Morgen als het zinvol is. */
|
||||
private fun detailTimeWindowLabel(event: ScheduleEvent, nowMs: Long): String {
|
||||
val start = parseZoned(event.startLocal, event.start) ?: return ""
|
||||
val stop = parseZoned(event.stopLocal, event.stop)
|
||||
val range = if (stop != null) {
|
||||
"${start.format(TimeFmt)} – ${stop.format(TimeFmt)}"
|
||||
} else {
|
||||
start.format(TimeFmt)
|
||||
}
|
||||
if (event.phase == "live" || event.isLive) return range
|
||||
val today = Instant.ofEpochMilli(nowMs).atZone(ZoneNl).toLocalDate()
|
||||
val day = start.toLocalDate()
|
||||
return when {
|
||||
day == today -> "Vandaag · $range"
|
||||
day == today.plusDays(1) -> "Morgen · $range"
|
||||
else -> "${start.format(DayTimeFmt)}${stop?.let { " – ${it.format(TimeFmt)}" } ?: ""}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun isStreamLabel(value: String?): Boolean {
|
||||
val s = value?.trim().orEmpty()
|
||||
if (s.isEmpty()) return false
|
||||
|
|
@ -1430,6 +1375,15 @@ private fun metaLine(event: ScheduleEvent): String =
|
|||
displayChannel(event),
|
||||
).joinToString(" · ")
|
||||
|
||||
/** Detail meta: provider · category · type — nooit Stream N. */
|
||||
private fun detailMetaLine(event: ScheduleEvent): String =
|
||||
listOfNotNull(
|
||||
prettyProvider(event.provider),
|
||||
event.category?.takeIf { it.isNotBlank() },
|
||||
event.eventType?.takeIf { it.isNotBlank() },
|
||||
displayChannel(event),
|
||||
).distinct().joinToString(" · ")
|
||||
|
||||
private fun eventArtUrl(event: ScheduleEvent): String? =
|
||||
event.imageLandscape?.takeIf { it.isNotBlank() }
|
||||
?: event.images.firstOrNull { looksLikeArtwork(it) }
|
||||
|
|
|
|||
Loading…
Reference in a new issue