Handle DST and ranges

This commit is contained in:
Damontecres 2025-11-01 18:31:22 -04:00
parent bf28209255
commit 8792c7dcb2
No known key found for this signature in database
2 changed files with 85 additions and 65 deletions

View file

@ -59,13 +59,11 @@ class LiveTvViewModel
val channels = MutableLiveData<List<TvChannel>>() val channels = MutableLiveData<List<TvChannel>>()
val channelProgramCount = mutableMapOf<UUID, Int>() val channelProgramCount = mutableMapOf<UUID, Int>()
val programs = MutableLiveData<FetchedPrograms>() val programs = MutableLiveData<FetchedPrograms>()
// val programsByChannel = MutableLiveData<Map<UUID, List<TvProgram>>>(mapOf())
val fetchingItem = MutableLiveData<LoadingState>(LoadingState.Pending) val fetchingItem = MutableLiveData<LoadingState>(LoadingState.Pending)
val fetchedItem = MutableLiveData<BaseItem?>(null) val fetchedItem = MutableLiveData<BaseItem?>(null)
private val range = 8 private val range = 150
// 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)
@ -115,7 +113,7 @@ class LiveTvViewModel
) = mutex.withLock { ) = mutex.withLock {
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1) val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
val minEndDate = start.plusMinutes(1L) val minEndDate = start.plusMinutes(1L)
val channelsToFetch = channels.subList(range.first, range.last) val channelsToFetch = channels.subList(range.first, range.last + 1)
Timber.v("Fetching programs for $range channels ${channelsToFetch.size}") Timber.v("Fetching programs for $range channels ${channelsToFetch.size}")
val request = val request =
GetProgramsDto( GetProgramsDto(
@ -199,20 +197,13 @@ class LiveTvViewModel
} }
val finalProgramList = val finalProgramList =
(programs + fake) (programs + fake)
.filterNot { .sortedWith(
// TODO filter out items that start after end
// This can happen due to daylight savings switch or just be bad data
it.startHours >= it.endHours
}.sortedWith(
compareBy( compareBy(
{ channelsIdToIndex[it.channelId]!! }, { channelsIdToIndex[it.channelId]!! },
{ it.start }, { it.start },
), ),
) )
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs") Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
finalProgramList
.filter { it.startHours == it.endHours }
.let { Timber.e("Items with same start & end!!! %s", it) }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = this@LiveTvViewModel.programs.value =
FetchedPrograms(range, finalProgramList, finalProgramsByChannel) FetchedPrograms(range, finalProgramList, finalProgramsByChannel)
@ -251,11 +242,10 @@ class LiveTvViewModel
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) { viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
if (series) { if (series) {
api.liveTvApi.cancelSeriesTimer(timerId) api.liveTvApi.cancelSeriesTimer(timerId)
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else { } else {
api.liveTvApi.cancelTimer(timerId) api.liveTvApi.cancelTimer(timerId)
refreshProgram(programIndex, programId)
} }
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} }
} }
} }
@ -269,7 +259,6 @@ class LiveTvViewModel
val d by api.liveTvApi.getDefaultTimer(programId.toServerString()) val d by api.liveTvApi.getDefaultTimer(programId.toServerString())
if (series) { if (series) {
api.liveTvApi.createSeriesTimer(d) api.liveTvApi.createSeriesTimer(d)
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else { } else {
val payload = val payload =
TimerInfoDto( TimerInfoDto(
@ -295,8 +284,8 @@ class LiveTvViewModel
keepUntil = d.keepUntil, keepUntil = d.keepUntil,
) )
api.liveTvApi.createTimer(payload) api.liveTvApi.createTimer(payload)
refreshProgram(programIndex, programId)
} }
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} }
} }
@ -325,17 +314,33 @@ class LiveTvViewModel
loading.setValueOnMain(LoadingState.Success) loading.setValueOnMain(LoadingState.Success)
} }
fun onFocusChannel(position: RowColumn): Job? { private var focusLoadingJob: Job? = null
return channels.value?.let { channels ->
fun onFocusChannel(position: RowColumn) {
channels.value?.let { channels ->
val fetchedRange = programs.value!!.range val fetchedRange = programs.value!!.range
val quarter = (fetchedRange.last - fetchedRange.start) / 4 val quarter = range / 4
val rangeStart = fetchedRange.start + quarter var rangeStart = fetchedRange.start + quarter
val rangeEnd = fetchedRange.last - quarter var rangeEnd = fetchedRange.last - quarter
if (rangeEnd - rangeStart < range) {
if (position.row < range / 2) {
// Close to beginning
rangeStart = 0
} else if (position.row > (channels.size - range / 2)) {
// Close to the end
rangeEnd = channels.size
}
}
val testRange = rangeStart..<rangeEnd val testRange = rangeStart..<rangeEnd
// Timber.v( Timber.v(
// "onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange", "onFocusChannel: position=%s, fetchedRange=%s, testRange=%s",
// ) position,
fetchedRange,
testRange,
)
val fetchStart = (position.row - range).coerceAtLeast(0) val fetchStart = (position.row - range).coerceAtLeast(0)
val fetchEnd = (position.row + range).coerceAtMost(channels.size) val fetchEnd = (position.row + range).coerceAtMost(channels.size)
val newFetchRange = fetchStart..<fetchEnd val newFetchRange = fetchStart..<fetchEnd
@ -344,11 +349,12 @@ class LiveTvViewModel
// Fetch new data // Fetch new data
if (position.row !in testRange && !newFetchRange.within(fetchedRange)) { if (position.row !in testRange && !newFetchRange.within(fetchedRange)) {
Timber.v("Loading more programs for channels $newFetchRange") Timber.v("Loading more programs for channels $newFetchRange")
return viewModelScope.launchIO { focusLoadingJob?.cancel()
focusLoadingJob =
viewModelScope.launchIO {
fetchProgramsWithLoading(channels, newFetchRange) fetchProgramsWithLoading(channels, newFetchRange)
} }
} }
return null
} }
} }
} }

View file

@ -51,7 +51,6 @@ import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.data.RowColumn 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.playback.isDpad
import com.github.damontecres.wholphin.ui.rememberPosition 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
@ -65,6 +64,7 @@ import timber.log.Timber
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.util.UUID import java.util.UUID
import kotlin.math.abs
@Composable @Composable
fun TvGuideGrid( fun TvGuideGrid(
@ -254,10 +254,6 @@ fun TvGuideGrid(
if (it.type == KeyEventType.KeyUp) { if (it.type == KeyEventType.KeyUp) {
return@onPreviewKeyEvent false return@onPreviewKeyEvent false
} }
if (isDpad(it) && loading) {
// Prevent movement during loading
return@onPreviewKeyEvent true
}
val item = focusedItem val item = focusedItem
val newFocusedItem = val newFocusedItem =
when (it.key) { when (it.key) {
@ -304,12 +300,16 @@ fun TvGuideGrid(
val currentChannel = channels[item.row].id val currentChannel = channels[item.row].id
val currentProgram = val currentProgram =
programs.programsByChannel[currentChannel]?.get(item.column) programs.programsByChannel[currentChannel]?.get(item.column)
val newChannelId = channels[newChannelIndex].id
val newChannelPrograms =
programs.programsByChannel[newChannelId]
if (newChannelPrograms == null) {
return@onPreviewKeyEvent true
}
if (currentProgram == null) { if (currentProgram == null) {
item item
} else { } else {
val start = currentProgram.startHours val start = currentProgram.startHours
val newChannelPrograms =
programs.programsByChannel[channels[newChannelIndex].id].orEmpty()
val pIndex = val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) } newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) { if (pIndex >= 0) {
@ -335,11 +335,20 @@ fun TvGuideGrid(
// 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 {
// Otherwise, moving to a new row
// Get the new row/channel's programs
val newChannelId = channels[newChannelIndex].id
val newChannelPrograms =
programs.programsByChannel[newChannelId]
if (newChannelPrograms == null) {
// This means there is no data for the new channel and it is loading in the background
return@onPreviewKeyEvent true
}
if (channelColumnFocused) {
// If focused on the channel column, move down a channel // If focused on the channel column, move down a channel
RowColumn(newChannelIndex, 0) RowColumn(newChannelIndex, 0)
} else { } else {
// Otherwise, moving to a new row
// Get current program & its start time // Get current program & its start time
val currentChannel = channels[item.row].id val currentChannel = channels[item.row].id
val currentProgram = val currentProgram =
@ -349,9 +358,6 @@ fun TvGuideGrid(
item item
} else { } else {
val start = currentProgram.startHours val start = currentProgram.startHours
// Get the new row/channel's programs
val newChannelPrograms =
programs.programsByChannel[channels[newChannelIndex].id].orEmpty()
// 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 =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) } newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
@ -377,6 +383,7 @@ fun TvGuideGrid(
} }
} }
} }
}
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> { Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
if (channelColumnFocused) { if (channelColumnFocused) {
@ -416,7 +423,7 @@ fun TvGuideGrid(
column = column =
newFocusedItem.column.coerceIn( newFocusedItem.column.coerceIn(
0, 0,
programs.size - 1, (programs.size - 1).coerceAtLeast(0),
), ),
) )
focusedItem = toFocus focusedItem = toFocus
@ -486,7 +493,14 @@ fun TvGuideGrid(
layoutInfo = { programIndex -> layoutInfo = { programIndex ->
val program = programs.programs[programIndex] val program = programs.programs[programIndex]
val channelIndex = channels.indexOfFirst { it.id == program.channelId } val channelIndex = channels.indexOfFirst { it.id == program.channelId }
ProgramGuideItem.Program(channelIndex, program.startHours, program.endHours) // Using the duration for endHour accounts for daylight savings switch
// Eg a 1:30am-1:00am show
val duration = abs(program.endHours - program.startHours)
ProgramGuideItem.Program(
channelIndex,
program.startHours,
program.startHours + duration,
)
}, },
) { programIndex -> ) { programIndex ->
val program = programs.programs[programIndex] val program = programs.programs[programIndex]