From d42f24806a6a6e5b4051ec244152b0ca8114e540 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 20 Oct 2025 14:50:13 -0400 Subject: [PATCH] Styling guide --- .../ui/detail/livetv/LiveTvViewModel.kt | 37 +- .../wholphin/ui/detail/livetv/TvGrid.kt | 676 ++++-------------- 2 files changed, 144 insertions(+), 569 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index ff5f84ee..a7e3076a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -21,7 +21,6 @@ import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber import java.time.LocalDateTime -import java.time.ZoneOffset import java.time.temporal.ChronoUnit import java.util.UUID import javax.inject.Inject @@ -85,15 +84,6 @@ class LiveTvViewModel .execute(api, request) .content.items .map { dto -> - val utcStart = dto.startDate!!.toEpochSecond(ZoneOffset.UTC) - utcStart - val ttt = - java.time.Duration - .between( - start, - dto.startDate!!, - ) - ttt TvProgram( id = dto.id, channelId = dto.channelId!!, @@ -103,30 +93,19 @@ class LiveTvViewModel java.time.Duration .between(start, dto.startDate!!) .seconds / (60f * 60f), -// dto.startDate!!.hours + -// ( -// java.time.Duration -// .between( -// start, -// dto.startDate!!, -// ).toDaysPart() / 24 -// ).coerceAtLeast(0), endHours = java.time.Duration .between(start, dto.endDate!!) .seconds / (60f * 60f), -// dto.endDate!!.hours + -// ( -// java.time.Duration -// .between( -// start, -// dto.endDate!!, -// ).toHours() / 24 -// ).coerceAtLeast(0), duration = dto.runTimeTicks!!.ticks, name = dto.seriesName ?: dto.name, - subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() }, - seasonEpisode = null, // TODO + subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false }, + seasonEpisode = + if (dto.indexNumber != null && dto.parentIndexNumber != null) { + SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!) + } else { + null + }, ) }.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side @@ -146,7 +125,7 @@ class LiveTvViewModel startHours = it.toFloat(), endHours = (it + 1).toFloat(), duration = 60.seconds, - name = "No date", + name = "No data", subtitle = null, seasonEpisode = null, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt index f0d35aeb..cc361b5a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt @@ -1,59 +1,41 @@ package com.github.damontecres.wholphin.ui.detail.livetv import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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 -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection -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 import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.times import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.tv.material3.Button -import androidx.tv.material3.ListItem import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import androidx.tv.material3.contentColorFor import androidx.tv.material3.surfaceColorAtElevation import coil3.compose.AsyncImage import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage -import com.github.damontecres.wholphin.ui.ifElse -import com.github.damontecres.wholphin.ui.playback.isDown -import com.github.damontecres.wholphin.ui.playback.isUp +import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.rememberInt -import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import eu.wewox.programguide.ProgramGuide @@ -64,16 +46,6 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.time.LocalDateTime import java.util.UUID -import kotlin.time.Duration.Companion.minutes - -fun translate( - channelIndex: Int, - programIndex: Int, -) { - val before = - channels.subList(0, channelIndex + 1).map { it.id }.sumOf { programs[it]?.size ?: 0 } - programIndex - before -} @Composable fun TvGrid( @@ -96,19 +68,15 @@ fun TvGrid( LoadingState.Success -> Column(modifier = modifier) { - Button( - onClick = {}, - ) { - Text( - text = "Button", - ) - } TvGrid( channels = channels, programList = programs, programs = programsByChannel, start = viewModel.start, - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface), ) } } @@ -126,15 +94,15 @@ fun TvGrid( val state = rememberSaveableProgramGuideState() val scope = rememberCoroutineScope() // val timeline = 0..<24 - var focusedProgram by rememberInt(0) - var focusedChannel by rememberInt(0) + var focusedProgramIndex by rememberInt(0) + var focusedChannelIndex by rememberInt(0) val dimensions = ProgramGuideDimensions( timelineHourWidth = 240.dp, timelineHeight = 32.dp, - channelWidth = 64.dp, - channelHeight = 64.dp, + channelWidth = 120.dp, + channelHeight = 80.dp, currentTimeWidth = 2.dp, ) @@ -152,44 +120,75 @@ fun TvGrid( } val newIndex = when (it.key) { - Key.DirectionRight -> focusedProgram + 1 - Key.DirectionLeft -> focusedProgram - 1 + Key.DirectionRight -> { + val nextProgramIndex = focusedProgramIndex + 1 + val programsBefore = programsBeforeChannel(focusedChannelIndex) + val relativePosition = nextProgramIndex - programsBefore + val channelPrograms = + programs[channels[focusedChannelIndex].id].orEmpty() + if (relativePosition >= channelPrograms.size) { + focusManager.moveFocus(FocusDirection.Right) + null + } else { + nextProgramIndex + } + } + + Key.DirectionLeft -> { + val nextProgramIndex = focusedProgramIndex - 1 + if (nextProgramIndex >= 0) { + val programsBefore = programsBeforeChannel(focusedChannelIndex) + val relativePosition = nextProgramIndex - programsBefore +// val channelPrograms = +// programs[channels[focusedChannel].id].orEmpty() + if (relativePosition < 0) { + focusManager.moveFocus(FocusDirection.Left) + null + } else { + nextProgramIndex + } + } else { + focusManager.moveFocus(FocusDirection.Left) + null + } + } + Key.DirectionUp -> { - val start = programList[focusedProgram].startHours - focusedChannel = (focusedChannel - 1) - if (focusedChannel < 0) { + val start = programList[focusedProgramIndex].startHours + focusedChannelIndex = (focusedChannelIndex - 1) + if (focusedChannelIndex < 0) { focusManager.moveFocus(FocusDirection.Up) null } else { - val channelId = channels[focusedChannel].id + val channelId = channels[focusedChannelIndex].id val pIndex = programs[channelId]?.indexOfFirst { start in (it.startHours..= 0) { - programsBeforeChannel(focusedChannel) + pIndex + programsBeforeChannel(focusedChannelIndex) + pIndex } else { - programsBeforeChannel(focusedChannel) + programs[channelId]!!.size + programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size } } } Key.DirectionDown -> { - val start = programList[focusedProgram].startHours - focusedChannel = - (focusedChannel + 1).coerceAtMost(channels.size - 1) - val channelId = channels[focusedChannel].id + val start = programList[focusedProgramIndex].startHours + focusedChannelIndex = + (focusedChannelIndex + 1).coerceAtMost(channels.size - 1) + val channelId = channels[focusedChannelIndex].id val pro = programs[channelId].orEmpty() val pIndex = pro.indexOfFirst { start in (it.startHours..= 0) { - programsBeforeChannel(focusedChannel) + pIndex + programsBeforeChannel(focusedChannelIndex) + pIndex } else { - programsBeforeChannel(focusedChannel) + programs[channelId]!!.size + programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size } } Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> { - Timber.v("Clicked on ${programList[focusedProgram]}") + Timber.v("Clicked on ${programList[focusedProgramIndex]}") null } @@ -198,13 +197,13 @@ fun TvGrid( } } if (newIndex != null) { - val before = programsBeforeChannel(focusedChannel) + val before = programsBeforeChannel(focusedChannelIndex) val max = - before + channels[focusedChannel].let { programs[it.id]!!.size } - 1 + before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1 val index = newIndex.coerceIn(before, max) scope.launch(ExceptionHandler()) { state.animateToProgram(index, Alignment.Center) - focusedProgram = index + focusedProgramIndex = index } return@onPreviewKeyEvent true } @@ -221,10 +220,13 @@ fun TvGrid( ) }, ) { index -> - val time = start.plusHours(index.toLong()).toLocalTime() - Text( - text = time.toString(), - ) + Box(modifier = Modifier.fillMaxSize()) { + val time = start.plusHours(index.toLong()).toLocalTime() + Text( + text = time.toString(), + modifier = Modifier.background(MaterialTheme.colorScheme.background), + ) + } } programs( @@ -236,14 +238,52 @@ fun TvGrid( }, ) { programIndex -> val program = programList[programIndex] - Text( - text = program.name ?: program.id.toString(), + val focused = programIndex == focusedProgramIndex + val background = + if (focused) { + MaterialTheme.colorScheme.inverseSurface + } else { + MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + } + val textColor = MaterialTheme.colorScheme.contentColorFor(background) + Box( modifier = - Modifier.ifElse( - programIndex == focusedProgram, - Modifier.background(MaterialTheme.colorScheme.error), - ), - ) + Modifier +// .scale(if (focused) 1.1f else 1f) + .padding(2.dp) + .fillMaxSize() + .background( + background, + shape = RoundedCornerShape(4.dp), + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = + Modifier + .fillMaxSize() + .padding(4.dp), + ) { + Text( + text = program.name ?: program.id.toString(), + color = textColor, + maxLines = 1, + modifier = Modifier.enableMarquee(focused), + ) + listOfNotNull( + program.seasonEpisode?.let { "S${it.season} E${it.episode}" }, + program.subtitle, + ).joinToString(" - ") + .ifBlank { null } + ?.let { + Text( + text = it, + color = textColor, + modifier = Modifier, + ) + } + } + } } channels( @@ -252,479 +292,35 @@ fun TvGrid( ProgramGuideItem.Channel(channelIndex) }, ) { channelIndex -> - Text( - text = channels[channelIndex].number ?: channelIndex.toString(), - modifier = Modifier.background(MaterialTheme.colorScheme.background), - ) - } - } -} - -@Composable -fun TvGridBox( - channels: List, - programs: Map>, - modifier: Modifier = Modifier, -) { - val startHour = 18f - var hour by remember { mutableFloatStateOf(startHour) } - Timber.v("hour=$hour") - val hoursToShow = 4f - val widthPerHour = 200.dp - - var focusedProgram by remember { mutableStateOf(null) } - - Column(modifier = modifier) { - // Header - Text( - text = hour.toString(), - ) - focusedProgram?.let { - Text( - text = it.name + " " + it.channelId, - ) - } - Row { - Spacer(Modifier.width(220.dp)) - var start = hour - while (start < (hour + hoursToShow)) { - Text( - text = start.toString(), - modifier = Modifier.width(widthPerHour), - ) - start += 1f - } - } - LazyColumn( - modifier = - Modifier, - ) { - itemsIndexed(channels) { channelIndex, channel -> - val programs = programs[channel.id].orEmpty() - var rowHasFocus by remember { mutableStateOf(false) } + val channel = channels[channelIndex] + Box( + modifier = + Modifier + .fillMaxSize() + .background( + MaterialTheme.colorScheme.background, + shape = RoundedCornerShape(4.dp), + ), + ) { Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier - .height(120.dp) - .fillMaxWidth() - .background( - if (rowHasFocus) { - MaterialTheme.colorScheme.surfaceColorAtElevation( - 3.dp, - ) - } else { - MaterialTheme.colorScheme.surface - }, - ), + .padding(4.dp) + .fillMaxSize(), ) { - val channelFocusRequester = remember { FocusRequester() } - // Channel info - ListItem( - selected = false, - onClick = { - Timber.v("Clicked channel $channel") - }, - leadingContent = { - Text( - text = channel.number ?: "", - modifier = Modifier.width(40.dp), - ) - }, - headlineContent = { - Text( - text = channel.name ?: "", - modifier = Modifier.width(80.dp), - ) - }, - trailingContent = { - AsyncImage( - model = channel.imageUrl, - contentDescription = null, - modifier = - Modifier - .width(64.dp) - .fillMaxHeight(), - ) - }, - modifier = - Modifier - .width(220.dp) - .fillMaxHeight() - .focusRequester(channelFocusRequester), + Text( + text = channel.number ?: channel.name ?: channelIndex.toString(), + modifier = Modifier, + ) + AsyncImage( + model = channel.imageUrl, + contentDescription = null, + modifier = Modifier.fillMaxHeight(.66f), ) - val focusRequester = remember { FocusRequester() } - var capturedFocus by remember { mutableStateOf(false) } - Row( - modifier = - Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .onKeyEvent { - Timber.v("onKeyEvent=$it") - if (it.type == KeyEventType.KeyUp) { - if (rowHasFocus) { - if (it.type == KeyEventType.KeyUp && it.key == Key.DirectionLeft && hour <= startHour) { - focusRequester.freeFocus() - channelFocusRequester.tryRequestFocus() - capturedFocus = false - return@onKeyEvent false - } - if (isUp(it) || isDown(it)) { - focusRequester.freeFocus() - capturedFocus = false - return@onKeyEvent false - } - val delta = - focusedProgram?.let { p -> - if (p.start.hours < hour) { - (p.end.hours - hour).coerceAtMost(.5f) - } else { - .5f - } - } ?: .5f - if (it.type == KeyEventType.KeyUp && it.key == Key.DirectionLeft && hour > startHour) { - hour = (hour - delta).coerceAtLeast(startHour) - return@onKeyEvent true - } else if (it.type == KeyEventType.KeyUp && it.key == Key.DirectionRight) { - hour += delta - return@onKeyEvent true - } - } - } - return@onKeyEvent false - }.onFocusChanged { - Timber.v("onFocusChanged=$it") - rowHasFocus = it.hasFocus - if (it.isFocused && !capturedFocus) { - focusRequester.captureFocus() - capturedFocus = true - } - }.focusProperties { - onEnter = { - Timber.v("onEnter") - focusRequester.captureFocus() - } - onExit = { - Timber.v("onExit") - capturedFocus = false - } - }.focusable(), - ) { - val filtered = - programs - .filter { - it.start.hours <= hour && it.end.hours > hour || - (it.start.hours > hour && it.start.hours < (hour + hoursToShow)) - }.sortedBy { it.start } - LaunchedEffect(filtered) { - Timber.v("Got ${filtered.size} for ${channel.number}") - } - filtered - .forEach { p -> - Timber.v("Showing program: $p") - val width = - (p.duration.inWholeMinutes) / 60f * widthPerHour * - if (p.start.hours < hour) { - (hour - p.start.hours).coerceIn(0f, 1f) - } else { - 1f - } - val backgroundColor = - if (rowHasFocus && p.start.hours <= hour && p.end.hours > (hour)) { - focusedProgram = p - MaterialTheme.colorScheme.error - } else { - Color.Unspecified - } - Box( - modifier = - Modifier - .width(width) - .fillMaxHeight() - .background(backgroundColor) - .border( - width = 2.dp, - color = MaterialTheme.colorScheme.inverseSurface, - ), - ) { - Text( - text = p.name ?: "Unknown", - color = MaterialTheme.colorScheme.onSurface, - ) - } - } - } } } } } } - -val LocalDateTime.hours get() = hour + minute / 60f - -val channel1Id = UUID.randomUUID() -val channel2Id = UUID.randomUUID() -val channel3Id = UUID.randomUUID() -val channel4Id = UUID.randomUUID() - -val channels = - listOf( - TvChannel( - id = channel1Id, - number = "2.1", - name = "WJTV", - imageUrl = "", - ), - TvChannel( - id = channel2Id, - number = "3.1", - name = "JYTV", - imageUrl = "", - ), - TvChannel( - id = channel3Id, - number = "4.1", - name = "AQTV", - imageUrl = "", - ), - TvChannel( - id = channel4Id, - number = "4.1", - name = "4444", - imageUrl = "", - ), - ) - -val programs = - mapOf( - channel1Id to - listOf( - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 19, 0, 0), - startHours = 18f, - endHours = 19f, - duration = 60.minutes, - name = "C1 Program #1", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 16, 19, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 19, 30, 0), - startHours = 19f, - endHours = 19.5f, - duration = 30.minutes, - name = "C1 Program #2", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 16, 19, 30, 0), - end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), - startHours = 19.5f, - endHours = 20f, - duration = 30.minutes, - name = "C1 Program #3", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 16, 20, 0, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 21, 0, 0), - startHours = 20f, - endHours = 21f, - duration = 60.minutes, - name = "C1 Program #3", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 22, 0, 0), - startHours = 21f, - endHours = 22f, - duration = 60.minutes, - name = "C1 Program #3", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 16, 22, 0, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 23, 0, 0), - startHours = 22f, - endHours = 23f, - duration = 60.minutes, - name = "C1 Program #3", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel1Id, - start = LocalDateTime.of(2025, 10, 17, 1, 0, 0, 0), - end = LocalDateTime.of(2025, 10, 17, 2, 0, 0), - startHours = 25f, - endHours = 26f, - duration = 60.minutes, - name = "C1 Program #4", - subtitle = null, - seasonEpisode = null, - ), - ), - channel2Id to - listOf( - TvProgram( - id = UUID.randomUUID(), - channelId = channel2Id, - start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 18, 30, 0), - startHours = 18f, - endHours = 18.5f, - duration = 30.minutes, - name = "C2 Program #1", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel2Id, - start = LocalDateTime.of(2025, 10, 16, 18, 30, 0), - end = LocalDateTime.of(2025, 10, 16, 19, 30, 0), - startHours = 18.5f, - endHours = 19f, - duration = 60.minutes, - name = "C2 Program #2", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel2Id, - start = LocalDateTime.of(2025, 10, 16, 19, 30, 0), - end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), - startHours = 19.5f, - endHours = 20f, - duration = 30.minutes, - name = "C2 Program #3", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel2Id, - start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 22, 0, 0), - startHours = 21f, - endHours = 22f, - duration = 60.minutes, - name = "C2 Program #4", - subtitle = null, - seasonEpisode = null, - ), - ), - channel3Id to - listOf( - TvProgram( - id = UUID.randomUUID(), - channelId = channel3Id, - start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 18, 15, 0), - duration = 15.minutes, - startHours = 18f, - endHours = 18.25f, - name = "C3 Program #1", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel3Id, - start = LocalDateTime.of(2025, 10, 16, 18, 15, 0), - end = LocalDateTime.of(2025, 10, 16, 18, 45, 0), - startHours = 18.25f, - endHours = 18.75f, - duration = 30.minutes, - name = "C3 Program #2", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel3Id, - start = LocalDateTime.of(2025, 10, 16, 18, 45, 0), - end = LocalDateTime.of(2025, 10, 16, 19, 0, 0), - startHours = 18.75f, - endHours = 19f, - duration = 15.minutes, - name = "C3 Program #3", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel3Id, - start = LocalDateTime.of(2025, 10, 16, 19, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), - startHours = 19f, - endHours = 20f, - duration = 60.minutes, - name = "C3 Program #3", - subtitle = null, - seasonEpisode = null, - ), - ), - channel4Id to - listOf( - TvProgram( - id = UUID.randomUUID(), - channelId = channel4Id, - start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 19, 0, 0), - startHours = 18f, - endHours = 19f, - duration = 60.minutes, - name = "C4 Program #1", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel4Id, - start = LocalDateTime.of(2025, 10, 16, 19, 0, 0), - end = LocalDateTime.of(2025, 10, 16, 19, 30, 0), - startHours = 19f, - endHours = 19.5f, - duration = 30.minutes, - name = "C4 Program #2", - subtitle = null, - seasonEpisode = null, - ), - TvProgram( - id = UUID.randomUUID(), - channelId = channel4Id, - start = LocalDateTime.of(2025, 10, 16, 19, 30, 0), - end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), - startHours = 19.5f, - endHours = 20f, - duration = 30.minutes, - name = "C4 Program #3", - subtitle = null, - seasonEpisode = null, - ), - ), - ) - -val programList = programs.values.flatten()