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 channelProgramCount = mutableMapOf<UUID, Int>()
val programs = MutableLiveData<FetchedPrograms>()
// val programsByChannel = MutableLiveData<Map<UUID, List<TvProgram>>>(mapOf())
val fetchingItem = MutableLiveData<LoadingState>(LoadingState.Pending)
val fetchedItem = MutableLiveData<BaseItem?>(null)
private val range = 8
// val fetchedRange = MutableLiveData<IntRange>(0..<range)
private val range = 150
fun init(firstLoad: Boolean) {
start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
@ -115,7 +113,7 @@ class LiveTvViewModel
) = mutex.withLock {
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
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}")
val request =
GetProgramsDto(
@ -199,20 +197,13 @@ class LiveTvViewModel
}
val finalProgramList =
(programs + fake)
.filterNot {
// 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(
.sortedWith(
compareBy(
{ channelsIdToIndex[it.channelId]!! },
{ it.start },
),
)
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) {
this@LiveTvViewModel.programs.value =
FetchedPrograms(range, finalProgramList, finalProgramsByChannel)
@ -251,11 +242,10 @@ class LiveTvViewModel
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
if (series) {
api.liveTvApi.cancelSeriesTimer(timerId)
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else {
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())
if (series) {
api.liveTvApi.createSeriesTimer(d)
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else {
val payload =
TimerInfoDto(
@ -295,8 +284,8 @@ class LiveTvViewModel
keepUntil = d.keepUntil,
)
api.liveTvApi.createTimer(payload)
refreshProgram(programIndex, programId)
}
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
}
}
@ -325,17 +314,33 @@ class LiveTvViewModel
loading.setValueOnMain(LoadingState.Success)
}
fun onFocusChannel(position: RowColumn): Job? {
return channels.value?.let { channels ->
private var focusLoadingJob: Job? = null
fun onFocusChannel(position: RowColumn) {
channels.value?.let { channels ->
val fetchedRange = programs.value!!.range
val quarter = (fetchedRange.last - fetchedRange.start) / 4
val rangeStart = fetchedRange.start + quarter
val rangeEnd = fetchedRange.last - quarter
val quarter = range / 4
var rangeStart = fetchedRange.start + 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
// Timber.v(
// "onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange",
// )
Timber.v(
"onFocusChannel: position=%s, fetchedRange=%s, testRange=%s",
position,
fetchedRange,
testRange,
)
val fetchStart = (position.row - range).coerceAtLeast(0)
val fetchEnd = (position.row + range).coerceAtMost(channels.size)
val newFetchRange = fetchStart..<fetchEnd
@ -344,11 +349,12 @@ class LiveTvViewModel
// Fetch new data
if (position.row !in testRange && !newFetchRange.within(fetchedRange)) {
Timber.v("Loading more programs for channels $newFetchRange")
return viewModelScope.launchIO {
fetchProgramsWithLoading(channels, newFetchRange)
}
focusLoadingJob?.cancel()
focusLoadingJob =
viewModelScope.launchIO {
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.enableMarquee
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.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
@ -65,6 +64,7 @@ import timber.log.Timber
import java.time.LocalDateTime
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.math.abs
@Composable
fun TvGuideGrid(
@ -254,10 +254,6 @@ fun TvGuideGrid(
if (it.type == KeyEventType.KeyUp) {
return@onPreviewKeyEvent false
}
if (isDpad(it) && loading) {
// Prevent movement during loading
return@onPreviewKeyEvent true
}
val item = focusedItem
val newFocusedItem =
when (it.key) {
@ -304,12 +300,16 @@ fun TvGuideGrid(
val currentChannel = channels[item.row].id
val currentProgram =
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) {
item
} else {
val start = currentProgram.startHours
val newChannelPrograms =
programs.programsByChannel[channels[newChannelIndex].id].orEmpty()
val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
@ -335,43 +335,50 @@ fun TvGuideGrid(
// If trying to move below the final channel, then move focus out of the grid
focusManager.moveFocus(FocusDirection.Down)
null
} else if (channelColumnFocused) {
// If focused on the channel column, move down a channel
RowColumn(newChannelIndex, 0)
} else {
// Otherwise, moving to a new row
// Get current program & its start time
val currentChannel = channels[item.row].id
val currentProgram =
programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) {
// Data is loading in the background
item
// 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
RowColumn(newChannelIndex, 0)
} else {
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
val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
// Found one, so focus on it
RowColumn(newChannelIndex, pIndex)
// Get current program & its start time
val currentChannel = channels[item.row].id
val currentProgram =
programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) {
// Data is loading in the background
item
} else {
// Didn't find one, probably due to missing data
// So now first the first one that starts after the previously focused program
val start = currentProgram.startHours
// Find the first program where the start time (of the previously focused program) is in the middle of a program
val pIndex =
newChannelPrograms.indexOfFirst { it.startHours >= start }
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
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,
)
// Didn't find one, probably due to missing data
// So now first the first one that starts after the previously focused program
val pIndex =
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,
)
}
}
}
}
@ -416,7 +423,7 @@ fun TvGuideGrid(
column =
newFocusedItem.column.coerceIn(
0,
programs.size - 1,
(programs.size - 1).coerceAtLeast(0),
),
)
focusedItem = toFocus
@ -486,7 +493,14 @@ fun TvGuideGrid(
layoutInfo = { programIndex ->
val program = programs.programs[programIndex]
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 ->
val program = programs.programs[programIndex]