Support selecting channel column to play

This commit is contained in:
Damontecres 2025-10-22 11:57:55 -04:00
parent 0002dece8c
commit be7bb65a7e
No known key found for this signature in database
2 changed files with 102 additions and 38 deletions

View file

@ -112,7 +112,7 @@ class LiveTvViewModel
channelId = dto.channelId!!, channelId = dto.channelId!!,
start = dto.startDate!!, start = dto.startDate!!,
end = dto.endDate!!, end = dto.endDate!!,
startHours = hoursBetween(start, dto.startDate!!), startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f),
endHours = hoursBetween(start, dto.endDate!!), endHours = hoursBetween(start, dto.endDate!!),
duration = dto.runTimeTicks!!.ticks, duration = dto.runTimeTicks!!.ticks,
name = dto.seriesName ?: dto.name, name = dto.seriesName ?: dto.name,

View file

@ -19,6 +19,7 @@ import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
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
@ -99,7 +100,15 @@ fun TvGuideGrid(
programList = programs, programList = programs,
programs = programsByChannel, programs = programsByChannel,
start = viewModel.start, start = viewModel.start,
onClick = { index, program -> onClickChannel = { index, channel ->
viewModel.navigationManager.navigateTo(
Destination.Playback(
itemId = channel.id,
positionMs = 0L,
),
)
},
onClickProgram = { index, program ->
if (program.isFake) { if (program.isFake) {
val now = LocalDateTime.now() val now = LocalDateTime.now()
if (now.isAfter(program.start) && now.isBefore(program.end)) { if (now.isAfter(program.start) && now.isBefore(program.end)) {
@ -170,13 +179,16 @@ fun TvGuideGrid(
} }
} }
const val CHANNEL_COLUMN = -1
@Composable @Composable
fun TvGuideGrid( fun TvGuideGrid(
channels: List<TvChannel>, channels: List<TvChannel>,
programList: List<TvProgram>, programList: List<TvProgram>,
programs: Map<UUID, List<TvProgram>>, programs: Map<UUID, List<TvProgram>>,
start: LocalDateTime, start: LocalDateTime,
onClick: (Int, TvProgram) -> Unit, onClickChannel: (Int, TvChannel) -> Unit,
onClickProgram: (Int, TvProgram) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -211,7 +223,8 @@ fun TvGuideGrid(
) )
} }
var gridHasFocus by remember { mutableStateOf(false) } var gridHasFocus by rememberSaveable { mutableStateOf(false) }
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
ProgramGuide( ProgramGuide(
state = state, state = state,
@ -239,8 +252,13 @@ fun TvGuideGrid(
} }
Key.DirectionRight -> { Key.DirectionRight -> {
if (channelColumnFocused) {
channelColumnFocused = false
focusedProgramIndex
} else {
val nextProgramIndex = focusedProgramIndex + 1 val nextProgramIndex = focusedProgramIndex + 1
val programsBefore = programsBeforeChannel.get(focusedChannelIndex) val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore val relativePosition = nextProgramIndex - programsBefore
val channelPrograms = val channelPrograms =
programs[channels[focusedChannelIndex].id].orEmpty() programs[channels[focusedChannelIndex].id].orEmpty()
@ -251,35 +269,45 @@ fun TvGuideGrid(
nextProgramIndex nextProgramIndex
} }
} }
}
Key.DirectionLeft -> { Key.DirectionLeft -> {
if (channelColumnFocused) {
focusManager.moveFocus(FocusDirection.Left)
null
} else {
val nextProgramIndex = focusedProgramIndex - 1 val nextProgramIndex = focusedProgramIndex - 1
if (nextProgramIndex >= 0) {
val programsBefore = val programsBefore =
programsBeforeChannel.get(focusedChannelIndex) programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore val relativePosition = nextProgramIndex - programsBefore
// val channelPrograms = // val channelPrograms =
// programs[channels[focusedChannel].id].orEmpty() // programs[channels[focusedChannel].id].orEmpty()
if (relativePosition < 0) { if (relativePosition >= 0) {
focusManager.moveFocus(FocusDirection.Left)
null
} else {
nextProgramIndex nextProgramIndex
} } else if (relativePosition == CHANNEL_COLUMN) {
channelColumnFocused = true
focusedProgramIndex
} else { } else {
Timber.w("Move left to relativePosition=$relativePosition should not occur")
focusManager.moveFocus(FocusDirection.Left) focusManager.moveFocus(FocusDirection.Left)
null null
} }
} }
}
Key.DirectionUp -> { Key.DirectionUp -> {
val start = programList[focusedProgramIndex].startHours
val newFocusedChannelIndex = (focusedChannelIndex - 1) val newFocusedChannelIndex = (focusedChannelIndex - 1)
// Timber.v("newFocusedChannelIndex=$newFocusedChannelIndex")
if (newFocusedChannelIndex < 0) { if (newFocusedChannelIndex < 0) {
focusManager.moveFocus(FocusDirection.Up) focusManager.moveFocus(FocusDirection.Up)
null null
} else if (channelColumnFocused) {
focusedChannelIndex = newFocusedChannelIndex
programsBeforeChannel.get(focusedChannelIndex)
} else { } else {
focusedChannelIndex = newFocusedChannelIndex.coerceAtLeast(0) val start = programList[focusedProgramIndex].startHours
focusedChannelIndex =
newFocusedChannelIndex.coerceAtLeast(0)
val channelId = channels[focusedChannelIndex].id val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty() val pro = programs[channelId].orEmpty()
val pIndex = val pIndex =
@ -298,14 +326,17 @@ fun TvGuideGrid(
} }
Key.DirectionDown -> { Key.DirectionDown -> {
// When the currently focused program starts
val start = programList[focusedProgramIndex].startHours
// Move channel focus down // Move channel focus down
val newFocusedChannelIndex = (focusedChannelIndex + 1) val newFocusedChannelIndex = (focusedChannelIndex + 1)
if (newFocusedChannelIndex >= channels.size) { if (newFocusedChannelIndex >= channels.size) {
// If trying to move below the final channel, then move focus out of the grid // If trying to move below the final channel, then move focus out of the grid
focusManager.moveFocus(FocusDirection.Down) focusManager.moveFocus(FocusDirection.Down)
null null
} else if (channelColumnFocused) {
// If focused on the channel column, move down a channel
focusedChannelIndex =
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
programsBeforeChannel.get(focusedChannelIndex)
} else { } else {
// Otherwise, moving to a new row // Otherwise, moving to a new row
focusedChannelIndex = focusedChannelIndex =
@ -313,6 +344,8 @@ fun TvGuideGrid(
// Get the new row/channel's programs // Get the new row/channel's programs
val channelId = channels[focusedChannelIndex].id val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty() val pro = programs[channelId].orEmpty()
// When the currently focused program starts
val start = programList[focusedProgramIndex].startHours
// Find the first program where the start time (of the previously focused program) is in the middle of a program // Find the first program where the start time (of the previously focused program) is in the middle of a program
val pIndex = val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) } pro.indexOfFirst { start in (it.startHours..<it.endHours) }
@ -336,9 +369,15 @@ fun TvGuideGrid(
} }
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> { Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
if (channelColumnFocused) {
val channel = channels[focusedChannelIndex]
Timber.v("Clicked on %s", channel)
onClickChannel.invoke(focusedChannelIndex, channel)
} else {
val program = programList[focusedProgramIndex] val program = programList[focusedProgramIndex]
Timber.v("Clicked on %s", program) Timber.v("Clicked on %s", program)
onClick.invoke(focusedProgramIndex, program) onClickProgram.invoke(focusedProgramIndex, program)
}
null null
} }
@ -347,6 +386,8 @@ fun TvGuideGrid(
} }
} }
if (newIndex != null) { if (newIndex != null) {
// Timber.v("newIndex=$newIndex")
if (newIndex >= 0) {
val before = programsBeforeChannel.get(focusedChannelIndex) val before = programsBeforeChannel.get(focusedChannelIndex)
val max = val max =
before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1 before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1
@ -356,6 +397,10 @@ fun TvGuideGrid(
state.animateToProgram(index, Alignment.Center) state.animateToProgram(index, Alignment.Center)
} }
return@onPreviewKeyEvent true return@onPreviewKeyEvent true
} else {
Timber.w("newIndex is <0: $newIndex")
return@onPreviewKeyEvent true
}
} }
return@onPreviewKeyEvent false return@onPreviewKeyEvent false
}, },
@ -420,7 +465,8 @@ fun TvGuideGrid(
}, },
) { programIndex -> ) { programIndex ->
val program = programList[programIndex] val program = programList[programIndex]
val focused = gridHasFocus && programIndex == focusedProgramIndex val focused =
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
val background = val background =
if (focused) { if (focused) {
MaterialTheme.colorScheme.inverseSurface MaterialTheme.colorScheme.inverseSurface
@ -480,12 +526,21 @@ fun TvGuideGrid(
}, },
) { channelIndex -> ) { channelIndex ->
val channel = channels[channelIndex] val channel = channels[channelIndex]
val focused =
gridHasFocus && channelColumnFocused && focusedChannelIndex == channelIndex
val background =
if (focused) {
MaterialTheme.colorScheme.inverseSurface
} else {
MaterialTheme.colorScheme.surface
}
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
Box( Box(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.background( .background(
MaterialTheme.colorScheme.surface, background,
shape = RoundedCornerShape(4.dp), shape = RoundedCornerShape(4.dp),
), ),
) { ) {
@ -499,6 +554,7 @@ fun TvGuideGrid(
) { ) {
Text( Text(
text = channel.number ?: channel.name ?: channelIndex.toString(), text = channel.number ?: channel.name ?: channelIndex.toString(),
color = textColor,
modifier = Modifier, modifier = Modifier,
) )
AsyncImage( AsyncImage(
@ -509,5 +565,13 @@ fun TvGuideGrid(
} }
} }
} }
topCorner {
Box(
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
)
}
} }
} }