mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
WIP
This commit is contained in:
parent
548f942609
commit
5f6eb7d313
3 changed files with 327 additions and 443 deletions
|
|
@ -228,7 +228,6 @@ dependencies {
|
|||
implementation(libs.timber)
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
implementation(libs.programguide)
|
||||
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
|
||||
|
|
|
|||
|
|
@ -1,490 +1,377 @@
|
|||
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.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.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
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.type
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.times
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.tv.material3.ListItem
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.tv.material3.surfaceColorAtElevation
|
||||
import coil3.compose.AsyncImage
|
||||
import com.github.damontecres.wholphin.ui.OneTimeLaunchedEffect
|
||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import eu.wewox.programguide.ProgramGuide
|
||||
import eu.wewox.programguide.ProgramGuideItem
|
||||
import com.github.damontecres.wholphin.ui.playback.isDown
|
||||
import com.github.damontecres.wholphin.ui.playback.isUp
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
private val hourWidth = 200.dp
|
||||
|
||||
data class Program(
|
||||
val channel: Int,
|
||||
val start: Float,
|
||||
val end: Float,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
/**
|
||||
* Formats time.
|
||||
*/
|
||||
fun formatTime(
|
||||
from: Float,
|
||||
to: Float? = null,
|
||||
): String {
|
||||
fun Float.hour(): String = toInt().formatWithPrefix(2)
|
||||
|
||||
fun Float.minutes(): String = ((this % 1) * 60).toInt().formatWithPrefix(2)
|
||||
|
||||
val fromFormatted = "${from.hour()}:${from.minutes()}"
|
||||
return if (to != null) {
|
||||
val toFormatted = "${to.hour()}:${to.minutes()}"
|
||||
"$fromFormatted - $toFormatted"
|
||||
} else {
|
||||
fromFormatted
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of programs to view in program guide.
|
||||
*/
|
||||
fun createPrograms(
|
||||
channels: Int = CHANNELS_COUNT,
|
||||
timeline: IntRange = 0..HOURS_COUNT,
|
||||
): List<Program> {
|
||||
var channel = 0
|
||||
var hour = timeline.first + HOURS.random()
|
||||
return buildList {
|
||||
while (channel < channels) {
|
||||
while (true) {
|
||||
val end = hour + HOURS.random()
|
||||
if (end > timeline.last) {
|
||||
break
|
||||
}
|
||||
|
||||
add(Program(channel, hour, end, "Program #$size"))
|
||||
hour = end
|
||||
}
|
||||
hour = timeline.first + HOURS.random()
|
||||
channel += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.formatWithPrefix(
|
||||
length: Int,
|
||||
prefix: Char = '0',
|
||||
): String {
|
||||
val number = toString()
|
||||
val prefixLength = (length - number.length).coerceAtLeast(0)
|
||||
val prefixFull = List(prefixLength) { prefix }.joinToString(separator = "")
|
||||
return "$prefixFull$number"
|
||||
}
|
||||
|
||||
private val HOURS = listOf(0.5f, 1f, 1.25f, 1.5f, 2f, 2.25f, 2.5f)
|
||||
|
||||
private const val CHANNELS_COUNT = 30
|
||||
private const val HOURS_COUNT = 24
|
||||
@Composable
|
||||
fun TvGrid(modifier: Modifier = Modifier) = TvGridBox(channels, programs, modifier)
|
||||
|
||||
@Composable
|
||||
fun TvGrid(modifier: Modifier = Modifier) {
|
||||
val channels = 20
|
||||
val timeline = 8..22
|
||||
val programs = remember { createPrograms(channels, timeline) }
|
||||
ProgramGuide(
|
||||
modifier =
|
||||
modifier,
|
||||
) {
|
||||
guideStartHour = timeline.first.toFloat()
|
||||
|
||||
programs(
|
||||
items = programs,
|
||||
layoutInfo = {
|
||||
ProgramGuideItem.Program(
|
||||
channelIndex = it.channel,
|
||||
startHour = it.start,
|
||||
endHour = it.end,
|
||||
)
|
||||
},
|
||||
itemContent = {
|
||||
ListItem(
|
||||
selected = false,
|
||||
onClick = {},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = it.title,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
channels(
|
||||
count = channels,
|
||||
layoutInfo = {
|
||||
ProgramGuideItem.Channel(
|
||||
index = it,
|
||||
)
|
||||
},
|
||||
itemContent = {
|
||||
Text(
|
||||
text = it.toString(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
timeline(
|
||||
count = timeline.count(),
|
||||
layoutInfo = {
|
||||
val start = timeline.toList()[it].toFloat()
|
||||
ProgramGuideItem.Timeline(
|
||||
startHour = start,
|
||||
endHour = start + 1f,
|
||||
)
|
||||
},
|
||||
itemContent = {
|
||||
Text(
|
||||
text = "${timeline.toList()[it].toFloat()} o'clock",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvGrid3(
|
||||
fun TvGridBox(
|
||||
channels: List<TvChannel>,
|
||||
programs: Map<UUID, List<TvProgram>>,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: LiveTvViewModel = hiltViewModel(),
|
||||
) {
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init()
|
||||
}
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||
val startHour = 18f
|
||||
var hour by remember { mutableFloatStateOf(startHour) }
|
||||
Timber.v("hour=$hour")
|
||||
val hoursToShow = 3f
|
||||
val widthPerHour = 200.dp
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
|
||||
LoadingState.Success -> {
|
||||
val channels by viewModel.channels.observeAsState(listOf())
|
||||
val programs by viewModel.programs.observeAsState(listOf())
|
||||
ProgramGuide(
|
||||
modifier = modifier,
|
||||
) {
|
||||
guideStartHour = 22f
|
||||
programs(
|
||||
items = programs,
|
||||
layoutInfo = { p ->
|
||||
if (p != null) {
|
||||
Timber.v("${p.id}: ${p.start.hour + p.start.minute / 60f}")
|
||||
ProgramGuideItem.Program(
|
||||
channelIndex = channels.indexOfFirst { it.id == p.channelId },
|
||||
startHour = p.start.hour + p.start.minute / 60f,
|
||||
endHour = p.start.hour + p.start.minute / 60f,
|
||||
Column(modifier = modifier) {
|
||||
// Header
|
||||
Text(
|
||||
text = hour.toString(),
|
||||
)
|
||||
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),
|
||||
)
|
||||
} else {
|
||||
ProgramGuideItem.Program(0, 0f, 0f)
|
||||
},
|
||||
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
|
||||
}
|
||||
if (it.type == KeyEventType.KeyUp && it.key == Key.DirectionLeft && hour > startHour) {
|
||||
hour -= .5f
|
||||
return@onKeyEvent true
|
||||
} else if (it.type == KeyEventType.KeyUp && it.key == Key.DirectionRight) {
|
||||
hour += .5f
|
||||
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}")
|
||||
}
|
||||
},
|
||||
itemContent = { program ->
|
||||
// Timber.v("Render $program")
|
||||
Text(
|
||||
text = program?.name ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.focusable(),
|
||||
)
|
||||
// ListItem(
|
||||
// selected = false,
|
||||
// onClick = {},
|
||||
// headlineContent = {
|
||||
// Text(
|
||||
// text = program?.name ?: "",
|
||||
// color = MaterialTheme.colorScheme.onSurface,
|
||||
// modifier = Modifier,
|
||||
// )
|
||||
// },
|
||||
// supportingContent = {
|
||||
// program?.subtitle?.let {
|
||||
// Text(
|
||||
// text = program.subtitle,
|
||||
// color = MaterialTheme.colorScheme.onSurface,
|
||||
// )
|
||||
// }
|
||||
// },
|
||||
// modifier = Modifier,
|
||||
// )
|
||||
},
|
||||
)
|
||||
channels(
|
||||
count = channels.size,
|
||||
layoutInfo = {
|
||||
ProgramGuideItem.Channel(
|
||||
index = it,
|
||||
)
|
||||
},
|
||||
itemContent = {
|
||||
val channel = channels[it]
|
||||
Text(
|
||||
text = channel.number ?: "",
|
||||
)
|
||||
},
|
||||
)
|
||||
timeline(
|
||||
count = 12,
|
||||
layoutInfo = {
|
||||
val start = 22f + it
|
||||
ProgramGuideItem.Timeline(
|
||||
startHour = start,
|
||||
endHour = start + 1f,
|
||||
)
|
||||
},
|
||||
itemContent = {
|
||||
Text(
|
||||
text = "${22f + it} o'clock",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TvGrid2(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: LiveTvViewModel = hiltViewModel(),
|
||||
) {
|
||||
OneTimeLaunchedEffect {
|
||||
viewModel.init()
|
||||
}
|
||||
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||
|
||||
when (val state = loading) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
LoadingState.Loading,
|
||||
LoadingState.Pending,
|
||||
-> LoadingPage()
|
||||
|
||||
LoadingState.Success -> {
|
||||
val channels by viewModel.channels.observeAsState(listOf())
|
||||
var focusedRowIndex by rememberInt()
|
||||
|
||||
val columnState = rememberLazyListState()
|
||||
val rowStates =
|
||||
rememberSaveable(
|
||||
saver =
|
||||
listSaver(
|
||||
save = { it.map { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) } },
|
||||
restore = {
|
||||
it.map {
|
||||
LazyListState(
|
||||
firstVisibleItemIndex = it[0],
|
||||
firstVisibleItemScrollOffset = it[1],
|
||||
filtered
|
||||
.forEach { p ->
|
||||
Timber.v("Showing program: $p")
|
||||
val width = p.duration.inWholeMinutes / 60f * widthPerHour
|
||||
val backgroundColor =
|
||||
if (rowHasFocus && p.start.hours <= hour && p.end.hours > (hour)) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
) { List(channels.size) { LazyListState(0, 0) } }
|
||||
|
||||
val currentRowState = rowStates[focusedRowIndex]
|
||||
LaunchedEffect(currentRowState.firstVisibleItemScrollOffset) {
|
||||
rowStates.forEachIndexed { index, state ->
|
||||
if (index != focusedRowIndex) {
|
||||
currentRowState.layoutInfo.visibleItemsInfo
|
||||
state.scrollToItem(currentRowState.firstVisibleItemIndex, currentRowState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = columnState,
|
||||
modifier = modifier,
|
||||
) {
|
||||
itemsIndexed(channels) { channelIndex, channel ->
|
||||
val programs by viewModel.getPrograms(channel.id).observeAsState(listOf())
|
||||
ChannelRow(
|
||||
lazyListState = rowStates[channelIndex],
|
||||
channel = channel,
|
||||
programs = programs,
|
||||
modifier =
|
||||
Modifier
|
||||
.height(56.dp)
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) {
|
||||
focusedRowIndex = channelIndex
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelRow(
|
||||
lazyListState: LazyListState,
|
||||
channel: TvChannel,
|
||||
programs: List<TvProgram?>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
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(),
|
||||
)
|
||||
LazyRow(
|
||||
state = lazyListState,
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
itemsIndexed(programs) { programIndex, program ->
|
||||
Timber.v("program=$program")
|
||||
val duration = (program?.duration ?: 30.minutes).coerceAtLeast(2.minutes)
|
||||
val width = duration.inWholeMinutes / 60f * hourWidth
|
||||
ListItem(
|
||||
selected = false,
|
||||
onClick = {},
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = program?.name ?: "",
|
||||
modifier = Modifier,
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
program?.subtitle?.let {
|
||||
Text(
|
||||
text = program.subtitle,
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.width(width).fillMaxHeight(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val LocalDateTime.hours get() = hour + minute / 60f
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
private fun ChannelRowPreview() {
|
||||
val channelId = UUID.randomUUID()
|
||||
val channel =
|
||||
val channel1Id = UUID.randomUUID()
|
||||
val channel2Id = UUID.randomUUID()
|
||||
val channel3Id = UUID.randomUUID()
|
||||
|
||||
val channels =
|
||||
listOf(
|
||||
TvChannel(
|
||||
id = channelId,
|
||||
id = channel1Id,
|
||||
number = "2.1",
|
||||
name = "WJTV",
|
||||
imageUrl = "",
|
||||
)
|
||||
val programs =
|
||||
listOf(
|
||||
TvProgram(
|
||||
id = UUID.randomUUID(),
|
||||
channelId = channelId,
|
||||
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
||||
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||
duration = 60.minutes,
|
||||
name = "Program #1",
|
||||
subtitle = null,
|
||||
seasonEpisode = null,
|
||||
),
|
||||
TvProgram(
|
||||
id = UUID.randomUUID(),
|
||||
channelId = channelId,
|
||||
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||
duration = 30.minutes,
|
||||
name = "Program #1",
|
||||
subtitle = null,
|
||||
seasonEpisode = null,
|
||||
),
|
||||
TvProgram(
|
||||
id = UUID.randomUUID(),
|
||||
channelId = channelId,
|
||||
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
||||
duration = 30.minutes,
|
||||
name = "Program #1",
|
||||
subtitle = null,
|
||||
seasonEpisode = null,
|
||||
),
|
||||
)
|
||||
WholphinTheme {
|
||||
ChannelRow(
|
||||
lazyListState = rememberLazyListState(),
|
||||
channel = channel,
|
||||
programs = programs,
|
||||
modifier = Modifier.height(140.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
TvChannel(
|
||||
id = channel2Id,
|
||||
number = "3.1",
|
||||
name = "JYTV",
|
||||
imageUrl = "",
|
||||
),
|
||||
TvChannel(
|
||||
id = channel3Id,
|
||||
number = "4.1",
|
||||
name = "AQTV",
|
||||
imageUrl = "",
|
||||
),
|
||||
)
|
||||
|
||||
@PreviewTvSpec
|
||||
@Composable
|
||||
private fun TvGridPreview() {
|
||||
}
|
||||
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),
|
||||
duration = 60.minutes,
|
||||
name = "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),
|
||||
duration = 30.minutes,
|
||||
name = "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),
|
||||
duration = 30.minutes,
|
||||
name = "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),
|
||||
duration = 60.minutes,
|
||||
name = "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),
|
||||
duration = 60.minutes,
|
||||
name = "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),
|
||||
duration = 60.minutes,
|
||||
name = "Program #3",
|
||||
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),
|
||||
duration = 30.minutes,
|
||||
name = "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),
|
||||
duration = 60.minutes,
|
||||
name = "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),
|
||||
duration = 30.minutes,
|
||||
name = "Program #3",
|
||||
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, 19, 0, 0),
|
||||
duration = 60.minutes,
|
||||
name = "Program #1",
|
||||
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, 19, 30, 0),
|
||||
duration = 30.minutes,
|
||||
name = "Program #2",
|
||||
subtitle = null,
|
||||
seasonEpisode = null,
|
||||
),
|
||||
TvProgram(
|
||||
id = UUID.randomUUID(),
|
||||
channelId = channel3Id,
|
||||
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
||||
duration = 30.minutes,
|
||||
name = "Program #3",
|
||||
subtitle = null,
|
||||
seasonEpisode = null,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ coreKtx = "1.17.0"
|
|||
appcompat = "1.7.1"
|
||||
composeBom = "2025.10.00"
|
||||
compose-runtime = "1.9.3"
|
||||
programguide = "1.6.0"
|
||||
timber = "5.0.1"
|
||||
tvFoundation = "1.0.0-alpha12"
|
||||
tvMaterial = "1.0.1"
|
||||
|
|
@ -57,7 +56,6 @@ androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "d
|
|||
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
|
||||
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
|
||||
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
|
||||
programguide = { module = "io.github.oleksandrbalan:programguide", version.ref = "programguide" }
|
||||
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-javalite" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
|
||||
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue