Styling guide

This commit is contained in:
Damontecres 2025-10-20 14:50:13 -04:00
parent 8af376cf63
commit d42f24806a
No known key found for this signature in database
2 changed files with 144 additions and 569 deletions

View file

@ -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,
)

View file

@ -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..<it.endHours) }
?: -1
if (pIndex >= 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..<it.endHours) }
if (pIndex >= 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<TvChannel>,
programs: Map<UUID, List<TvProgram>>,
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<TvProgram?>(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()