diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index e26dd5da..c4b54405 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -29,7 +30,6 @@ import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.TimerInfoDto import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest -import org.jellyfin.sdk.model.api.request.GetLiveTvProgramsRequest import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber import java.time.LocalDateTime @@ -39,7 +39,7 @@ import javax.inject.Inject import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -const val MAX_HOURS = 48L +const val MAX_HOURS = 3L @HiltViewModel class LiveTvViewModel @@ -56,12 +56,18 @@ class LiveTvViewModel private val mutex = Mutex() val channels = MutableLiveData>() + val channelProgramCount = mutableMapOf() val programs = MutableLiveData>() val programsByChannel = MutableLiveData>>(mapOf()) val fetchingItem = MutableLiveData(LoadingState.Pending) val fetchedItem = MutableLiveData(null) + val offset = MutableLiveData(0) + val programOffset = MutableLiveData(0) + private val range = 10 + private var currentIndex = 0 + fun init(firstLoad: Boolean) { start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) if (!firstLoad) { @@ -86,7 +92,7 @@ class LiveTvViewModel Timber.d("Got ${channels.size} channels") channelsIdToIndex = channels.withIndex().associateBy({ it.value.id }, { it.index }) - fetchPrograms(channels) + fetchPrograms(channels.subList(0, range.coerceAtMost(channels.size))) withContext(Dispatchers.Main) { this@LiveTvViewModel.channels.value = channels @@ -105,9 +111,9 @@ 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 request = - GetLiveTvProgramsRequest( + GetProgramsDto( maxStartDate = maxStartDate, minEndDate = minEndDate, channelIds = channels.map { it.id }, @@ -115,7 +121,7 @@ class LiveTvViewModel ) val programs = api.liveTvApi - .getLiveTvPrograms(request) + .getPrograms(request) .content.items .map { dto -> val category = @@ -182,6 +188,9 @@ class LiveTvViewModel fake.addAll(fakePrograms) } } + finalProgramsByChannel.forEach { (channelId, programs) -> + channelProgramCount[channelId] = programs.size + } val finalProgramList = (programs + fake).sortedWith( compareBy( @@ -300,6 +309,34 @@ class LiveTvViewModel } loading.setValueOnMain(LoadingState.Success) } + + fun onFocusChannel(index: Int): Job? { + return channels.value?.let { channels -> + val offset = offset.value!! + val absoluteIndex = offset + index + val rangeStart = (currentIndex - range / 2).coerceAtLeast(0) + val rangeEnd = (currentIndex + range / 2).coerceAtMost(channels.size) + Timber.v( + "onFocusChannel: index=$index, currentIndex=$currentIndex, offset=$offset, rangeStart=$rangeStart, rangeEnd=$rangeEnd", + ) + if (absoluteIndex !in (rangeStart..$fetchEnd") + return viewModelScope.launchIO { + fetchPrograms(channels.subList(fetchStart, fetchEnd)) + val programOffset = + (offset.. ErrorMessage(state, modifier) LoadingState.Pending, @@ -105,7 +106,9 @@ fun TvGuideGrid( channels = channels, programList = programs, programs = programsByChannel, + channelProgramCount = viewModel.channelProgramCount, start = viewModel.start, + programOffset = programOffset, onClickChannel = { index, channel -> viewModel.navigationManager.navigateTo( Destination.Playback( @@ -114,6 +117,9 @@ fun TvGuideGrid( ), ) }, + onFocusChannel = { index -> + viewModel.onFocusChannel(index) + }, onClickProgram = { index, program -> if (program.isFake) { val now = LocalDateTime.now() @@ -194,9 +200,12 @@ fun TvGuideGrid( channels: List, programList: List, programs: Map>, + channelProgramCount: Map, start: LocalDateTime, onClickChannel: (Int, TvChannel) -> Unit, onClickProgram: (Int, TvProgram) -> Unit, + onFocusChannel: (Int) -> Unit, + programOffset: Int, modifier: Modifier = Modifier, ) { val context = LocalContext.current @@ -223,13 +232,25 @@ fun TvGuideGrid( .build( object : CacheLoader() { override fun load(key: Int): Int = - channels - .subList(0, key) - .map { it.id } - .sumOf { programs[it]?.size ?: 0 } + ( + channels + .subList(0, key) + .map { it.id } + .sumOf { + channelProgramCount[it] ?: 0 + } - programOffset + ).coerceAtLeast(0) }, ) } + var movementEnabled by remember { mutableStateOf(true) } + LaunchedEffect(programOffset) { + movementEnabled = false + programsBeforeChannel.invalidateAll() + Timber.v("Adjusting focusedProgramIndex, $focusedProgramIndex - $programOffset") + focusedProgramIndex = (focusedProgramIndex - programOffset).coerceAtLeast(0) + movementEnabled = true + } var gridHasFocus by rememberSaveable { mutableStateOf(false) } var channelColumnFocused by rememberSaveable { mutableStateOf(false) } @@ -247,6 +268,9 @@ fun TvGuideGrid( if (it.type == KeyEventType.KeyUp) { return@onPreviewKeyEvent false } + if (!movementEnabled) { + return@onPreviewKeyEvent true + } val newIndex = when (it.key) { Key.Back -> { @@ -328,7 +352,10 @@ fun TvGuideGrid( if (pIndex >= 0) { programsBeforeChannel.get(focusedChannelIndex) + pIndex } else { - programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size + programsBeforeChannel.get(focusedChannelIndex) + ( + programs[channelId]?.size + ?: 0 + ) } } } @@ -371,7 +398,10 @@ fun TvGuideGrid( programsBeforeChannel.get(focusedChannelIndex) + pIndex } else { // Did not find one, so focus on the final program in the list - programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size + programsBeforeChannel.get(focusedChannelIndex) + ( + programs[channelId]?.size + ?: 0 + ) } } } @@ -399,11 +429,16 @@ fun TvGuideGrid( if (newIndex >= 0) { val before = programsBeforeChannel.get(focusedChannelIndex) val max = - before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1 + 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 {