Reimplement guide movement & program fetching

This commit is contained in:
Damontecres 2025-11-01 15:22:31 -04:00
parent 841b4a0907
commit 1ecbc37a07
No known key found for this signature in database
2 changed files with 278 additions and 258 deletions

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.data.RowColumn
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
@ -65,8 +66,9 @@ class LiveTvViewModel
val offset = MutableLiveData(0) val offset = MutableLiveData(0)
val programOffset = MutableLiveData(0) val programOffset = MutableLiveData(0)
private val range = 10 private val range = 8
private var currentIndex = 0 private var currentIndex = 0
val fetchedRange = MutableLiveData<IntRange>(0..<range)
fun init(firstLoad: Boolean) { fun init(firstLoad: Boolean) {
start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
@ -92,7 +94,7 @@ class LiveTvViewModel
Timber.d("Got ${channels.size} channels") Timber.d("Got ${channels.size} channels")
channelsIdToIndex = channelsIdToIndex =
channels.withIndex().associateBy({ it.value.id }, { it.index }) channels.withIndex().associateBy({ it.value.id }, { it.index })
fetchPrograms(channels.subList(0, range.coerceAtMost(channels.size))) fetchPrograms(channels, 0..<range.coerceAtMost(channels.size))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@LiveTvViewModel.channels.value = channels this@LiveTvViewModel.channels.value = channels
@ -103,107 +105,110 @@ class LiveTvViewModel
private suspend fun fetchProgramsWithLoading(channels: List<TvChannel>) { private suspend fun fetchProgramsWithLoading(channels: List<TvChannel>) {
loading.setValueOnMain(LoadingState.Loading) loading.setValueOnMain(LoadingState.Loading)
fetchPrograms(channels) fetchPrograms(channels, fetchedRange.value!!)
loading.setValueOnMain(LoadingState.Success) loading.setValueOnMain(LoadingState.Success)
} }
private suspend fun fetchPrograms(channels: List<TvChannel>) = private suspend fun fetchPrograms(
mutex.withLock { channels: List<TvChannel>,
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1) range: IntRange,
val minEndDate = start.plusMinutes(1L) ) = mutex.withLock {
Timber.v("Fetching programs for ${channels.size} channels") val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
val request = val minEndDate = start.plusMinutes(1L)
GetProgramsDto( Timber.v("Fetching programs for ${channels.size} channels")
maxStartDate = maxStartDate, val request =
minEndDate = minEndDate, GetProgramsDto(
channelIds = channels.map { it.id }, maxStartDate = maxStartDate,
sortBy = listOf(ItemSortBy.START_DATE), minEndDate = minEndDate,
) channelIds = channels.map { it.id },
val programs = sortBy = listOf(ItemSortBy.START_DATE),
api.liveTvApi )
.getPrograms(request) val programs =
.content.items api.liveTvApi
.map { dto -> .getPrograms(request)
val category = .content.items
if (dto.isKids ?: false) { .map { dto ->
ProgramCategory.KIDS val category =
} else if (dto.isMovie ?: false) { if (dto.isKids ?: false) {
ProgramCategory.MOVIE ProgramCategory.KIDS
} else if (dto.isNews ?: false) { } else if (dto.isMovie ?: false) {
ProgramCategory.NEWS ProgramCategory.MOVIE
} else if (dto.isSports ?: false) { } else if (dto.isNews ?: false) {
ProgramCategory.SPORTS ProgramCategory.NEWS
} else if (dto.isSports ?: false) {
ProgramCategory.SPORTS
} else {
null
}
TvProgram(
id = dto.id,
channelId = dto.channelId!!,
start = dto.startDate!!,
end = dto.endDate!!,
startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f),
endHours = hoursBetween(start, dto.endDate!!),
duration = dto.runTimeTicks!!.ticks,
name = dto.seriesName ?: dto.name,
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
seasonEpisode =
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!)
} else { } else {
null null
} },
TvProgram( isRecording = dto.timerId.isNotNullOrBlank(),
id = dto.id, isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
channelId = dto.channelId!!, isRepeat = dto.isRepeat ?: false,
start = dto.startDate!!, category = category,
end = dto.endDate!!, )
startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f), }
endHours = hoursBetween(start, dto.endDate!!),
duration = dto.runTimeTicks!!.ticks, val programsByChannel = programs.groupBy { it.channelId }
name = dto.seriesName ?: dto.name, val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false }, val fake = mutableListOf<TvProgram>()
seasonEpisode = val finalProgramsByChannel =
if (dto.indexNumber != null && dto.parentIndexNumber != null) { programsByChannel.toMutableMap().apply {
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!) emptyChannels.forEach { channel ->
} else { val fakePrograms =
null (0..<MAX_HOURS).map {
}, TvProgram(
isRecording = dto.timerId.isNotNullOrBlank(), id = UUID.randomUUID(), // TODO
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(), channelId = channel.id,
isRepeat = dto.isRepeat ?: false, start = start.plusHours(it),
category = category, end = start.plusHours(it + 1),
) startHours = it.toFloat(),
} endHours = (it + 1).toFloat(),
duration = 60.seconds,
val programsByChannel = programs.groupBy { it.channelId } name = "No data",
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() } subtitle = null,
val fake = mutableListOf<TvProgram>() seasonEpisode = null,
val finalProgramsByChannel = isRecording = false,
programsByChannel.toMutableMap().apply { isSeriesRecording = false,
emptyChannels.forEach { channel -> isRepeat = false,
val fakePrograms = category = ProgramCategory.FAKE,
(0..<MAX_HOURS).map { )
TvProgram( }
id = UUID.randomUUID(), // TODO put(channel.id, fakePrograms)
channelId = channel.id, fake.addAll(fakePrograms)
start = start.plusHours(it),
end = start.plusHours(it + 1),
startHours = it.toFloat(),
endHours = (it + 1).toFloat(),
duration = 60.seconds,
name = "No data",
subtitle = null,
seasonEpisode = null,
isRecording = false,
isSeriesRecording = false,
isRepeat = false,
category = ProgramCategory.FAKE,
)
}
put(channel.id, fakePrograms)
fake.addAll(fakePrograms)
}
} }
finalProgramsByChannel.forEach { (channelId, programs) ->
channelProgramCount[channelId] = programs.size
}
val finalProgramList =
(programs + fake).sortedWith(
compareBy(
{ channelsIdToIndex[it.channelId]!! },
{ it.start },
),
)
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = finalProgramList
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
} }
finalProgramsByChannel.forEach { (channelId, programs) ->
channelProgramCount[channelId] = programs.size
} }
val finalProgramList =
(programs + fake).sortedWith(
compareBy(
{ channelsIdToIndex[it.channelId]!! },
{ it.start },
),
)
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = finalProgramList
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
this@LiveTvViewModel.fetchedRange.value = range
}
}
fun getItem(programId: UUID) { fun getItem(programId: UUID) {
fetchingItem.value = LoadingState.Loading fetchingItem.value = LoadingState.Loading
@ -310,28 +315,32 @@ class LiveTvViewModel
loading.setValueOnMain(LoadingState.Success) loading.setValueOnMain(LoadingState.Success)
} }
fun onFocusChannel(index: Int): Job? { fun onFocusChannel(position: RowColumn): Job? {
return channels.value?.let { channels -> return channels.value?.let { channels ->
val offset = offset.value!! val fetchedRange = fetchedRange.value!!
val absoluteIndex = offset + index val quarter = (fetchedRange.last - fetchedRange.start) / 4
val rangeStart = (currentIndex - range / 2).coerceAtLeast(0) val rangeStart = fetchedRange.start + quarter
val rangeEnd = (currentIndex + range / 2).coerceAtMost(channels.size) val rangeEnd = fetchedRange.last - quarter
val testRange = rangeStart..<rangeEnd
Timber.v( Timber.v(
"onFocusChannel: index=$index, currentIndex=$currentIndex, offset=$offset, rangeStart=$rangeStart, rangeEnd=$rangeEnd", "onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange",
) )
if (absoluteIndex !in (rangeStart..<rangeEnd)) { val fetchStart = (position.row - range).coerceAtLeast(0)
val fetchStart = (absoluteIndex - range).coerceAtLeast(0) val fetchEnd = (position.row + range).coerceAtMost(channels.size)
val fetchEnd = (absoluteIndex + range).coerceAtMost(channels.size) val newFetchRange = fetchStart..<fetchEnd
Timber.v("Loading more programs for channels $fetchStart=>$fetchEnd") // If current channel is not within +/- range
// And the potential new fetch range is not wholly within the current (eg not near the top or bottom)
// Fetch new data
if (position.row !in testRange && !newFetchRange.within(fetchedRange)) {
Timber.v("Loading more programs for channels $newFetchRange")
return viewModelScope.launchIO { return viewModelScope.launchIO {
fetchPrograms(channels.subList(fetchStart, fetchEnd)) fetchPrograms(channels, newFetchRange)
val programOffset = // withContext(Dispatchers.Main) {
(offset..<fetchStart).sumOf { channelProgramCount[channels[it].id]!! } // this@LiveTvViewModel.offset.value = fetchStart
withContext(Dispatchers.Main) { // currentIndex = index
this@LiveTvViewModel.offset.value = fetchStart // this@LiveTvViewModel.programOffset.value = programOffset
currentIndex = index // }
this@LiveTvViewModel.programOffset.value = programOffset
}
} }
} }
return null return null
@ -339,6 +348,8 @@ class LiveTvViewModel
} }
} }
fun IntRange.within(other: IntRange): Boolean = this.first >= other.first && this.last <= other.last
/** /**
* Returns the number of hours between two [LocalDateTime] * Returns the number of hours between two [LocalDateTime]
*/ */

View file

@ -48,14 +48,13 @@ import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.CircularProgress
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.data.RowColumn
import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.ui.rememberPosition
import com.github.damontecres.wholphin.ui.tryRequestFocus 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 com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import eu.wewox.programguide.ProgramGuide import eu.wewox.programguide.ProgramGuide
import eu.wewox.programguide.ProgramGuideDimensions import eu.wewox.programguide.ProgramGuideDimensions
import eu.wewox.programguide.ProgramGuideItem import eu.wewox.programguide.ProgramGuideItem
@ -81,7 +80,7 @@ fun TvGuideGrid(
val channels by viewModel.channels.observeAsState(listOf()) val channels by viewModel.channels.observeAsState(listOf())
val programs by viewModel.programs.observeAsState(listOf()) val programs by viewModel.programs.observeAsState(listOf())
val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf()) val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
val programOffset by viewModel.programOffset.observeAsState(0) val channelOffset by viewModel.offset.observeAsState(0)
when (val state = loading) { when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state, modifier) is LoadingState.Error -> ErrorMessage(state, modifier)
LoadingState.Pending, LoadingState.Pending,
@ -108,7 +107,7 @@ fun TvGuideGrid(
programs = programsByChannel, programs = programsByChannel,
channelProgramCount = viewModel.channelProgramCount, channelProgramCount = viewModel.channelProgramCount,
start = viewModel.start, start = viewModel.start,
programOffset = programOffset, channelOffset = channelOffset,
onClickChannel = { index, channel -> onClickChannel = { index, channel ->
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
Destination.Playback( Destination.Playback(
@ -117,9 +116,7 @@ fun TvGuideGrid(
), ),
) )
}, },
onFocusChannel = { index -> onFocus = viewModel::onFocusChannel,
viewModel.onFocusChannel(index)
},
onClickProgram = { index, program -> onClickProgram = { index, program ->
if (program.isFake) { if (program.isFake) {
val now = LocalDateTime.now() val now = LocalDateTime.now()
@ -204,17 +201,33 @@ fun TvGuideGrid(
start: LocalDateTime, start: LocalDateTime,
onClickChannel: (Int, TvChannel) -> Unit, onClickChannel: (Int, TvChannel) -> Unit,
onClickProgram: (Int, TvProgram) -> Unit, onClickProgram: (Int, TvProgram) -> Unit,
onFocusChannel: (Int) -> Unit, onFocus: (RowColumn) -> Unit,
programOffset: Int, channelOffset: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val state = rememberSaveableProgramGuideState() val state = rememberSaveableProgramGuideState()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
var focusedProgramIndex by rememberInt(0) // var focusedProgramIndex by rememberInt(0)
var focusedChannelIndex by rememberInt(0) // var focusedChannelIndex by rememberInt(0)
var focusedItem by rememberPosition(RowColumn(0, 0))
val focusedChannelIndex = focusedItem.row
val focusedProgramIndex =
remember(channelOffset, focusedItem) {
focusedItem.let { focus ->
(channelOffset..<focus.row).sumOf { channelProgramCount[channels[it].id]!! } + focus.column
}
}
LaunchedEffect(focusedProgramIndex) {
val (channelIndex, programIndex) = focusedItem
Timber.v("Focusing on $focusedItem, programIndex=$focusedProgramIndex")
scope.launch(ExceptionHandler()) {
state.animateToProgram(focusedProgramIndex, Alignment.Center)
}
}
val dimensions = val dimensions =
ProgramGuideDimensions( ProgramGuideDimensions(
timelineHourWidth = 240.dp, timelineHourWidth = 240.dp,
@ -224,33 +237,33 @@ fun TvGuideGrid(
currentTimeWidth = 2.dp, currentTimeWidth = 2.dp,
) )
val programsBeforeChannel = // val programsBeforeChannel =
remember { // remember {
CacheBuilder // CacheBuilder
.newBuilder() // .newBuilder()
.maximumSize(200) // .maximumSize(200)
.build<Int, Int>( // .build<Int, Int>(
object : CacheLoader<Int, Int>() { // object : CacheLoader<Int, Int>() {
override fun load(key: Int): Int = // override fun load(key: Int): Int =
( // (
channels // channels
.subList(0, key) // .subList(0, key)
.map { it.id } // .map { it.id }
.sumOf { // .sumOf {
channelProgramCount[it] ?: 0 // channelProgramCount[it] ?: 0
} - programOffset // } - programOffset
).coerceAtLeast(0) // ).coerceAtLeast(0)
}, // },
) // )
} // }
var movementEnabled by remember { mutableStateOf(true) } var movementEnabled by remember { mutableStateOf(true) }
LaunchedEffect(programOffset) { // LaunchedEffect(programOffset) {
movementEnabled = false // movementEnabled = false
programsBeforeChannel.invalidateAll() // programsBeforeChannel.invalidateAll()
Timber.v("Adjusting focusedProgramIndex, $focusedProgramIndex - $programOffset") // Timber.v("Adjusting focusedProgramIndex, $focusedProgramIndex - $programOffset")
focusedProgramIndex = (focusedProgramIndex - programOffset).coerceAtLeast(0) // focusedProgramIndex = (focusedProgramIndex - programOffset).coerceAtLeast(0)
movementEnabled = true // movementEnabled = true
} // }
var gridHasFocus by rememberSaveable { mutableStateOf(false) } var gridHasFocus by rememberSaveable { mutableStateOf(false) }
var channelColumnFocused by rememberSaveable { mutableStateOf(false) } var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
@ -271,13 +284,13 @@ fun TvGuideGrid(
if (!movementEnabled) { if (!movementEnabled) {
return@onPreviewKeyEvent true return@onPreviewKeyEvent true
} }
val newIndex = val item = focusedItem
val newFocusedItem =
when (it.key) { when (it.key) {
Key.Back -> { Key.Back -> {
val pos = programsBeforeChannel.get(focusedChannelIndex) if (item.column > 0) {
if (focusedProgramIndex - pos > 0) {
// Not at beginning of row, so move to beginning // Not at beginning of row, so move to beginning
pos item.copy(column = 0)
} else { } else {
// At beginning, so allow normal back button behavior // At beginning, so allow normal back button behavior
return@onPreviewKeyEvent false return@onPreviewKeyEvent false
@ -287,20 +300,9 @@ fun TvGuideGrid(
Key.DirectionRight -> { Key.DirectionRight -> {
if (channelColumnFocused) { if (channelColumnFocused) {
channelColumnFocused = false channelColumnFocused = false
focusedProgramIndex item.copy(column = 0)
} else { } else {
val nextProgramIndex = focusedProgramIndex + 1 item.copy(column = item.column + 1)
val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore
val channelPrograms =
programs[channels[focusedChannelIndex].id].orEmpty()
if (relativePosition >= channelPrograms.size) {
focusManager.moveFocus(FocusDirection.Right)
null
} else {
nextProgramIndex
}
} }
} }
@ -308,54 +310,45 @@ fun TvGuideGrid(
if (channelColumnFocused) { if (channelColumnFocused) {
focusManager.moveFocus(FocusDirection.Left) focusManager.moveFocus(FocusDirection.Left)
null null
} else if (item.column == 0) {
channelColumnFocused = true
item
} else { } else {
val nextProgramIndex = focusedProgramIndex - 1 item.copy(column = item.column - 1)
val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore
// val channelPrograms =
// programs[channels[focusedChannel].id].orEmpty()
if (relativePosition >= 0) {
nextProgramIndex
} else if (relativePosition == CHANNEL_COLUMN) {
channelColumnFocused = true
focusedProgramIndex
} else {
Timber.w("Move left to relativePosition=$relativePosition should not occur")
focusManager.moveFocus(FocusDirection.Left)
null
}
} }
} }
Key.DirectionUp -> { Key.DirectionUp -> {
val newFocusedChannelIndex = (focusedChannelIndex - 1) if (item.row <= 0) {
// Timber.v("newFocusedChannelIndex=$newFocusedChannelIndex")
if (newFocusedChannelIndex < 0) {
focusManager.moveFocus(FocusDirection.Up) focusManager.moveFocus(FocusDirection.Up)
null null
} else if (channelColumnFocused) {
focusedChannelIndex = newFocusedChannelIndex
programsBeforeChannel.get(focusedChannelIndex)
} else { } else {
val start = programList[focusedProgramIndex].startHours val newChannelIndex = item.row - 1
focusedChannelIndex = if (channelColumnFocused) {
newFocusedChannelIndex.coerceAtLeast(0) RowColumn(newChannelIndex, 0)
val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty()
val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else { } else {
val pIndex = pro.indexOfFirst { it.startHours >= start } val currentChannel = channels[item.row].id
if (pIndex >= 0) { val currentProgram =
programsBeforeChannel.get(focusedChannelIndex) + pIndex programs[currentChannel]?.get(item.column)
if (currentProgram == null) {
item
} else { } else {
programsBeforeChannel.get(focusedChannelIndex) + ( val start = currentProgram.startHours
programs[channelId]?.size val newChannelPrograms =
?: 0 programs[channels[newChannelIndex].id].orEmpty()
) val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
RowColumn(newChannelIndex, pIndex)
} else {
val pIndex =
newChannelPrograms.indexOfFirst { it.startHours >= start }
if (pIndex >= 0) {
RowColumn(newChannelIndex, pIndex)
} else {
RowColumn(newChannelIndex, 0)
}
}
} }
} }
} }
@ -363,45 +356,49 @@ fun TvGuideGrid(
Key.DirectionDown -> { Key.DirectionDown -> {
// Move channel focus down // Move channel focus down
val newFocusedChannelIndex = (focusedChannelIndex + 1) val newChannelIndex = item.row + 1
if (newFocusedChannelIndex >= channels.size) { if (newChannelIndex >= 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) { } else if (channelColumnFocused) {
// If focused on the channel column, move down a channel // If focused on the channel column, move down a channel
focusedChannelIndex = RowColumn(newChannelIndex, 0)
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
programsBeforeChannel.get(focusedChannelIndex)
} else { } else {
// Otherwise, moving to a new row // Otherwise, moving to a new row
focusedChannelIndex = // Get current program & its start time
newFocusedChannelIndex.coerceAtMost(channels.size - 1) val currentChannel = channels[item.row].id
// Get the new row/channel's programs val currentProgram =
val channelId = channels[focusedChannelIndex].id programs[currentChannel]?.get(item.column)
val pro = programs[channelId].orEmpty() if (currentProgram == null) {
// When the currently focused program starts // Data is loading in the background
val start = programList[focusedProgramIndex].startHours item
// Find the first program where the start time (of the previously focused program) is in the middle of a program
val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
// Found one, so focus on it
// Get the sum of all of the previous channels' program sizes, plus the index found to convert a relative program index into absolute
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else { } else {
// Didn't find one, probably due to missing data val start = currentProgram.startHours
// So now first the first one that starts after the previously focused program // Get the new row/channel's programs
val pIndex = pro.indexOfFirst { it.startHours >= start } val newChannelPrograms =
programs[channels[newChannelIndex].id].orEmpty()
// Find the first program where the start time (of the previously focused program) is in the middle of a program
val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) { if (pIndex >= 0) {
// Found one, so focus on it // Found one, so focus on it
programsBeforeChannel.get(focusedChannelIndex) + pIndex RowColumn(newChannelIndex, pIndex)
} else { } else {
// Did not find one, so focus on the final program in the list // Didn't find one, probably due to missing data
programsBeforeChannel.get(focusedChannelIndex) + ( // So now first the first one that starts after the previously focused program
programs[channelId]?.size val pIndex =
?: 0 newChannelPrograms.indexOfFirst { it.startHours >= start }
) if (pIndex >= 0) {
// Found one, so focus on it
RowColumn(newChannelIndex, pIndex)
} else {
// Did not find one, so focus on the final program in the list
RowColumn(
newChannelIndex,
newChannelPrograms.size - 1,
)
}
} }
} }
} }
@ -413,9 +410,15 @@ fun TvGuideGrid(
Timber.v("Clicked on %s", channel) Timber.v("Clicked on %s", channel)
onClickChannel.invoke(focusedChannelIndex, channel) onClickChannel.invoke(focusedChannelIndex, channel)
} else { } else {
val program = programList[focusedProgramIndex] val currentChannel = channels[item.row].id
Timber.v("Clicked on %s", program) val currentProgram =
onClickProgram.invoke(focusedProgramIndex, program) programs[currentChannel]?.get(item.column)
if (currentProgram == null) {
// Data is loading in the background
return@onPreviewKeyEvent true
}
Timber.v("Clicked on %s", currentProgram)
onClickProgram.invoke(focusedProgramIndex, currentProgram)
} }
null null
} }
@ -424,28 +427,34 @@ fun TvGuideGrid(
null null
} }
} }
if (newIndex != null) {
// Timber.v("newIndex=$newIndex") if (newFocusedItem != null) {
if (newIndex >= 0) { focusedItem = newFocusedItem
val before = programsBeforeChannel.get(focusedChannelIndex) onFocus(newFocusedItem)
val max = return@onPreviewKeyEvent true
before +
channels[focusedChannelIndex].let {
programs[it.id]?.size ?: 0
} - 1
val index = newIndex.coerceIn(before, max)
Timber.v("move: programIndex=$index, channelIndex=$focusedChannelIndex")
scope.launch(ExceptionHandler()) {
focusedProgramIndex = index
state.animateToProgram(index, Alignment.Center)
onFocusChannel.invoke(focusedChannelIndex)
}
return@onPreviewKeyEvent true
} else {
Timber.w("newIndex is <0: $newIndex")
return@onPreviewKeyEvent true
}
} }
// if (newIndex != null) {
// // Timber.v("newIndex=$newIndex")
// if (newIndex >= 0) {
// val before = programsBeforeChannel.get(focusedChannelIndex)
// val max =
// before +
// channels[focusedChannelIndex].let {
// programs[it.id]?.size ?: 0
// } - 1
// val index = newIndex.coerceIn(before, max)
// Timber.v("move: programIndex=$index, channelIndex=$focusedChannelIndex")
// scope.launch(ExceptionHandler()) {
// focusedProgramIndex = index
// state.animateToProgram(index, Alignment.Center)
// onFocusChannel.invoke(focusedChannelIndex)
// }
// return@onPreviewKeyEvent true
// } else {
// Timber.w("newIndex is <0: $newIndex")
// return@onPreviewKeyEvent true
// }
// }
return@onPreviewKeyEvent false return@onPreviewKeyEvent false
}, },
) { ) {