mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
WIP
This commit is contained in:
parent
b288f0f100
commit
841b4a0907
2 changed files with 85 additions and 13 deletions
|
|
@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
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.ItemSortBy
|
||||||
import org.jellyfin.sdk.model.api.TimerInfoDto
|
import org.jellyfin.sdk.model.api.TimerInfoDto
|
||||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
||||||
import org.jellyfin.sdk.model.api.request.GetLiveTvProgramsRequest
|
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
@ -39,7 +39,7 @@ import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
const val MAX_HOURS = 48L
|
const val MAX_HOURS = 3L
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LiveTvViewModel
|
class LiveTvViewModel
|
||||||
|
|
@ -56,12 +56,18 @@ class LiveTvViewModel
|
||||||
private val mutex = Mutex()
|
private val mutex = Mutex()
|
||||||
|
|
||||||
val channels = MutableLiveData<List<TvChannel>>()
|
val channels = MutableLiveData<List<TvChannel>>()
|
||||||
|
val channelProgramCount = mutableMapOf<UUID, Int>()
|
||||||
val programs = MutableLiveData<List<TvProgram>>()
|
val programs = MutableLiveData<List<TvProgram>>()
|
||||||
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)
|
||||||
|
|
||||||
|
val offset = MutableLiveData(0)
|
||||||
|
val programOffset = MutableLiveData(0)
|
||||||
|
private val range = 10
|
||||||
|
private var currentIndex = 0
|
||||||
|
|
||||||
fun init(firstLoad: Boolean) {
|
fun init(firstLoad: Boolean) {
|
||||||
start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
start = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
||||||
if (!firstLoad) {
|
if (!firstLoad) {
|
||||||
|
|
@ -86,7 +92,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)
|
fetchPrograms(channels.subList(0, range.coerceAtMost(channels.size)))
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@LiveTvViewModel.channels.value = channels
|
this@LiveTvViewModel.channels.value = channels
|
||||||
|
|
@ -105,9 +111,9 @@ 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 request =
|
val request =
|
||||||
GetLiveTvProgramsRequest(
|
GetProgramsDto(
|
||||||
maxStartDate = maxStartDate,
|
maxStartDate = maxStartDate,
|
||||||
minEndDate = minEndDate,
|
minEndDate = minEndDate,
|
||||||
channelIds = channels.map { it.id },
|
channelIds = channels.map { it.id },
|
||||||
|
|
@ -115,7 +121,7 @@ class LiveTvViewModel
|
||||||
)
|
)
|
||||||
val programs =
|
val programs =
|
||||||
api.liveTvApi
|
api.liveTvApi
|
||||||
.getLiveTvPrograms(request)
|
.getPrograms(request)
|
||||||
.content.items
|
.content.items
|
||||||
.map { dto ->
|
.map { dto ->
|
||||||
val category =
|
val category =
|
||||||
|
|
@ -182,6 +188,9 @@ class LiveTvViewModel
|
||||||
fake.addAll(fakePrograms)
|
fake.addAll(fakePrograms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finalProgramsByChannel.forEach { (channelId, programs) ->
|
||||||
|
channelProgramCount[channelId] = programs.size
|
||||||
|
}
|
||||||
val finalProgramList =
|
val finalProgramList =
|
||||||
(programs + fake).sortedWith(
|
(programs + fake).sortedWith(
|
||||||
compareBy(
|
compareBy(
|
||||||
|
|
@ -300,6 +309,34 @@ class LiveTvViewModel
|
||||||
}
|
}
|
||||||
loading.setValueOnMain(LoadingState.Success)
|
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..<rangeEnd)) {
|
||||||
|
val fetchStart = (absoluteIndex - range).coerceAtLeast(0)
|
||||||
|
val fetchEnd = (absoluteIndex + range).coerceAtMost(channels.size)
|
||||||
|
Timber.v("Loading more programs for channels $fetchStart=>$fetchEnd")
|
||||||
|
return viewModelScope.launchIO {
|
||||||
|
fetchPrograms(channels.subList(fetchStart, fetchEnd))
|
||||||
|
val programOffset =
|
||||||
|
(offset..<fetchStart).sumOf { channelProgramCount[channels[it].id]!! }
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@LiveTvViewModel.offset.value = fetchStart
|
||||||
|
currentIndex = index
|
||||||
|
this@LiveTvViewModel.programOffset.value = programOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,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)
|
||||||
when (val state = loading) {
|
when (val state = loading) {
|
||||||
is LoadingState.Error -> ErrorMessage(state, modifier)
|
is LoadingState.Error -> ErrorMessage(state, modifier)
|
||||||
LoadingState.Pending,
|
LoadingState.Pending,
|
||||||
|
|
@ -105,7 +106,9 @@ fun TvGuideGrid(
|
||||||
channels = channels,
|
channels = channels,
|
||||||
programList = programs,
|
programList = programs,
|
||||||
programs = programsByChannel,
|
programs = programsByChannel,
|
||||||
|
channelProgramCount = viewModel.channelProgramCount,
|
||||||
start = viewModel.start,
|
start = viewModel.start,
|
||||||
|
programOffset = programOffset,
|
||||||
onClickChannel = { index, channel ->
|
onClickChannel = { index, channel ->
|
||||||
viewModel.navigationManager.navigateTo(
|
viewModel.navigationManager.navigateTo(
|
||||||
Destination.Playback(
|
Destination.Playback(
|
||||||
|
|
@ -114,6 +117,9 @@ fun TvGuideGrid(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onFocusChannel = { index ->
|
||||||
|
viewModel.onFocusChannel(index)
|
||||||
|
},
|
||||||
onClickProgram = { index, program ->
|
onClickProgram = { index, program ->
|
||||||
if (program.isFake) {
|
if (program.isFake) {
|
||||||
val now = LocalDateTime.now()
|
val now = LocalDateTime.now()
|
||||||
|
|
@ -194,9 +200,12 @@ fun TvGuideGrid(
|
||||||
channels: List<TvChannel>,
|
channels: List<TvChannel>,
|
||||||
programList: List<TvProgram>,
|
programList: List<TvProgram>,
|
||||||
programs: Map<UUID, List<TvProgram>>,
|
programs: Map<UUID, List<TvProgram>>,
|
||||||
|
channelProgramCount: Map<UUID, Int>,
|
||||||
start: LocalDateTime,
|
start: LocalDateTime,
|
||||||
onClickChannel: (Int, TvChannel) -> Unit,
|
onClickChannel: (Int, TvChannel) -> Unit,
|
||||||
onClickProgram: (Int, TvProgram) -> Unit,
|
onClickProgram: (Int, TvProgram) -> Unit,
|
||||||
|
onFocusChannel: (Int) -> Unit,
|
||||||
|
programOffset: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
@ -223,13 +232,25 @@ fun TvGuideGrid(
|
||||||
.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 { programs[it]?.size ?: 0 }
|
.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 gridHasFocus by rememberSaveable { mutableStateOf(false) }
|
||||||
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
|
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -247,6 +268,9 @@ fun TvGuideGrid(
|
||||||
if (it.type == KeyEventType.KeyUp) {
|
if (it.type == KeyEventType.KeyUp) {
|
||||||
return@onPreviewKeyEvent false
|
return@onPreviewKeyEvent false
|
||||||
}
|
}
|
||||||
|
if (!movementEnabled) {
|
||||||
|
return@onPreviewKeyEvent true
|
||||||
|
}
|
||||||
val newIndex =
|
val newIndex =
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
Key.Back -> {
|
Key.Back -> {
|
||||||
|
|
@ -328,7 +352,10 @@ fun TvGuideGrid(
|
||||||
if (pIndex >= 0) {
|
if (pIndex >= 0) {
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
||||||
} else {
|
} else {
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
|
programsBeforeChannel.get(focusedChannelIndex) + (
|
||||||
|
programs[channelId]?.size
|
||||||
|
?: 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -371,7 +398,10 @@ fun TvGuideGrid(
|
||||||
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
||||||
} else {
|
} else {
|
||||||
// Did not find one, so focus on the final program in the list
|
// 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) {
|
if (newIndex >= 0) {
|
||||||
val before = programsBeforeChannel.get(focusedChannelIndex)
|
val before = programsBeforeChannel.get(focusedChannelIndex)
|
||||||
val max =
|
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)
|
val index = newIndex.coerceIn(before, max)
|
||||||
|
Timber.v("move: programIndex=$index, channelIndex=$focusedChannelIndex")
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
focusedProgramIndex = index
|
focusedProgramIndex = index
|
||||||
state.animateToProgram(index, Alignment.Center)
|
state.animateToProgram(index, Alignment.Center)
|
||||||
|
onFocusChannel.invoke(focusedChannelIndex)
|
||||||
}
|
}
|
||||||
return@onPreviewKeyEvent true
|
return@onPreviewKeyEvent true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue