This commit is contained in:
Damontecres 2025-11-01 17:26:24 -04:00
parent 8f08f0d118
commit bf28209255
No known key found for this signature in database
2 changed files with 84 additions and 58 deletions

View file

@ -58,14 +58,14 @@ 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<List<TvProgram>>() val programs = MutableLiveData<FetchedPrograms>()
val programsByChannel = MutableLiveData<Map<UUID, List<TvProgram>>>(mapOf()) // 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 = 8
val fetchedRange = MutableLiveData<IntRange>(0..<range) // 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)
@ -100,9 +100,12 @@ class LiveTvViewModel
} }
} }
private suspend fun fetchProgramsWithLoading(channels: List<TvChannel>) { private suspend fun fetchProgramsWithLoading(
channels: List<TvChannel>,
range: IntRange,
) {
loading.setValueOnMain(LoadingState.Loading) loading.setValueOnMain(LoadingState.Loading)
fetchPrograms(channels, fetchedRange.value!!) fetchPrograms(channels, range)
loading.setValueOnMain(LoadingState.Success) loading.setValueOnMain(LoadingState.Success)
} }
@ -112,12 +115,13 @@ 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)
Timber.v("Fetching programs for ${channels.size} channels") val channelsToFetch = channels.subList(range.first, range.last)
Timber.v("Fetching programs for $range channels ${channelsToFetch.size}")
val request = val request =
GetProgramsDto( GetProgramsDto(
maxStartDate = maxStartDate, maxStartDate = maxStartDate,
minEndDate = minEndDate, minEndDate = minEndDate,
channelIds = channels.map { it.id }, channelIds = channelsToFetch.map { it.id },
sortBy = listOf(ItemSortBy.START_DATE), sortBy = listOf(ItemSortBy.START_DATE),
) )
val programs = val programs =
@ -161,7 +165,8 @@ class LiveTvViewModel
} }
val programsByChannel = programs.groupBy { it.channelId } val programsByChannel = programs.groupBy { it.channelId }
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() } val emptyChannels =
channelsToFetch.filter { programsByChannel[it.id].orEmpty().isEmpty() }
val fake = mutableListOf<TvProgram>() val fake = mutableListOf<TvProgram>()
val finalProgramsByChannel = val finalProgramsByChannel =
programsByChannel.toMutableMap().apply { programsByChannel.toMutableMap().apply {
@ -209,9 +214,8 @@ class LiveTvViewModel
.filter { it.startHours == it.endHours } .filter { it.startHours == it.endHours }
.let { Timber.e("Items with same start & end!!! %s", it) } .let { Timber.e("Items with same start & end!!! %s", it) }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = finalProgramList this@LiveTvViewModel.programs.value =
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel FetchedPrograms(range, finalProgramList, finalProgramsByChannel)
this@LiveTvViewModel.fetchedRange.value = range
} }
} }
@ -247,7 +251,7 @@ 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()) fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else { } else {
api.liveTvApi.cancelTimer(timerId) api.liveTvApi.cancelTimer(timerId)
refreshProgram(programIndex, programId) refreshProgram(programIndex, programId)
@ -265,7 +269,7 @@ 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()) fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else { } else {
val payload = val payload =
TimerInfoDto( TimerInfoDto(
@ -301,36 +305,37 @@ class LiveTvViewModel
programId: UUID, programId: UUID,
) = mutex.withLock { ) = mutex.withLock {
loading.setValueOnMain(LoadingState.Loading) loading.setValueOnMain(LoadingState.Loading)
val program by api.liveTvApi.getProgram(programId.toServerString()) // TODO
val newProgram = // val program by api.liveTvApi.getProgram(programId.toServerString())
programs.value?.getOrNull(programIndex)?.copy( // val newProgram =
isRecording = program.timerId.isNotNullOrBlank(), // programs.value?.getOrNull(programIndex)?.copy(
isSeriesRecording = program.seriesTimerId.isNotNullOrBlank(), // isRecording = program.timerId.isNotNullOrBlank(),
) // isSeriesRecording = program.seriesTimerId.isNotNullOrBlank(),
Timber.v("new program %s", newProgram) // )
if (newProgram != null) { // Timber.v("new program %s", newProgram)
programs.value // if (newProgram != null) {
?.toMutableList() // programs.value
?.apply { // ?.toMutableList()
this[programIndex] = newProgram // ?.apply {
}?.let { // this[programIndex] = newProgram
this@LiveTvViewModel.programs.setValueOnMain(it) // }?.let {
} // this@LiveTvViewModel.programs.setValueOnMain(it)
} // }
// }
loading.setValueOnMain(LoadingState.Success) loading.setValueOnMain(LoadingState.Success)
} }
fun onFocusChannel(position: RowColumn): Job? { fun onFocusChannel(position: RowColumn): Job? {
return channels.value?.let { channels -> return channels.value?.let { channels ->
val fetchedRange = fetchedRange.value!! val fetchedRange = programs.value!!.range
val quarter = (fetchedRange.last - fetchedRange.start) / 4 val quarter = (fetchedRange.last - fetchedRange.start) / 4
val rangeStart = fetchedRange.start + quarter val rangeStart = fetchedRange.start + quarter
val rangeEnd = fetchedRange.last - quarter val rangeEnd = fetchedRange.last - quarter
val testRange = rangeStart..<rangeEnd val testRange = rangeStart..<rangeEnd
Timber.v( // Timber.v(
"onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange", // "onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$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
@ -340,7 +345,7 @@ class LiveTvViewModel
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 { return viewModelScope.launchIO {
fetchPrograms(channels, newFetchRange) fetchProgramsWithLoading(channels, newFetchRange)
} }
} }
return null return null
@ -396,3 +401,9 @@ enum class ProgramCategory(
SPORTS(AppColors.DarkRed), SPORTS(AppColors.DarkRed),
FAKE(null), FAKE(null),
} }
data class FetchedPrograms(
val range: IntRange,
val programs: List<TvProgram>,
val programsByChannel: Map<UUID, List<TvProgram>>,
)

View file

@ -51,6 +51,7 @@ 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
@ -78,9 +79,9 @@ fun TvGuideGrid(
} }
val loading by viewModel.loading.observeAsState(LoadingState.Pending) val loading by viewModel.loading.observeAsState(LoadingState.Pending)
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(FetchedPrograms(0..0, listOf(), mapOf()))
val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf()) // val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
val fetchedRange by viewModel.fetchedRange.observeAsState(0..0) // val fetchedRange by viewModel.fetchedRange.observeAsState(0..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,
@ -103,11 +104,11 @@ fun TvGuideGrid(
TvGuideGrid( TvGuideGrid(
loading = state is LoadingState.Loading, loading = state is LoadingState.Loading,
channels = channels, channels = channels,
programList = programs, // programList = programs,
programs = programsByChannel, programs = programs,
channelProgramCount = viewModel.channelProgramCount, channelProgramCount = viewModel.channelProgramCount,
start = viewModel.start, start = viewModel.start,
channelOffset = fetchedRange.start, // channelOffset = fetchedRange.start,
onClickChannel = { index, channel -> onClickChannel = { index, channel ->
viewModel.navigationManager.navigateTo( viewModel.navigationManager.navigateTo(
Destination.Playback( Destination.Playback(
@ -195,14 +196,15 @@ const val CHANNEL_COLUMN = -1
fun TvGuideGrid( fun TvGuideGrid(
loading: Boolean, loading: Boolean,
channels: List<TvChannel>, channels: List<TvChannel>,
programList: List<TvProgram>, // programList: List<TvProgram>,
programs: Map<UUID, List<TvProgram>>, // programs: Map<UUID, List<TvProgram>>,
programs: FetchedPrograms,
channelProgramCount: Map<UUID, Int>, channelProgramCount: Map<UUID, Int>,
start: LocalDateTime, start: LocalDateTime,
onClickChannel: (Int, TvChannel) -> Unit, onClickChannel: (Int, TvChannel) -> Unit,
onClickProgram: (Int, TvProgram) -> Unit, onClickProgram: (Int, TvProgram) -> Unit,
onFocus: (RowColumn) -> Unit, onFocus: (RowColumn) -> Unit,
channelOffset: Int, // channelOffset: Int,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@ -213,16 +215,18 @@ fun TvGuideGrid(
var focusedItem by rememberPosition(RowColumn(0, 0)) var focusedItem by rememberPosition(RowColumn(0, 0))
val focusedChannelIndex = focusedItem.row val focusedChannelIndex = focusedItem.row
val focusedProgramIndex = val focusedProgramIndex =
remember(channelOffset, focusedItem) { remember(programs.range, focusedItem) {
focusedItem.let { focus -> focusedItem.let { focus ->
(channelOffset..<focus.row).sumOf { channelProgramCount[channels[it].id]!! } + focus.column (programs.range.start..<focus.row).sumOf {
val channelId = channels[it].id
channelProgramCount[channelId] ?: 0
} + focus.column
} }
} }
LaunchedEffect(focusedProgramIndex) { LaunchedEffect(focusedProgramIndex) {
// Timber.v("Focusing on $focusedItem, programIndex=$focusedProgramIndex") // Timber.v("Focusing on $focusedItem, programIndex=$focusedProgramIndex")
scope.launch(ExceptionHandler()) { scope.launch(ExceptionHandler()) {
// Timber.v("program=${programList.getOrNull(focusedProgramIndex)}")
state.animateToProgram(focusedProgramIndex, Alignment.Center) state.animateToProgram(focusedProgramIndex, Alignment.Center)
} }
} }
@ -250,6 +254,10 @@ 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) {
@ -295,13 +303,13 @@ fun TvGuideGrid(
} else { } else {
val currentChannel = channels[item.row].id val currentChannel = channels[item.row].id
val currentProgram = val currentProgram =
programs[currentChannel]?.get(item.column) programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) { if (currentProgram == null) {
item item
} else { } else {
val start = currentProgram.startHours val start = currentProgram.startHours
val newChannelPrograms = val newChannelPrograms =
programs[channels[newChannelIndex].id].orEmpty() 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,7 +343,7 @@ fun TvGuideGrid(
// 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 =
programs[currentChannel]?.get(item.column) programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) { if (currentProgram == null) {
// Data is loading in the background // Data is loading in the background
item item
@ -343,7 +351,7 @@ fun TvGuideGrid(
val start = currentProgram.startHours val start = currentProgram.startHours
// Get the new row/channel's programs // Get the new row/channel's programs
val newChannelPrograms = val newChannelPrograms =
programs[channels[newChannelIndex].id].orEmpty() 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) }
@ -378,13 +386,16 @@ fun TvGuideGrid(
} else { } else {
val currentChannel = channels[item.row].id val currentChannel = channels[item.row].id
val currentProgram = val currentProgram =
programs[currentChannel]?.get(item.column) programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) { if (currentProgram == null) {
// Data is loading in the background // Data is loading in the background
return@onPreviewKeyEvent true return@onPreviewKeyEvent true
} }
Timber.v("Clicked on %s", currentProgram) Timber.v("Clicked on %s", currentProgram)
onClickProgram.invoke(focusedProgramIndex, currentProgram) onClickProgram.invoke(
focusedProgramIndex,
currentProgram,
)
} }
null null
} }
@ -396,7 +407,7 @@ fun TvGuideGrid(
if (newFocusedItem != null) { if (newFocusedItem != null) {
val channel = channels[newFocusedItem.row] val channel = channels[newFocusedItem.row]
val programs = programs[channel.id].orEmpty() val programs = programs.programsByChannel[channel.id].orEmpty()
// Ensure it isn't going out of range // Ensure it isn't going out of range
val toFocus = val toFocus =
newFocusedItem newFocusedItem
@ -424,7 +435,12 @@ fun TvGuideGrid(
}, },
) { ) {
Surface( Surface(
colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary.copy(alpha = .25f)), colors =
SurfaceDefaults.colors(
MaterialTheme.colorScheme.tertiary.copy(
alpha = .25f,
),
),
modifier = Modifier, modifier = Modifier,
) { ) {
// Empty // Empty
@ -465,16 +481,15 @@ fun TvGuideGrid(
) )
} }
} }
programs( programs(
count = programList.size, count = programs.programs.size,
layoutInfo = { programIndex -> layoutInfo = { programIndex ->
val program = programList[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) ProgramGuideItem.Program(channelIndex, program.startHours, program.endHours)
}, },
) { programIndex -> ) { programIndex ->
val program = programList[programIndex] val program = programs.programs[programIndex]
val focused = val focused =
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
val background = val background =