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 channelProgramCount = mutableMapOf<UUID, Int>()
val programs = MutableLiveData<List<TvProgram>>()
val programsByChannel = MutableLiveData<Map<UUID, List<TvProgram>>>(mapOf())
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)
// val fetchedRange = MutableLiveData<IntRange>(0..<range)
fun init(firstLoad: Boolean) {
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)
fetchPrograms(channels, fetchedRange.value!!)
fetchPrograms(channels, range)
loading.setValueOnMain(LoadingState.Success)
}
@ -112,12 +115,13 @@ class LiveTvViewModel
) = mutex.withLock {
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
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 =
GetProgramsDto(
maxStartDate = maxStartDate,
minEndDate = minEndDate,
channelIds = channels.map { it.id },
channelIds = channelsToFetch.map { it.id },
sortBy = listOf(ItemSortBy.START_DATE),
)
val programs =
@ -161,7 +165,8 @@ class LiveTvViewModel
}
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 finalProgramsByChannel =
programsByChannel.toMutableMap().apply {
@ -209,9 +214,8 @@ class LiveTvViewModel
.filter { it.startHours == it.endHours }
.let { Timber.e("Items with same start & end!!! %s", it) }
withContext(Dispatchers.Main) {
this@LiveTvViewModel.programs.value = finalProgramList
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
this@LiveTvViewModel.fetchedRange.value = range
this@LiveTvViewModel.programs.value =
FetchedPrograms(range, finalProgramList, finalProgramsByChannel)
}
}
@ -247,7 +251,7 @@ class LiveTvViewModel
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
if (series) {
api.liveTvApi.cancelSeriesTimer(timerId)
fetchProgramsWithLoading(channels.value.orEmpty())
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else {
api.liveTvApi.cancelTimer(timerId)
refreshProgram(programIndex, programId)
@ -265,7 +269,7 @@ class LiveTvViewModel
val d by api.liveTvApi.getDefaultTimer(programId.toServerString())
if (series) {
api.liveTvApi.createSeriesTimer(d)
fetchProgramsWithLoading(channels.value.orEmpty())
fetchProgramsWithLoading(channels.value.orEmpty(), programs.value!!.range)
} else {
val payload =
TimerInfoDto(
@ -301,36 +305,37 @@ class LiveTvViewModel
programId: UUID,
) = mutex.withLock {
loading.setValueOnMain(LoadingState.Loading)
val program by api.liveTvApi.getProgram(programId.toServerString())
val newProgram =
programs.value?.getOrNull(programIndex)?.copy(
isRecording = program.timerId.isNotNullOrBlank(),
isSeriesRecording = program.seriesTimerId.isNotNullOrBlank(),
)
Timber.v("new program %s", newProgram)
if (newProgram != null) {
programs.value
?.toMutableList()
?.apply {
this[programIndex] = newProgram
}?.let {
this@LiveTvViewModel.programs.setValueOnMain(it)
}
}
// TODO
// val program by api.liveTvApi.getProgram(programId.toServerString())
// val newProgram =
// programs.value?.getOrNull(programIndex)?.copy(
// isRecording = program.timerId.isNotNullOrBlank(),
// isSeriesRecording = program.seriesTimerId.isNotNullOrBlank(),
// )
// Timber.v("new program %s", newProgram)
// if (newProgram != null) {
// programs.value
// ?.toMutableList()
// ?.apply {
// this[programIndex] = newProgram
// }?.let {
// this@LiveTvViewModel.programs.setValueOnMain(it)
// }
// }
loading.setValueOnMain(LoadingState.Success)
}
fun onFocusChannel(position: RowColumn): Job? {
return channels.value?.let { channels ->
val fetchedRange = fetchedRange.value!!
val fetchedRange = programs.value!!.range
val quarter = (fetchedRange.last - fetchedRange.start) / 4
val rangeStart = fetchedRange.start + quarter
val rangeEnd = fetchedRange.last - quarter
val testRange = rangeStart..<rangeEnd
Timber.v(
"onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange",
)
// Timber.v(
// "onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange",
// )
val fetchStart = (position.row - range).coerceAtLeast(0)
val fetchEnd = (position.row + range).coerceAtMost(channels.size)
val newFetchRange = fetchStart..<fetchEnd
@ -340,7 +345,7 @@ class LiveTvViewModel
if (position.row !in testRange && !newFetchRange.within(fetchedRange)) {
Timber.v("Loading more programs for channels $newFetchRange")
return viewModelScope.launchIO {
fetchPrograms(channels, newFetchRange)
fetchProgramsWithLoading(channels, newFetchRange)
}
}
return null
@ -396,3 +401,9 @@ enum class ProgramCategory(
SPORTS(AppColors.DarkRed),
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.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
@ -78,9 +79,9 @@ fun TvGuideGrid(
}
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
val channels by viewModel.channels.observeAsState(listOf())
val programs by viewModel.programs.observeAsState(listOf())
val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
val fetchedRange by viewModel.fetchedRange.observeAsState(0..0)
val programs by viewModel.programs.observeAsState(FetchedPrograms(0..0, listOf(), mapOf()))
// val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
// val fetchedRange by viewModel.fetchedRange.observeAsState(0..0)
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state, modifier)
LoadingState.Pending,
@ -103,11 +104,11 @@ fun TvGuideGrid(
TvGuideGrid(
loading = state is LoadingState.Loading,
channels = channels,
programList = programs,
programs = programsByChannel,
// programList = programs,
programs = programs,
channelProgramCount = viewModel.channelProgramCount,
start = viewModel.start,
channelOffset = fetchedRange.start,
// channelOffset = fetchedRange.start,
onClickChannel = { index, channel ->
viewModel.navigationManager.navigateTo(
Destination.Playback(
@ -195,14 +196,15 @@ const val CHANNEL_COLUMN = -1
fun TvGuideGrid(
loading: Boolean,
channels: List<TvChannel>,
programList: List<TvProgram>,
programs: Map<UUID, List<TvProgram>>,
// programList: List<TvProgram>,
// programs: Map<UUID, List<TvProgram>>,
programs: FetchedPrograms,
channelProgramCount: Map<UUID, Int>,
start: LocalDateTime,
onClickChannel: (Int, TvChannel) -> Unit,
onClickProgram: (Int, TvProgram) -> Unit,
onFocus: (RowColumn) -> Unit,
channelOffset: Int,
// channelOffset: Int,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
@ -213,16 +215,18 @@ fun TvGuideGrid(
var focusedItem by rememberPosition(RowColumn(0, 0))
val focusedChannelIndex = focusedItem.row
val focusedProgramIndex =
remember(channelOffset, focusedItem) {
remember(programs.range, focusedItem) {
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) {
// Timber.v("Focusing on $focusedItem, programIndex=$focusedProgramIndex")
scope.launch(ExceptionHandler()) {
// Timber.v("program=${programList.getOrNull(focusedProgramIndex)}")
state.animateToProgram(focusedProgramIndex, Alignment.Center)
}
}
@ -250,6 +254,10 @@ 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) {
@ -295,13 +303,13 @@ fun TvGuideGrid(
} else {
val currentChannel = channels[item.row].id
val currentProgram =
programs[currentChannel]?.get(item.column)
programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) {
item
} else {
val start = currentProgram.startHours
val newChannelPrograms =
programs[channels[newChannelIndex].id].orEmpty()
programs.programsByChannel[channels[newChannelIndex].id].orEmpty()
val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
@ -335,7 +343,7 @@ fun TvGuideGrid(
// Get current program & its start time
val currentChannel = channels[item.row].id
val currentProgram =
programs[currentChannel]?.get(item.column)
programs.programsByChannel[currentChannel]?.get(item.column)
if (currentProgram == null) {
// Data is loading in the background
item
@ -343,7 +351,7 @@ fun TvGuideGrid(
val start = currentProgram.startHours
// Get the new row/channel's programs
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
val pIndex =
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
@ -378,13 +386,16 @@ fun TvGuideGrid(
} else {
val currentChannel = channels[item.row].id
val currentProgram =
programs[currentChannel]?.get(item.column)
programs.programsByChannel[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)
onClickProgram.invoke(
focusedProgramIndex,
currentProgram,
)
}
null
}
@ -396,7 +407,7 @@ fun TvGuideGrid(
if (newFocusedItem != null) {
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
val toFocus =
newFocusedItem
@ -424,7 +435,12 @@ fun TvGuideGrid(
},
) {
Surface(
colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary.copy(alpha = .25f)),
colors =
SurfaceDefaults.colors(
MaterialTheme.colorScheme.tertiary.copy(
alpha = .25f,
),
),
modifier = Modifier,
) {
// Empty
@ -465,16 +481,15 @@ fun TvGuideGrid(
)
}
}
programs(
count = programList.size,
count = programs.programs.size,
layoutInfo = { programIndex ->
val program = programList[programIndex]
val program = programs.programs[programIndex]
val channelIndex = channels.indexOfFirst { it.id == program.channelId }
ProgramGuideItem.Program(channelIndex, program.startHours, program.endHours)
},
) { programIndex ->
val program = programList[programIndex]
val program = programs.programs[programIndex]
val focused =
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
val background =