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 org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -85,15 +84,6 @@ class LiveTvViewModel
.execute(api, request) .execute(api, request)
.content.items .content.items
.map { dto -> .map { dto ->
val utcStart = dto.startDate!!.toEpochSecond(ZoneOffset.UTC)
utcStart
val ttt =
java.time.Duration
.between(
start,
dto.startDate!!,
)
ttt
TvProgram( TvProgram(
id = dto.id, id = dto.id,
channelId = dto.channelId!!, channelId = dto.channelId!!,
@ -103,30 +93,19 @@ class LiveTvViewModel
java.time.Duration java.time.Duration
.between(start, dto.startDate!!) .between(start, dto.startDate!!)
.seconds / (60f * 60f), .seconds / (60f * 60f),
// dto.startDate!!.hours +
// (
// java.time.Duration
// .between(
// start,
// dto.startDate!!,
// ).toDaysPart() / 24
// ).coerceAtLeast(0),
endHours = endHours =
java.time.Duration java.time.Duration
.between(start, dto.endDate!!) .between(start, dto.endDate!!)
.seconds / (60f * 60f), .seconds / (60f * 60f),
// dto.endDate!!.hours +
// (
// java.time.Duration
// .between(
// start,
// dto.endDate!!,
// ).toHours() / 24
// ).coerceAtLeast(0),
duration = dto.runTimeTicks!!.ticks, duration = dto.runTimeTicks!!.ticks,
name = dto.seriesName ?: dto.name, name = dto.seriesName ?: dto.name,
subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() }, subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
seasonEpisode = null, // TODO 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 }.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
@ -146,7 +125,7 @@ class LiveTvViewModel
startHours = it.toFloat(), startHours = it.toFloat(),
endHours = (it + 1).toFloat(), endHours = (it + 1).toFloat(),
duration = 60.seconds, duration = 60.seconds,
name = "No date", name = "No data",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
) )

View file

@ -1,59 +1,41 @@
package com.github.damontecres.wholphin.ui.detail.livetv package com.github.damontecres.wholphin.ui.detail.livetv
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState 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.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection 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.Key
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key 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.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.contentColorFor
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.playback.isDown
import com.github.damontecres.wholphin.ui.playback.isUp
import com.github.damontecres.wholphin.ui.rememberInt 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.ExceptionHandler
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import eu.wewox.programguide.ProgramGuide import eu.wewox.programguide.ProgramGuide
@ -64,16 +46,6 @@ import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID 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 @Composable
fun TvGrid( fun TvGrid(
@ -96,19 +68,15 @@ fun TvGrid(
LoadingState.Success -> LoadingState.Success ->
Column(modifier = modifier) { Column(modifier = modifier) {
Button(
onClick = {},
) {
Text(
text = "Button",
)
}
TvGrid( TvGrid(
channels = channels, channels = channels,
programList = programs, programList = programs,
programs = programsByChannel, programs = programsByChannel,
start = viewModel.start, start = viewModel.start,
modifier = Modifier.fillMaxSize(), modifier =
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
) )
} }
} }
@ -126,15 +94,15 @@ fun TvGrid(
val state = rememberSaveableProgramGuideState() val state = rememberSaveableProgramGuideState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
// val timeline = 0..<24 // val timeline = 0..<24
var focusedProgram by rememberInt(0) var focusedProgramIndex by rememberInt(0)
var focusedChannel by rememberInt(0) var focusedChannelIndex by rememberInt(0)
val dimensions = val dimensions =
ProgramGuideDimensions( ProgramGuideDimensions(
timelineHourWidth = 240.dp, timelineHourWidth = 240.dp,
timelineHeight = 32.dp, timelineHeight = 32.dp,
channelWidth = 64.dp, channelWidth = 120.dp,
channelHeight = 64.dp, channelHeight = 80.dp,
currentTimeWidth = 2.dp, currentTimeWidth = 2.dp,
) )
@ -152,44 +120,75 @@ fun TvGrid(
} }
val newIndex = val newIndex =
when (it.key) { when (it.key) {
Key.DirectionRight -> focusedProgram + 1 Key.DirectionRight -> {
Key.DirectionLeft -> focusedProgram - 1 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 -> { Key.DirectionUp -> {
val start = programList[focusedProgram].startHours val start = programList[focusedProgramIndex].startHours
focusedChannel = (focusedChannel - 1) focusedChannelIndex = (focusedChannelIndex - 1)
if (focusedChannel < 0) { if (focusedChannelIndex < 0) {
focusManager.moveFocus(FocusDirection.Up) focusManager.moveFocus(FocusDirection.Up)
null null
} else { } else {
val channelId = channels[focusedChannel].id val channelId = channels[focusedChannelIndex].id
val pIndex = val pIndex =
programs[channelId]?.indexOfFirst { start in (it.startHours..<it.endHours) } programs[channelId]?.indexOfFirst { start in (it.startHours..<it.endHours) }
?: -1 ?: -1
if (pIndex >= 0) { if (pIndex >= 0) {
programsBeforeChannel(focusedChannel) + pIndex programsBeforeChannel(focusedChannelIndex) + pIndex
} else { } else {
programsBeforeChannel(focusedChannel) + programs[channelId]!!.size programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size
} }
} }
} }
Key.DirectionDown -> { Key.DirectionDown -> {
val start = programList[focusedProgram].startHours val start = programList[focusedProgramIndex].startHours
focusedChannel = focusedChannelIndex =
(focusedChannel + 1).coerceAtMost(channels.size - 1) (focusedChannelIndex + 1).coerceAtMost(channels.size - 1)
val channelId = channels[focusedChannel].id val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty() val pro = programs[channelId].orEmpty()
val pIndex = val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) } pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) { if (pIndex >= 0) {
programsBeforeChannel(focusedChannel) + pIndex programsBeforeChannel(focusedChannelIndex) + pIndex
} else { } else {
programsBeforeChannel(focusedChannel) + programs[channelId]!!.size programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size
} }
} }
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> { Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
Timber.v("Clicked on ${programList[focusedProgram]}") Timber.v("Clicked on ${programList[focusedProgramIndex]}")
null null
} }
@ -198,13 +197,13 @@ fun TvGrid(
} }
} }
if (newIndex != null) { if (newIndex != null) {
val before = programsBeforeChannel(focusedChannel) val before = programsBeforeChannel(focusedChannelIndex)
val max = 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) val index = newIndex.coerceIn(before, max)
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
state.animateToProgram(index, Alignment.Center) state.animateToProgram(index, Alignment.Center)
focusedProgram = index focusedProgramIndex = index
} }
return@onPreviewKeyEvent true return@onPreviewKeyEvent true
} }
@ -221,10 +220,13 @@ fun TvGrid(
) )
}, },
) { index -> ) { index ->
val time = start.plusHours(index.toLong()).toLocalTime() Box(modifier = Modifier.fillMaxSize()) {
Text( val time = start.plusHours(index.toLong()).toLocalTime()
text = time.toString(), Text(
) text = time.toString(),
modifier = Modifier.background(MaterialTheme.colorScheme.background),
)
}
} }
programs( programs(
@ -236,14 +238,52 @@ fun TvGrid(
}, },
) { programIndex -> ) { programIndex ->
val program = programList[programIndex] val program = programList[programIndex]
Text( val focused = programIndex == focusedProgramIndex
text = program.name ?: program.id.toString(), val background =
if (focused) {
MaterialTheme.colorScheme.inverseSurface
} else {
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
}
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
Box(
modifier = modifier =
Modifier.ifElse( Modifier
programIndex == focusedProgram, // .scale(if (focused) 1.1f else 1f)
Modifier.background(MaterialTheme.colorScheme.error), .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( channels(
@ -252,479 +292,35 @@ fun TvGrid(
ProgramGuideItem.Channel(channelIndex) ProgramGuideItem.Channel(channelIndex)
}, },
) { channelIndex -> ) { channelIndex ->
Text( val channel = channels[channelIndex]
text = channels[channelIndex].number ?: channelIndex.toString(), Box(
modifier = Modifier.background(MaterialTheme.colorScheme.background), modifier =
) Modifier
} .fillMaxSize()
} .background(
} MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(4.dp),
@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) }
Row( Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier =
Modifier Modifier
.height(120.dp) .padding(4.dp)
.fillMaxWidth() .fillMaxSize(),
.background(
if (rowHasFocus) {
MaterialTheme.colorScheme.surfaceColorAtElevation(
3.dp,
)
} else {
MaterialTheme.colorScheme.surface
},
),
) { ) {
val channelFocusRequester = remember { FocusRequester() } Text(
// Channel info text = channel.number ?: channel.name ?: channelIndex.toString(),
ListItem( modifier = Modifier,
selected = false, )
onClick = { AsyncImage(
Timber.v("Clicked channel $channel") model = channel.imageUrl,
}, contentDescription = null,
leadingContent = { modifier = Modifier.fillMaxHeight(.66f),
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),
) )
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()