mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Styling guide
This commit is contained in:
parent
8af376cf63
commit
d42f24806a
2 changed files with 144 additions and 569 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,11 +220,14 @@ fun TvGrid(
|
|||
)
|
||||
},
|
||||
) { index ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
val time = start.plusHours(index.toLong()).toLocalTime()
|
||||
Text(
|
||||
text = time.toString(),
|
||||
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
programs(
|
||||
count = programList.size,
|
||||
|
|
@ -236,14 +238,52 @@ fun TvGrid(
|
|||
},
|
||||
) { programIndex ->
|
||||
val program = programList[programIndex]
|
||||
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
|
||||
// .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(),
|
||||
modifier =
|
||||
Modifier.ifElse(
|
||||
programIndex == focusedProgram,
|
||||
Modifier.background(MaterialTheme.colorScheme.error),
|
||||
),
|
||||
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) }
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.height(120.dp)
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
if (rowHasFocus) {
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||
3.dp,
|
||||
)
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
},
|
||||
),
|
||||
) {
|
||||
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),
|
||||
)
|
||||
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
|
||||
}
|
||||
val channel = channels[channelIndex]
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.width(width)
|
||||
.fillMaxHeight()
|
||||
.background(backgroundColor)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.inverseSurface,
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
MaterialTheme.colorScheme.background,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(4.dp)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
Text(
|
||||
text = p.name ?: "Unknown",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
text = channel.number ?: channel.name ?: channelIndex.toString(),
|
||||
modifier = Modifier,
|
||||
)
|
||||
AsyncImage(
|
||||
model = channel.imageUrl,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxHeight(.66f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue