mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Minimal UI implementation
This commit is contained in:
parent
24763d6d8c
commit
8af376cf63
2 changed files with 301 additions and 107 deletions
|
|
@ -4,10 +4,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
|
||||||
import com.github.damontecres.wholphin.util.GetProgramsDtoHandler
|
import com.github.damontecres.wholphin.util.GetProgramsDtoHandler
|
||||||
import com.github.damontecres.wholphin.util.LazyList
|
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
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
|
||||||
|
|
@ -19,14 +16,19 @@ import org.jellyfin.sdk.api.client.extensions.imageApi
|
||||||
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||||
import org.jellyfin.sdk.model.api.GetProgramsDto
|
import org.jellyfin.sdk.model.api.GetProgramsDto
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
|
||||||
import org.jellyfin.sdk.model.extensions.ticks
|
import org.jellyfin.sdk.model.extensions.ticks
|
||||||
|
import timber.log.Timber
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.AbstractList
|
import java.time.ZoneOffset
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
const val MAX_HOURS = 24L
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LiveTvViewModel
|
class LiveTvViewModel
|
||||||
|
|
@ -36,9 +38,12 @@ class LiveTvViewModel
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
val start = LocalDateTime.now()
|
val start =
|
||||||
|
LocalDateTime.now().minusMinutes(30).truncatedTo(ChronoUnit.HOURS)
|
||||||
|
|
||||||
val channels = MutableLiveData<List<TvChannel>>()
|
val channels = MutableLiveData<List<TvChannel>>()
|
||||||
val programs = MutableLiveData<List<TvProgram?>>()
|
val programs = MutableLiveData<List<TvProgram>>()
|
||||||
|
val programsByChannel = MutableLiveData<Map<UUID, List<TvProgram>>>(mapOf())
|
||||||
|
|
||||||
private val programMap = mutableMapOf<UUID, MutableLiveData<List<TvProgram?>>>()
|
private val programMap = mutableMapOf<UUID, MutableLiveData<List<TvProgram?>>>()
|
||||||
|
|
||||||
|
|
@ -47,101 +52,180 @@ class LiveTvViewModel
|
||||||
viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Could not fetch channels")) {
|
viewModelScope.launch(Dispatchers.IO + LoadingExceptionHandler(loading, "Could not fetch channels")) {
|
||||||
val channelData by api.liveTvApi.getLiveTvChannels(
|
val channelData by api.liveTvApi.getLiveTvChannels(
|
||||||
GetLiveTvChannelsRequest(
|
GetLiveTvChannelsRequest(
|
||||||
startIndex = 8,
|
startIndex = 0,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val channels =
|
val channels =
|
||||||
LazyList(channelData.items) {
|
channelData.items
|
||||||
TvChannel(
|
.map {
|
||||||
it.id,
|
TvChannel(
|
||||||
it.channelNumber,
|
it.id,
|
||||||
it.channelName,
|
it.channelNumber,
|
||||||
api.imageApi.getItemImageUrl(it.id, ImageType.PRIMARY),
|
it.channelName,
|
||||||
)
|
api.imageApi.getItemImageUrl(it.id, ImageType.PRIMARY),
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
Timber.d("Got ${channels.size} channels")
|
||||||
|
val channelsIdToIndex =
|
||||||
|
channels.withIndex().associateBy({ it.value.id }, { it.index })
|
||||||
|
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
|
||||||
|
val minEndDate = start.plusMinutes(1L)
|
||||||
val request =
|
val request =
|
||||||
GetProgramsDto(
|
GetProgramsDto(
|
||||||
minStartDate = start,
|
maxStartDate = maxStartDate,
|
||||||
|
minEndDate = minEndDate,
|
||||||
|
// maxEndDate = start.plusHours(25),
|
||||||
channelIds = channels.map { it.id },
|
channelIds = channels.map { it.id },
|
||||||
|
sortBy = listOf(ItemSortBy.START_DATE),
|
||||||
)
|
)
|
||||||
val pager = ApiRequestPager(api, request, GetProgramsDtoHandler, viewModelScope).init()
|
// val pager = ApiRequestPager(api, request, GetProgramsDtoHandler, viewModelScope).init()
|
||||||
(0..<pager.size).forEach { pager.getBlocking(it) }
|
// (0..<pager.size).forEach { pager.getBlocking(it) }
|
||||||
val programs =
|
val programs =
|
||||||
LazyList(pager) { item ->
|
GetProgramsDtoHandler
|
||||||
item?.data?.let { dto ->
|
.execute(api, request)
|
||||||
|
.content.items
|
||||||
|
.map { dto ->
|
||||||
|
val utcStart = dto.startDate!!.toEpochSecond(ZoneOffset.UTC)
|
||||||
|
utcStart
|
||||||
|
val ttt =
|
||||||
|
java.time.Duration
|
||||||
|
.between(
|
||||||
|
start,
|
||||||
|
dto.startDate!!,
|
||||||
|
)
|
||||||
|
ttt
|
||||||
TvProgram(
|
TvProgram(
|
||||||
id = dto.id,
|
id = dto.id,
|
||||||
channelId = dto.channelId!!,
|
channelId = dto.channelId!!,
|
||||||
start = dto.startDate!!,
|
start = dto.startDate!!,
|
||||||
end = dto.endDate!!,
|
end = dto.endDate!!,
|
||||||
|
startHours =
|
||||||
|
java.time.Duration
|
||||||
|
.between(start, dto.startDate!!)
|
||||||
|
.seconds / (60f * 60f),
|
||||||
|
// dto.startDate!!.hours +
|
||||||
|
// (
|
||||||
|
// java.time.Duration
|
||||||
|
// .between(
|
||||||
|
// start,
|
||||||
|
// dto.startDate!!,
|
||||||
|
// ).toDaysPart() / 24
|
||||||
|
// ).coerceAtLeast(0),
|
||||||
|
endHours =
|
||||||
|
java.time.Duration
|
||||||
|
.between(start, dto.endDate!!)
|
||||||
|
.seconds / (60f * 60f),
|
||||||
|
// dto.endDate!!.hours +
|
||||||
|
// (
|
||||||
|
// java.time.Duration
|
||||||
|
// .between(
|
||||||
|
// start,
|
||||||
|
// dto.endDate!!,
|
||||||
|
// ).toHours() / 24
|
||||||
|
// ).coerceAtLeast(0),
|
||||||
duration = dto.runTimeTicks!!.ticks,
|
duration = dto.runTimeTicks!!.ticks,
|
||||||
name = dto.seriesName ?: dto.name,
|
name = dto.seriesName ?: dto.name,
|
||||||
subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() },
|
subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() },
|
||||||
seasonEpisode = null, // TODO
|
seasonEpisode = null, // TODO
|
||||||
)
|
)
|
||||||
|
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
|
||||||
|
|
||||||
|
val programsByChannel = programs.groupBy { it.channelId }
|
||||||
|
val emptyChannels = channels.filter { programsByChannel[it.id].orEmpty().isEmpty() }
|
||||||
|
val fake = mutableListOf<TvProgram>()
|
||||||
|
val finalProgramsByChannel =
|
||||||
|
programsByChannel.toMutableMap().apply {
|
||||||
|
emptyChannels.forEach { channel ->
|
||||||
|
val fakePrograms =
|
||||||
|
(0..<MAX_HOURS).map {
|
||||||
|
TvProgram(
|
||||||
|
id = UUID.randomUUID(), // TODO
|
||||||
|
channelId = channel.id,
|
||||||
|
start = start.plusHours(it),
|
||||||
|
end = start.plusHours(it + 1),
|
||||||
|
startHours = it.toFloat(),
|
||||||
|
endHours = (it + 1).toFloat(),
|
||||||
|
duration = 60.seconds,
|
||||||
|
name = "No date",
|
||||||
|
subtitle = null,
|
||||||
|
seasonEpisode = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
put(channel.id, fakePrograms)
|
||||||
|
fake.addAll(fakePrograms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val finalProgramList =
|
||||||
|
(programs + fake).sortedWith(
|
||||||
|
compareBy(
|
||||||
|
{ channelsIdToIndex[it.channelId]!! },
|
||||||
|
{ it.start },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Timber.d("Got ${programs.size} programs & ${fake.size} fake programs")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@LiveTvViewModel.channels.value = channels
|
this@LiveTvViewModel.channels.value = channels
|
||||||
this@LiveTvViewModel.programs.value = programs
|
this@LiveTvViewModel.programs.value = finalProgramList
|
||||||
|
this@LiveTvViewModel.programsByChannel.value = finalProgramsByChannel
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPrograms(channelId: UUID): MutableLiveData<List<TvProgram?>> =
|
// fun getPrograms(channelId: UUID): MutableLiveData<List<TvProgram?>> =
|
||||||
programMap.getOrPut(channelId) {
|
// programMap.getOrPut(channelId) {
|
||||||
val data = MutableLiveData<List<TvProgram?>>(listOf())
|
// val data = MutableLiveData<List<TvProgram?>>(listOf())
|
||||||
viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
// viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) {
|
||||||
val request =
|
// val request =
|
||||||
GetProgramsDto(
|
// GetProgramsDto(
|
||||||
minStartDate = start,
|
// minStartDate = start,
|
||||||
channelIds = listOf(channelId),
|
// channelIds = listOf(channelId),
|
||||||
)
|
// )
|
||||||
val pager = ApiRequestPager(api, request, GetProgramsDtoHandler, viewModelScope).init()
|
// val pager = ApiRequestPager(api, request, GetProgramsDtoHandler, viewModelScope).init()
|
||||||
val programList =
|
// val programList =
|
||||||
if (pager.isEmpty()) {
|
// if (pager.isEmpty()) {
|
||||||
object : AbstractList<TvProgram?>() {
|
// object : AbstractList<TvProgram?>() {
|
||||||
override fun get(index: Int): TvProgram? {
|
// override fun get(index: Int): TvProgram? {
|
||||||
val start = start.plusHours(index.toLong())
|
// val start = start.plusHours(index.toLong())
|
||||||
val end = start.plusHours(1L)
|
// val end = start.plusHours(1L)
|
||||||
return TvProgram(
|
// return TvProgram(
|
||||||
id = UUID.randomUUID(),
|
// id = UUID.randomUUID(),
|
||||||
channelId = channelId,
|
// channelId = channelId,
|
||||||
start = start,
|
// start = start,
|
||||||
end = start,
|
// end = start,
|
||||||
duration = 60.minutes,
|
// duration = 60.minutes,
|
||||||
name = "Unknown",
|
// name = "Unknown",
|
||||||
subtitle = null,
|
// subtitle = null,
|
||||||
seasonEpisode = null, // TODO
|
// seasonEpisode = null, // TODO
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
override val size: Int
|
// override val size: Int
|
||||||
get() = Int.MAX_VALUE
|
// get() = Int.MAX_VALUE
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
LazyList(pager) { item ->
|
// LazyList(pager) { item ->
|
||||||
item?.data?.let { dto ->
|
// item?.data?.let { dto ->
|
||||||
TvProgram(
|
// TvProgram(
|
||||||
id = dto.id,
|
// id = dto.id,
|
||||||
channelId = channelId,
|
// channelId = channelId,
|
||||||
start = dto.startDate!!,
|
// start = dto.startDate!!,
|
||||||
end = dto.endDate!!,
|
// end = dto.endDate!!,
|
||||||
duration = dto.runTimeTicks!!.ticks,
|
// duration = dto.runTimeTicks!!.ticks,
|
||||||
name = dto.seriesName ?: dto.name,
|
// name = dto.seriesName ?: dto.name,
|
||||||
subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() },
|
// subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() },
|
||||||
seasonEpisode = null, // TODO
|
// seasonEpisode = null, // TODO
|
||||||
)
|
// )
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
withContext(Dispatchers.Main) {
|
// withContext(Dispatchers.Main) {
|
||||||
data.value = programList
|
// data.value = programList
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
data
|
// data
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
data class TvChannel(
|
data class TvChannel(
|
||||||
|
|
@ -156,6 +240,8 @@ data class TvProgram(
|
||||||
val channelId: UUID,
|
val channelId: UUID,
|
||||||
val start: LocalDateTime,
|
val start: LocalDateTime,
|
||||||
val end: LocalDateTime,
|
val end: LocalDateTime,
|
||||||
|
val startHours: Float,
|
||||||
|
val endHours: Float,
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val subtitle: String?,
|
val subtitle: String?,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
|
@ -16,6 +17,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -23,6 +25,7 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusProperties
|
import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
|
@ -34,19 +37,25 @@ import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.times
|
import androidx.compose.ui.unit.times
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ListItem
|
import androidx.tv.material3.ListItem
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.playback.isDown
|
import com.github.damontecres.wholphin.ui.playback.isDown
|
||||||
import com.github.damontecres.wholphin.ui.playback.isUp
|
import com.github.damontecres.wholphin.ui.playback.isUp
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
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 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
|
||||||
|
|
@ -66,14 +75,57 @@ fun translate(
|
||||||
programIndex - before
|
programIndex - before
|
||||||
}
|
}
|
||||||
|
|
||||||
fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 }
|
@Composable
|
||||||
|
fun TvGrid(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: LiveTvViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init()
|
||||||
|
}
|
||||||
|
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())
|
||||||
|
when (val state = loading) {
|
||||||
|
is LoadingState.Error -> ErrorMessage(state)
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> LoadingPage()
|
||||||
|
|
||||||
|
LoadingState.Success ->
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Button(
|
||||||
|
onClick = {},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Button",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TvGrid(
|
||||||
|
channels = channels,
|
||||||
|
programList = programs,
|
||||||
|
programs = programsByChannel,
|
||||||
|
start = viewModel.start,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TvGrid(modifier: Modifier = Modifier) {
|
fun TvGrid(
|
||||||
|
channels: List<TvChannel>,
|
||||||
|
programList: List<TvProgram>,
|
||||||
|
programs: Map<UUID, List<TvProgram>>,
|
||||||
|
start: LocalDateTime,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
val state = rememberSaveableProgramGuideState()
|
val state = rememberSaveableProgramGuideState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val startHours = 6
|
// val timeline = 0..<24
|
||||||
val timeline = 16..24
|
|
||||||
var focusedProgram by rememberInt(0)
|
var focusedProgram by rememberInt(0)
|
||||||
var focusedChannel by rememberInt(0)
|
var focusedChannel by rememberInt(0)
|
||||||
|
|
||||||
|
|
@ -86,6 +138,8 @@ fun TvGrid(modifier: Modifier = Modifier) {
|
||||||
currentTimeWidth = 2.dp,
|
currentTimeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 }
|
||||||
|
|
||||||
ProgramGuide(
|
ProgramGuide(
|
||||||
state = state,
|
state = state,
|
||||||
dimensions = dimensions,
|
dimensions = dimensions,
|
||||||
|
|
@ -101,12 +155,32 @@ fun TvGrid(modifier: Modifier = Modifier) {
|
||||||
Key.DirectionRight -> focusedProgram + 1
|
Key.DirectionRight -> focusedProgram + 1
|
||||||
Key.DirectionLeft -> focusedProgram - 1
|
Key.DirectionLeft -> focusedProgram - 1
|
||||||
Key.DirectionUp -> {
|
Key.DirectionUp -> {
|
||||||
val start = programList[focusedProgram].start.hours
|
val start = programList[focusedProgram].startHours
|
||||||
focusedChannel = (focusedChannel - 1).coerceAtLeast(0)
|
focusedChannel = (focusedChannel - 1)
|
||||||
|
if (focusedChannel < 0) {
|
||||||
|
focusManager.moveFocus(FocusDirection.Up)
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
val channelId = channels[focusedChannel].id
|
||||||
|
val pIndex =
|
||||||
|
programs[channelId]?.indexOfFirst { start in (it.startHours..<it.endHours) }
|
||||||
|
?: -1
|
||||||
|
if (pIndex >= 0) {
|
||||||
|
programsBeforeChannel(focusedChannel) + pIndex
|
||||||
|
} else {
|
||||||
|
programsBeforeChannel(focusedChannel) + programs[channelId]!!.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key.DirectionDown -> {
|
||||||
|
val start = programList[focusedProgram].startHours
|
||||||
|
focusedChannel =
|
||||||
|
(focusedChannel + 1).coerceAtMost(channels.size - 1)
|
||||||
val channelId = channels[focusedChannel].id
|
val channelId = channels[focusedChannel].id
|
||||||
|
val pro = programs[channelId].orEmpty()
|
||||||
val pIndex =
|
val pIndex =
|
||||||
programs[channelId]?.indexOfFirst { start in (it.start.hours..<it.end.hours) }
|
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
|
||||||
?: -1
|
|
||||||
if (pIndex >= 0) {
|
if (pIndex >= 0) {
|
||||||
programsBeforeChannel(focusedChannel) + pIndex
|
programsBeforeChannel(focusedChannel) + pIndex
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -114,20 +188,9 @@ fun TvGrid(modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Key.DirectionDown -> {
|
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
|
||||||
val start = programList[focusedProgram].start.hours
|
Timber.v("Clicked on ${programList[focusedProgram]}")
|
||||||
focusedChannel =
|
null
|
||||||
(focusedChannel + 1).coerceAtMost(channels.size - 1)
|
|
||||||
val channelId = channels[focusedChannel].id
|
|
||||||
val pro = programs[channelId]!!
|
|
||||||
val pIndex =
|
|
||||||
pro.indexOfFirst { start in (it.start.hours..<it.end.hours) }
|
|
||||||
?: -1
|
|
||||||
if (pIndex >= 0) {
|
|
||||||
programsBeforeChannel(focusedChannel) + pIndex
|
|
||||||
} else {
|
|
||||||
programsBeforeChannel(focusedChannel) + programs[channelId]!!.size
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|
@ -148,20 +211,19 @@ fun TvGrid(modifier: Modifier = Modifier) {
|
||||||
return@onPreviewKeyEvent false
|
return@onPreviewKeyEvent false
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
guideStartHour = timeline.first.toFloat()
|
guideStartHour = 0f
|
||||||
timeline(
|
timeline(
|
||||||
count = timeline.count(),
|
count = MAX_HOURS.toInt(),
|
||||||
layoutInfo = {
|
layoutInfo = { index ->
|
||||||
val start = timeline.toList()[it].toFloat()
|
|
||||||
ProgramGuideItem.Timeline(
|
ProgramGuideItem.Timeline(
|
||||||
startHour = start,
|
startHour = index.toFloat(),
|
||||||
endHour = start + 1f,
|
endHour = index + 1f,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { index ->
|
) { index ->
|
||||||
val start = timeline.toList()[index].toFloat()
|
val time = start.plusHours(index.toLong()).toLocalTime()
|
||||||
Text(
|
Text(
|
||||||
text = "$start o'clock",
|
text = time.toString(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,7 +232,7 @@ fun TvGrid(modifier: Modifier = Modifier) {
|
||||||
layoutInfo = { programIndex ->
|
layoutInfo = { programIndex ->
|
||||||
val program = programList[programIndex]
|
val program = programList[programIndex]
|
||||||
val channelIndex = channels.indexOfFirst { it.id == program.channelId }
|
val channelIndex = channels.indexOfFirst { it.id == program.channelId }
|
||||||
ProgramGuideItem.Program(channelIndex, program.start.hours, program.end.hours)
|
ProgramGuideItem.Program(channelIndex, program.startHours, program.endHours)
|
||||||
},
|
},
|
||||||
) { programIndex ->
|
) { programIndex ->
|
||||||
val program = programList[programIndex]
|
val program = programList[programIndex]
|
||||||
|
|
@ -191,7 +253,7 @@ fun TvGrid(modifier: Modifier = Modifier) {
|
||||||
},
|
},
|
||||||
) { channelIndex ->
|
) { channelIndex ->
|
||||||
Text(
|
Text(
|
||||||
text = channels[channelIndex].name ?: channelIndex.toString(),
|
text = channels[channelIndex].number ?: channelIndex.toString(),
|
||||||
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
modifier = Modifier.background(MaterialTheme.colorScheme.background),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -442,6 +504,8 @@ val programs =
|
||||||
channelId = channel1Id,
|
channelId = channel1Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||||
|
startHours = 18f,
|
||||||
|
endHours = 19f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C1 Program #1",
|
name = "C1 Program #1",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -452,6 +516,8 @@ val programs =
|
||||||
channelId = channel1Id,
|
channelId = channel1Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||||
|
startHours = 19f,
|
||||||
|
endHours = 19.5f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C1 Program #2",
|
name = "C1 Program #2",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -462,6 +528,8 @@ val programs =
|
||||||
channelId = channel1Id,
|
channelId = channel1Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
||||||
|
startHours = 19.5f,
|
||||||
|
endHours = 20f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C1 Program #3",
|
name = "C1 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -472,6 +540,8 @@ val programs =
|
||||||
channelId = channel1Id,
|
channelId = channel1Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 20, 0, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 20, 0, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 21, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 21, 0, 0),
|
||||||
|
startHours = 20f,
|
||||||
|
endHours = 21f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C1 Program #3",
|
name = "C1 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -482,6 +552,8 @@ val programs =
|
||||||
channelId = channel1Id,
|
channelId = channel1Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 22, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 22, 0, 0),
|
||||||
|
startHours = 21f,
|
||||||
|
endHours = 22f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C1 Program #3",
|
name = "C1 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -492,11 +564,25 @@ val programs =
|
||||||
channelId = channel1Id,
|
channelId = channel1Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 22, 0, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 22, 0, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 23, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 23, 0, 0),
|
||||||
|
startHours = 22f,
|
||||||
|
endHours = 23f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C1 Program #3",
|
name = "C1 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
seasonEpisode = null,
|
seasonEpisode = null,
|
||||||
),
|
),
|
||||||
|
TvProgram(
|
||||||
|
id = UUID.randomUUID(),
|
||||||
|
channelId = channel1Id,
|
||||||
|
start = LocalDateTime.of(2025, 10, 17, 1, 0, 0, 0),
|
||||||
|
end = LocalDateTime.of(2025, 10, 17, 2, 0, 0),
|
||||||
|
startHours = 25f,
|
||||||
|
endHours = 26f,
|
||||||
|
duration = 60.minutes,
|
||||||
|
name = "C1 Program #4",
|
||||||
|
subtitle = null,
|
||||||
|
seasonEpisode = null,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
channel2Id to
|
channel2Id to
|
||||||
listOf(
|
listOf(
|
||||||
|
|
@ -505,6 +591,8 @@ val programs =
|
||||||
channelId = channel2Id,
|
channelId = channel2Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 18, 30, 0),
|
end = LocalDateTime.of(2025, 10, 16, 18, 30, 0),
|
||||||
|
startHours = 18f,
|
||||||
|
endHours = 18.5f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C2 Program #1",
|
name = "C2 Program #1",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -515,6 +603,8 @@ val programs =
|
||||||
channelId = channel2Id,
|
channelId = channel2Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 30, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 30, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||||
|
startHours = 18.5f,
|
||||||
|
endHours = 19f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C2 Program #2",
|
name = "C2 Program #2",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -525,6 +615,8 @@ val programs =
|
||||||
channelId = channel2Id,
|
channelId = channel2Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
||||||
|
startHours = 19.5f,
|
||||||
|
endHours = 20f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C2 Program #3",
|
name = "C2 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -535,6 +627,8 @@ val programs =
|
||||||
channelId = channel2Id,
|
channelId = channel2Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 22, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 22, 0, 0),
|
||||||
|
startHours = 21f,
|
||||||
|
endHours = 22f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C2 Program #4",
|
name = "C2 Program #4",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -549,6 +643,8 @@ val programs =
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 18, 15, 0),
|
end = LocalDateTime.of(2025, 10, 16, 18, 15, 0),
|
||||||
duration = 15.minutes,
|
duration = 15.minutes,
|
||||||
|
startHours = 18f,
|
||||||
|
endHours = 18.25f,
|
||||||
name = "C3 Program #1",
|
name = "C3 Program #1",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
seasonEpisode = null,
|
seasonEpisode = null,
|
||||||
|
|
@ -558,6 +654,8 @@ val programs =
|
||||||
channelId = channel3Id,
|
channelId = channel3Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 15, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 15, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 18, 45, 0),
|
end = LocalDateTime.of(2025, 10, 16, 18, 45, 0),
|
||||||
|
startHours = 18.25f,
|
||||||
|
endHours = 18.75f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C3 Program #2",
|
name = "C3 Program #2",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -568,6 +666,8 @@ val programs =
|
||||||
channelId = channel3Id,
|
channelId = channel3Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 45, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 45, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||||
|
startHours = 18.75f,
|
||||||
|
endHours = 19f,
|
||||||
duration = 15.minutes,
|
duration = 15.minutes,
|
||||||
name = "C3 Program #3",
|
name = "C3 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -578,6 +678,8 @@ val programs =
|
||||||
channelId = channel3Id,
|
channelId = channel3Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
||||||
|
startHours = 19f,
|
||||||
|
endHours = 20f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C3 Program #3",
|
name = "C3 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -591,6 +693,8 @@ val programs =
|
||||||
channelId = channel4Id,
|
channelId = channel4Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||||
|
startHours = 18f,
|
||||||
|
endHours = 19f,
|
||||||
duration = 60.minutes,
|
duration = 60.minutes,
|
||||||
name = "C4 Program #1",
|
name = "C4 Program #1",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -601,6 +705,8 @@ val programs =
|
||||||
channelId = channel4Id,
|
channelId = channel4Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||||
|
startHours = 19f,
|
||||||
|
endHours = 19.5f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C4 Program #2",
|
name = "C4 Program #2",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
@ -611,6 +717,8 @@ val programs =
|
||||||
channelId = channel4Id,
|
channelId = channel4Id,
|
||||||
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
|
||||||
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
|
||||||
|
startHours = 19.5f,
|
||||||
|
endHours = 20f,
|
||||||
duration = 30.minutes,
|
duration = 30.minutes,
|
||||||
name = "C4 Program #3",
|
name = "C4 Program #3",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue