mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Reimplement guide movement & program fetching
This commit is contained in:
parent
841b4a0907
commit
1ecbc37a07
2 changed files with 278 additions and 258 deletions
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
|
import com.github.damontecres.wholphin.ui.data.RowColumn
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
|
@ -65,8 +66,9 @@ class LiveTvViewModel
|
||||||
|
|
||||||
val offset = MutableLiveData(0)
|
val offset = MutableLiveData(0)
|
||||||
val programOffset = MutableLiveData(0)
|
val programOffset = MutableLiveData(0)
|
||||||
private val range = 10
|
private val range = 8
|
||||||
private var currentIndex = 0
|
private var currentIndex = 0
|
||||||
|
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)
|
||||||
|
|
@ -92,7 +94,7 @@ class LiveTvViewModel
|
||||||
Timber.d("Got ${channels.size} channels")
|
Timber.d("Got ${channels.size} channels")
|
||||||
channelsIdToIndex =
|
channelsIdToIndex =
|
||||||
channels.withIndex().associateBy({ it.value.id }, { it.index })
|
channels.withIndex().associateBy({ it.value.id }, { it.index })
|
||||||
fetchPrograms(channels.subList(0, range.coerceAtMost(channels.size)))
|
fetchPrograms(channels, 0..<range.coerceAtMost(channels.size))
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@LiveTvViewModel.channels.value = channels
|
this@LiveTvViewModel.channels.value = channels
|
||||||
|
|
@ -103,107 +105,110 @@ class LiveTvViewModel
|
||||||
|
|
||||||
private suspend fun fetchProgramsWithLoading(channels: List<TvChannel>) {
|
private suspend fun fetchProgramsWithLoading(channels: List<TvChannel>) {
|
||||||
loading.setValueOnMain(LoadingState.Loading)
|
loading.setValueOnMain(LoadingState.Loading)
|
||||||
fetchPrograms(channels)
|
fetchPrograms(channels, fetchedRange.value!!)
|
||||||
loading.setValueOnMain(LoadingState.Success)
|
loading.setValueOnMain(LoadingState.Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchPrograms(channels: List<TvChannel>) =
|
private suspend fun fetchPrograms(
|
||||||
mutex.withLock {
|
channels: List<TvChannel>,
|
||||||
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
|
range: IntRange,
|
||||||
val minEndDate = start.plusMinutes(1L)
|
) = mutex.withLock {
|
||||||
Timber.v("Fetching programs for ${channels.size} channels")
|
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
|
||||||
val request =
|
val minEndDate = start.plusMinutes(1L)
|
||||||
GetProgramsDto(
|
Timber.v("Fetching programs for ${channels.size} channels")
|
||||||
maxStartDate = maxStartDate,
|
val request =
|
||||||
minEndDate = minEndDate,
|
GetProgramsDto(
|
||||||
channelIds = channels.map { it.id },
|
maxStartDate = maxStartDate,
|
||||||
sortBy = listOf(ItemSortBy.START_DATE),
|
minEndDate = minEndDate,
|
||||||
)
|
channelIds = channels.map { it.id },
|
||||||
val programs =
|
sortBy = listOf(ItemSortBy.START_DATE),
|
||||||
api.liveTvApi
|
)
|
||||||
.getPrograms(request)
|
val programs =
|
||||||
.content.items
|
api.liveTvApi
|
||||||
.map { dto ->
|
.getPrograms(request)
|
||||||
val category =
|
.content.items
|
||||||
if (dto.isKids ?: false) {
|
.map { dto ->
|
||||||
ProgramCategory.KIDS
|
val category =
|
||||||
} else if (dto.isMovie ?: false) {
|
if (dto.isKids ?: false) {
|
||||||
ProgramCategory.MOVIE
|
ProgramCategory.KIDS
|
||||||
} else if (dto.isNews ?: false) {
|
} else if (dto.isMovie ?: false) {
|
||||||
ProgramCategory.NEWS
|
ProgramCategory.MOVIE
|
||||||
} else if (dto.isSports ?: false) {
|
} else if (dto.isNews ?: false) {
|
||||||
ProgramCategory.SPORTS
|
ProgramCategory.NEWS
|
||||||
|
} else if (dto.isSports ?: false) {
|
||||||
|
ProgramCategory.SPORTS
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
TvProgram(
|
||||||
|
id = dto.id,
|
||||||
|
channelId = dto.channelId!!,
|
||||||
|
start = dto.startDate!!,
|
||||||
|
end = dto.endDate!!,
|
||||||
|
startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f),
|
||||||
|
endHours = hoursBetween(start, dto.endDate!!),
|
||||||
|
duration = dto.runTimeTicks!!.ticks,
|
||||||
|
name = dto.seriesName ?: dto.name,
|
||||||
|
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
||||||
|
seasonEpisode =
|
||||||
|
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
|
||||||
|
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
},
|
||||||
TvProgram(
|
isRecording = dto.timerId.isNotNullOrBlank(),
|
||||||
id = dto.id,
|
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
|
||||||
channelId = dto.channelId!!,
|
isRepeat = dto.isRepeat ?: false,
|
||||||
start = dto.startDate!!,
|
category = category,
|
||||||
end = dto.endDate!!,
|
)
|
||||||
startHours = hoursBetween(start, dto.startDate!!).coerceAtLeast(0f),
|
}
|
||||||
endHours = hoursBetween(start, dto.endDate!!),
|
|
||||||
duration = dto.runTimeTicks!!.ticks,
|
val programsByChannel = programs.groupBy { it.channelId }
|
||||||
name = dto.seriesName ?: dto.name,
|
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
|
||||||
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
val fake = mutableListOf<TvProgram>()
|
||||||
seasonEpisode =
|
val finalProgramsByChannel =
|
||||||
if (dto.indexNumber != null && dto.parentIndexNumber != null) {
|
programsByChannel.toMutableMap().apply {
|
||||||
SeasonEpisode(dto.parentIndexNumber!!, dto.indexNumber!!)
|
emptyChannels.forEach { channel ->
|
||||||
} else {
|
val fakePrograms =
|
||||||
null
|
(0..<MAX_HOURS).map {
|
||||||
},
|
TvProgram(
|
||||||
isRecording = dto.timerId.isNotNullOrBlank(),
|
id = UUID.randomUUID(), // TODO
|
||||||
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
|
channelId = channel.id,
|
||||||
isRepeat = dto.isRepeat ?: false,
|
start = start.plusHours(it),
|
||||||
category = category,
|
end = start.plusHours(it + 1),
|
||||||
)
|
startHours = it.toFloat(),
|
||||||
}
|
endHours = (it + 1).toFloat(),
|
||||||
|
duration = 60.seconds,
|
||||||
val programsByChannel = programs.groupBy { it.channelId }
|
name = "No data",
|
||||||
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
|
subtitle = null,
|
||||||
val fake = mutableListOf<TvProgram>()
|
seasonEpisode = null,
|
||||||
val finalProgramsByChannel =
|
isRecording = false,
|
||||||
programsByChannel.toMutableMap().apply {
|
isSeriesRecording = false,
|
||||||
emptyChannels.forEach { channel ->
|
isRepeat = false,
|
||||||
val fakePrograms =
|
category = ProgramCategory.FAKE,
|
||||||
(0..<MAX_HOURS).map {
|
)
|
||||||
TvProgram(
|
}
|
||||||
id = UUID.randomUUID(), // TODO
|
put(channel.id, fakePrograms)
|
||||||
channelId = channel.id,
|
fake.addAll(fakePrograms)
|
||||||
start = start.plusHours(it),
|
|
||||||
end = start.plusHours(it + 1),
|
|
||||||
startHours = it.toFloat(),
|
|
||||||
endHours = (it + 1).toFloat(),
|
|
||||||
duration = 60.seconds,
|
|
||||||
name = "No data",
|
|
||||||
subtitle = null,
|
|
||||||
seasonEpisode = null,
|
|
||||||
isRecording = false,
|
|
||||||
isSeriesRecording = false,
|
|
||||||
isRepeat = false,
|
|
||||||
category = ProgramCategory.FAKE,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
put(channel.id, fakePrograms)
|
|
||||||
fake.addAll(fakePrograms)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finalProgramsByChannel.forEach { (channelId, programs) ->
|
|
||||||
channelProgramCount[channelId] = programs.size
|
|
||||||
}
|
|
||||||
val finalProgramList =
|
|
||||||
(programs + fake).sortedWith(
|
|
||||||
compareBy(
|
|
||||||
{ channelsIdToIndex[it.channelId]!! },
|
|
||||||
{ it.start },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
this@LiveTvViewModel.programs.value = finalProgramList
|
|
||||||
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
|
|
||||||
}
|
}
|
||||||
|
finalProgramsByChannel.forEach { (channelId, programs) ->
|
||||||
|
channelProgramCount[channelId] = programs.size
|
||||||
}
|
}
|
||||||
|
val finalProgramList =
|
||||||
|
(programs + fake).sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ channelsIdToIndex[it.channelId]!! },
|
||||||
|
{ it.start },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@LiveTvViewModel.programs.value = finalProgramList
|
||||||
|
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
|
||||||
|
this@LiveTvViewModel.fetchedRange.value = range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getItem(programId: UUID) {
|
fun getItem(programId: UUID) {
|
||||||
fetchingItem.value = LoadingState.Loading
|
fetchingItem.value = LoadingState.Loading
|
||||||
|
|
@ -310,28 +315,32 @@ class LiveTvViewModel
|
||||||
loading.setValueOnMain(LoadingState.Success)
|
loading.setValueOnMain(LoadingState.Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onFocusChannel(index: Int): Job? {
|
fun onFocusChannel(position: RowColumn): Job? {
|
||||||
return channels.value?.let { channels ->
|
return channels.value?.let { channels ->
|
||||||
val offset = offset.value!!
|
val fetchedRange = fetchedRange.value!!
|
||||||
val absoluteIndex = offset + index
|
val quarter = (fetchedRange.last - fetchedRange.start) / 4
|
||||||
val rangeStart = (currentIndex - range / 2).coerceAtLeast(0)
|
val rangeStart = fetchedRange.start + quarter
|
||||||
val rangeEnd = (currentIndex + range / 2).coerceAtMost(channels.size)
|
val rangeEnd = fetchedRange.last - quarter
|
||||||
|
val testRange = rangeStart..<rangeEnd
|
||||||
|
|
||||||
Timber.v(
|
Timber.v(
|
||||||
"onFocusChannel: index=$index, currentIndex=$currentIndex, offset=$offset, rangeStart=$rangeStart, rangeEnd=$rangeEnd",
|
"onFocusChannel: position=$position, fetchedRange=$fetchedRange, testRange=$testRange",
|
||||||
)
|
)
|
||||||
if (absoluteIndex !in (rangeStart..<rangeEnd)) {
|
val fetchStart = (position.row - range).coerceAtLeast(0)
|
||||||
val fetchStart = (absoluteIndex - range).coerceAtLeast(0)
|
val fetchEnd = (position.row + range).coerceAtMost(channels.size)
|
||||||
val fetchEnd = (absoluteIndex + range).coerceAtMost(channels.size)
|
val newFetchRange = fetchStart..<fetchEnd
|
||||||
Timber.v("Loading more programs for channels $fetchStart=>$fetchEnd")
|
// If current channel is not within +/- range
|
||||||
|
// And the potential new fetch range is not wholly within the current (eg not near the top or bottom)
|
||||||
|
// Fetch new data
|
||||||
|
if (position.row !in testRange && !newFetchRange.within(fetchedRange)) {
|
||||||
|
Timber.v("Loading more programs for channels $newFetchRange")
|
||||||
return viewModelScope.launchIO {
|
return viewModelScope.launchIO {
|
||||||
fetchPrograms(channels.subList(fetchStart, fetchEnd))
|
fetchPrograms(channels, newFetchRange)
|
||||||
val programOffset =
|
// withContext(Dispatchers.Main) {
|
||||||
(offset..<fetchStart).sumOf { channelProgramCount[channels[it].id]!! }
|
// this@LiveTvViewModel.offset.value = fetchStart
|
||||||
withContext(Dispatchers.Main) {
|
// currentIndex = index
|
||||||
this@LiveTvViewModel.offset.value = fetchStart
|
// this@LiveTvViewModel.programOffset.value = programOffset
|
||||||
currentIndex = index
|
// }
|
||||||
this@LiveTvViewModel.programOffset.value = programOffset
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
|
|
@ -339,6 +348,8 @@ class LiveTvViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun IntRange.within(other: IntRange): Boolean = this.first >= other.first && this.last <= other.last
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the number of hours between two [LocalDateTime]
|
* Returns the number of hours between two [LocalDateTime]
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -48,14 +48,13 @@ import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
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.enableMarquee
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
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
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import com.google.common.cache.CacheBuilder
|
|
||||||
import com.google.common.cache.CacheLoader
|
|
||||||
import eu.wewox.programguide.ProgramGuide
|
import eu.wewox.programguide.ProgramGuide
|
||||||
import eu.wewox.programguide.ProgramGuideDimensions
|
import eu.wewox.programguide.ProgramGuideDimensions
|
||||||
import eu.wewox.programguide.ProgramGuideItem
|
import eu.wewox.programguide.ProgramGuideItem
|
||||||
|
|
@ -81,7 +80,7 @@ fun TvGuideGrid(
|
||||||
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(listOf())
|
||||||
val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
|
val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
|
||||||
val programOffset by viewModel.programOffset.observeAsState(0)
|
val channelOffset by viewModel.offset.observeAsState(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,
|
||||||
|
|
@ -108,7 +107,7 @@ fun TvGuideGrid(
|
||||||
programs = programsByChannel,
|
programs = programsByChannel,
|
||||||
channelProgramCount = viewModel.channelProgramCount,
|
channelProgramCount = viewModel.channelProgramCount,
|
||||||
start = viewModel.start,
|
start = viewModel.start,
|
||||||
programOffset = programOffset,
|
channelOffset = channelOffset,
|
||||||
onClickChannel = { index, channel ->
|
onClickChannel = { index, channel ->
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
|
|
@ -117,9 +116,7 @@ fun TvGuideGrid(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onFocusChannel = { index ->
|
onFocus = viewModel::onFocusChannel,
|
||||||
viewModel.onFocusChannel(index)
|
|
||||||
},
|
|
||||||
onClickProgram = { index, program ->
|
onClickProgram = { index, program ->
|
||||||
if (program.isFake) {
|
if (program.isFake) {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
|
|
@ -204,17 +201,33 @@ fun TvGuideGrid(
|
||||||
start: LocalDateTime,
|
start: LocalDateTime,
|
||||||
onClickChannel: (Int, TvChannel) -> Unit,
|
onClickChannel: (Int, TvChannel) -> Unit,
|
||||||
onClickProgram: (Int, TvProgram) -> Unit,
|
onClickProgram: (Int, TvProgram) -> Unit,
|
||||||
onFocusChannel: (Int) -> Unit,
|
onFocus: (RowColumn) -> Unit,
|
||||||
programOffset: Int,
|
channelOffset: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val state = rememberSaveableProgramGuideState()
|
val state = rememberSaveableProgramGuideState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var focusedProgramIndex by rememberInt(0)
|
// var focusedProgramIndex by rememberInt(0)
|
||||||
var focusedChannelIndex by rememberInt(0)
|
// var focusedChannelIndex by rememberInt(0)
|
||||||
|
|
||||||
|
var focusedItem by rememberPosition(RowColumn(0, 0))
|
||||||
|
val focusedChannelIndex = focusedItem.row
|
||||||
|
val focusedProgramIndex =
|
||||||
|
remember(channelOffset, focusedItem) {
|
||||||
|
focusedItem.let { focus ->
|
||||||
|
(channelOffset..<focus.row).sumOf { channelProgramCount[channels[it].id]!! } + focus.column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(focusedProgramIndex) {
|
||||||
|
val (channelIndex, programIndex) = focusedItem
|
||||||
|
Timber.v("Focusing on $focusedItem, programIndex=$focusedProgramIndex")
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
state.animateToProgram(focusedProgramIndex, Alignment.Center)
|
||||||
|
}
|
||||||
|
}
|
||||||
val dimensions =
|
val dimensions =
|
||||||
ProgramGuideDimensions(
|
ProgramGuideDimensions(
|
||||||
timelineHourWidth = 240.dp,
|
timelineHourWidth = 240.dp,
|
||||||
|
|
@ -224,33 +237,33 @@ fun TvGuideGrid(
|
||||||
currentTimeWidth = 2.dp,
|
currentTimeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
val programsBeforeChannel =
|
// val programsBeforeChannel =
|
||||||
remember {
|
// remember {
|
||||||
CacheBuilder
|
// CacheBuilder
|
||||||
.newBuilder()
|
// .newBuilder()
|
||||||
.maximumSize(200)
|
// .maximumSize(200)
|
||||||
.build<Int, Int>(
|
// .build<Int, Int>(
|
||||||
object : CacheLoader<Int, Int>() {
|
// object : CacheLoader<Int, Int>() {
|
||||||
override fun load(key: Int): Int =
|
// override fun load(key: Int): Int =
|
||||||
(
|
// (
|
||||||
channels
|
// channels
|
||||||
.subList(0, key)
|
// .subList(0, key)
|
||||||
.map { it.id }
|
// .map { it.id }
|
||||||
.sumOf {
|
// .sumOf {
|
||||||
channelProgramCount[it] ?: 0
|
// channelProgramCount[it] ?: 0
|
||||||
} - programOffset
|
// } - programOffset
|
||||||
).coerceAtLeast(0)
|
// ).coerceAtLeast(0)
|
||||||
},
|
// },
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
var movementEnabled by remember { mutableStateOf(true) }
|
var movementEnabled by remember { mutableStateOf(true) }
|
||||||
LaunchedEffect(programOffset) {
|
// LaunchedEffect(programOffset) {
|
||||||
movementEnabled = false
|
// movementEnabled = false
|
||||||
programsBeforeChannel.invalidateAll()
|
// programsBeforeChannel.invalidateAll()
|
||||||
Timber.v("Adjusting focusedProgramIndex, $focusedProgramIndex - $programOffset")
|
// Timber.v("Adjusting focusedProgramIndex, $focusedProgramIndex - $programOffset")
|
||||||
focusedProgramIndex = (focusedProgramIndex - programOffset).coerceAtLeast(0)
|
// focusedProgramIndex = (focusedProgramIndex - programOffset).coerceAtLeast(0)
|
||||||
movementEnabled = true
|
// movementEnabled = true
|
||||||
}
|
// }
|
||||||
|
|
||||||
var gridHasFocus by rememberSaveable { mutableStateOf(false) }
|
var gridHasFocus by rememberSaveable { mutableStateOf(false) }
|
||||||
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
|
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -271,13 +284,13 @@ fun TvGuideGrid(
|
||||||
if (!movementEnabled) {
|
if (!movementEnabled) {
|
||||||
return@onPreviewKeyEvent true
|
return@onPreviewKeyEvent true
|
||||||
}
|
}
|
||||||
val newIndex =
|
val item = focusedItem
|
||||||
|
val newFocusedItem =
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
Key.Back -> {
|
Key.Back -> {
|
||||||
val pos = programsBeforeChannel.get(focusedChannelIndex)
|
if (item.column > 0) {
|
||||||
if (focusedProgramIndex - pos > 0) {
|
|
||||||
// Not at beginning of row, so move to beginning
|
// Not at beginning of row, so move to beginning
|
||||||
pos
|
item.copy(column = 0)
|
||||||
} else {
|
} else {
|
||||||
// At beginning, so allow normal back button behavior
|
// At beginning, so allow normal back button behavior
|
||||||
return@onPreviewKeyEvent false
|
return@onPreviewKeyEvent false
|
||||||
|
|
@ -287,20 +300,9 @@ fun TvGuideGrid(
|
||||||
Key.DirectionRight -> {
|
Key.DirectionRight -> {
|
||||||
if (channelColumnFocused) {
|
if (channelColumnFocused) {
|
||||||
channelColumnFocused = false
|
channelColumnFocused = false
|
||||||
focusedProgramIndex
|
item.copy(column = 0)
|
||||||
} else {
|
} else {
|
||||||
val nextProgramIndex = focusedProgramIndex + 1
|
item.copy(column = item.column + 1)
|
||||||
val programsBefore =
|
|
||||||
programsBeforeChannel.get(focusedChannelIndex)
|
|
||||||
val relativePosition = nextProgramIndex - programsBefore
|
|
||||||
val channelPrograms =
|
|
||||||
programs[channels[focusedChannelIndex].id].orEmpty()
|
|
||||||
if (relativePosition >= channelPrograms.size) {
|
|
||||||
focusManager.moveFocus(FocusDirection.Right)
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
nextProgramIndex
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -308,54 +310,45 @@ fun TvGuideGrid(
|
||||||
if (channelColumnFocused) {
|
if (channelColumnFocused) {
|
||||||
focusManager.moveFocus(FocusDirection.Left)
|
focusManager.moveFocus(FocusDirection.Left)
|
||||||
null
|
null
|
||||||
|
} else if (item.column == 0) {
|
||||||
|
channelColumnFocused = true
|
||||||
|
item
|
||||||
} else {
|
} else {
|
||||||
val nextProgramIndex = focusedProgramIndex - 1
|
item.copy(column = item.column - 1)
|
||||||
val programsBefore =
|
|
||||||
programsBeforeChannel.get(focusedChannelIndex)
|
|
||||||
val relativePosition = nextProgramIndex - programsBefore
|
|
||||||
// val channelPrograms =
|
|
||||||
// programs[channels[focusedChannel].id].orEmpty()
|
|
||||||
if (relativePosition >= 0) {
|
|
||||||
nextProgramIndex
|
|
||||||
} else if (relativePosition == CHANNEL_COLUMN) {
|
|
||||||
channelColumnFocused = true
|
|
||||||
focusedProgramIndex
|
|
||||||
} else {
|
|
||||||
Timber.w("Move left to relativePosition=$relativePosition should not occur")
|
|
||||||
focusManager.moveFocus(FocusDirection.Left)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Key.DirectionUp -> {
|
Key.DirectionUp -> {
|
||||||
val newFocusedChannelIndex = (focusedChannelIndex - 1)
|
if (item.row <= 0) {
|
||||||
// Timber.v("newFocusedChannelIndex=$newFocusedChannelIndex")
|
|
||||||
if (newFocusedChannelIndex < 0) {
|
|
||||||
focusManager.moveFocus(FocusDirection.Up)
|
focusManager.moveFocus(FocusDirection.Up)
|
||||||
null
|
null
|
||||||
} else if (channelColumnFocused) {
|
|
||||||
focusedChannelIndex = newFocusedChannelIndex
|
|
||||||
programsBeforeChannel.get(focusedChannelIndex)
|
|
||||||
} else {
|
} else {
|
||||||
val start = programList[focusedProgramIndex].startHours
|
val newChannelIndex = item.row - 1
|
||||||
focusedChannelIndex =
|
if (channelColumnFocused) {
|
||||||
newFocusedChannelIndex.coerceAtLeast(0)
|
RowColumn(newChannelIndex, 0)
|
||||||
val channelId = channels[focusedChannelIndex].id
|
|
||||||
val pro = programs[channelId].orEmpty()
|
|
||||||
val pIndex =
|
|
||||||
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
|
|
||||||
if (pIndex >= 0) {
|
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
|
||||||
} else {
|
} else {
|
||||||
val pIndex = pro.indexOfFirst { it.startHours >= start }
|
val currentChannel = channels[item.row].id
|
||||||
if (pIndex >= 0) {
|
val currentProgram =
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
programs[currentChannel]?.get(item.column)
|
||||||
|
if (currentProgram == null) {
|
||||||
|
item
|
||||||
} else {
|
} else {
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + (
|
val start = currentProgram.startHours
|
||||||
programs[channelId]?.size
|
val newChannelPrograms =
|
||||||
?: 0
|
programs[channels[newChannelIndex].id].orEmpty()
|
||||||
)
|
val pIndex =
|
||||||
|
newChannelPrograms.indexOfFirst { start in (it.startHours..<it.endHours) }
|
||||||
|
if (pIndex >= 0) {
|
||||||
|
RowColumn(newChannelIndex, pIndex)
|
||||||
|
} else {
|
||||||
|
val pIndex =
|
||||||
|
newChannelPrograms.indexOfFirst { it.startHours >= start }
|
||||||
|
if (pIndex >= 0) {
|
||||||
|
RowColumn(newChannelIndex, pIndex)
|
||||||
|
} else {
|
||||||
|
RowColumn(newChannelIndex, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -363,45 +356,49 @@ fun TvGuideGrid(
|
||||||
|
|
||||||
Key.DirectionDown -> {
|
Key.DirectionDown -> {
|
||||||
// Move channel focus down
|
// Move channel focus down
|
||||||
val newFocusedChannelIndex = (focusedChannelIndex + 1)
|
val newChannelIndex = item.row + 1
|
||||||
if (newFocusedChannelIndex >= channels.size) {
|
if (newChannelIndex >= channels.size) {
|
||||||
// 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 if (channelColumnFocused) {
|
||||||
// If focused on the channel column, move down a channel
|
// If focused on the channel column, move down a channel
|
||||||
focusedChannelIndex =
|
RowColumn(newChannelIndex, 0)
|
||||||
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
|
|
||||||
programsBeforeChannel.get(focusedChannelIndex)
|
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, moving to a new row
|
// Otherwise, moving to a new row
|
||||||
focusedChannelIndex =
|
// Get current program & its start time
|
||||||
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
|
val currentChannel = channels[item.row].id
|
||||||
// Get the new row/channel's programs
|
val currentProgram =
|
||||||
val channelId = channels[focusedChannelIndex].id
|
programs[currentChannel]?.get(item.column)
|
||||||
val pro = programs[channelId].orEmpty()
|
if (currentProgram == null) {
|
||||||
// When the currently focused program starts
|
// Data is loading in the background
|
||||||
val start = programList[focusedProgramIndex].startHours
|
item
|
||||||
// Find the first program where the start time (of the previously focused program) is in the middle of a program
|
|
||||||
val pIndex =
|
|
||||||
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
|
|
||||||
if (pIndex >= 0) {
|
|
||||||
// Found one, so focus on it
|
|
||||||
// Get the sum of all of the previous channels' program sizes, plus the index found to convert a relative program index into absolute
|
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
|
||||||
} else {
|
} else {
|
||||||
// Didn't find one, probably due to missing data
|
val start = currentProgram.startHours
|
||||||
// So now first the first one that starts after the previously focused program
|
// Get the new row/channel's programs
|
||||||
val pIndex = pro.indexOfFirst { it.startHours >= start }
|
val newChannelPrograms =
|
||||||
|
programs[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) {
|
if (pIndex >= 0) {
|
||||||
// Found one, so focus on it
|
// Found one, so focus on it
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
RowColumn(newChannelIndex, pIndex)
|
||||||
} else {
|
} else {
|
||||||
// Did not find one, so focus on the final program in the list
|
// Didn't find one, probably due to missing data
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + (
|
// So now first the first one that starts after the previously focused program
|
||||||
programs[channelId]?.size
|
val pIndex =
|
||||||
?: 0
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -413,9 +410,15 @@ fun TvGuideGrid(
|
||||||
Timber.v("Clicked on %s", channel)
|
Timber.v("Clicked on %s", channel)
|
||||||
onClickChannel.invoke(focusedChannelIndex, channel)
|
onClickChannel.invoke(focusedChannelIndex, channel)
|
||||||
} else {
|
} else {
|
||||||
val program = programList[focusedProgramIndex]
|
val currentChannel = channels[item.row].id
|
||||||
Timber.v("Clicked on %s", program)
|
val currentProgram =
|
||||||
onClickProgram.invoke(focusedProgramIndex, program)
|
programs[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)
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -424,28 +427,34 @@ fun TvGuideGrid(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newIndex != null) {
|
|
||||||
// Timber.v("newIndex=$newIndex")
|
if (newFocusedItem != null) {
|
||||||
if (newIndex >= 0) {
|
focusedItem = newFocusedItem
|
||||||
val before = programsBeforeChannel.get(focusedChannelIndex)
|
onFocus(newFocusedItem)
|
||||||
val max =
|
return@onPreviewKeyEvent true
|
||||||
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 {
|
|
||||||
Timber.w("newIndex is <0: $newIndex")
|
|
||||||
return@onPreviewKeyEvent true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// if (newIndex != null) {
|
||||||
|
// // Timber.v("newIndex=$newIndex")
|
||||||
|
// if (newIndex >= 0) {
|
||||||
|
// val before = programsBeforeChannel.get(focusedChannelIndex)
|
||||||
|
// val max =
|
||||||
|
// 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 {
|
||||||
|
// Timber.w("newIndex is <0: $newIndex")
|
||||||
|
// return@onPreviewKeyEvent true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
return@onPreviewKeyEvent false
|
return@onPreviewKeyEvent false
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue