From fb4e36168c03c5ff3c89e7b7f326e1d48bbceed5 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 20 Oct 2025 15:42:57 -0400 Subject: [PATCH] Mark recordings --- .../ui/detail/livetv/LiveTvViewModel.kt | 30 +++++-- .../wholphin/ui/detail/livetv/TvGrid.kt | 85 ++++++++++++++++--- 2 files changed, 96 insertions(+), 19 deletions(-) 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 1d5bd274..0968ac01 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode +import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.util.GetProgramsDtoHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler @@ -40,7 +41,7 @@ class LiveTvViewModel val loading = MutableLiveData(LoadingState.Pending) val start = - LocalDateTime.now().minusMinutes(30).truncatedTo(ChronoUnit.HOURS) + LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) val channels = MutableLiveData>() val programs = MutableLiveData>() @@ -91,14 +92,8 @@ class LiveTvViewModel channelId = dto.channelId!!, start = dto.startDate!!, end = dto.endDate!!, - startHours = - java.time.Duration - .between(start, dto.startDate!!) - .seconds / (60f * 60f), - endHours = - java.time.Duration - .between(start, dto.endDate!!) - .seconds / (60f * 60f), + startHours = hoursBetween(start, dto.startDate!!), + endHours = hoursBetween(start, dto.endDate!!), duration = dto.runTimeTicks!!.ticks, name = dto.seriesName ?: dto.name, subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false }, @@ -108,6 +103,8 @@ class LiveTvViewModel } else { null }, + isRecording = dto.timerId.isNotNullOrBlank(), + isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(), ) }.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side @@ -130,6 +127,8 @@ class LiveTvViewModel name = "No data", subtitle = null, seasonEpisode = null, + isRecording = false, + isSeriesRecording = false, ) } put(channel.id, fakePrograms) @@ -209,6 +208,17 @@ class LiveTvViewModel // } } +/** + * Returns the number of hours between two [LocalDateTime] + */ +fun hoursBetween( + start: LocalDateTime, + target: LocalDateTime, +): Float = + java.time.Duration + .between(start, target) + .seconds / (60f * 60f) + data class TvChannel( val id: UUID, val number: String?, @@ -227,4 +237,6 @@ data class TvProgram( val name: String?, val subtitle: String?, val seasonEpisode: SeasonEpisode?, + val isRecording: Boolean, + val isSeriesRecording: Boolean, ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt index 83ec1ecb..b7411798 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt @@ -9,16 +9,20 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key @@ -28,6 +32,8 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Surface +import androidx.tv.material3.SurfaceDefaults import androidx.tv.material3.Text import androidx.tv.material3.contentColorFor import androidx.tv.material3.surfaceColorAtElevation @@ -39,6 +45,8 @@ import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt import com.github.damontecres.wholphin.util.ExceptionHandler 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.ProgramGuideDimensions import eu.wewox.programguide.ProgramGuideItem @@ -103,7 +111,6 @@ fun TvGrid( val focusManager = LocalFocusManager.current val state = rememberSaveableProgramGuideState() val scope = rememberCoroutineScope() -// val timeline = 0..<24 var focusedProgramIndex by rememberInt(0) var focusedChannelIndex by rememberInt(0) @@ -116,7 +123,23 @@ fun TvGrid( currentTimeWidth = 2.dp, ) - fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 } + val programsBeforeChannel = + remember { + CacheBuilder + .newBuilder() + .maximumSize(200) + .build( + object : CacheLoader() { + override fun load(key: Int): Int = + channels + .subList(0, key) + .map { it.id } + .sumOf { programs[it]?.size ?: 0 } + }, + ) + } + +// fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 } ProgramGuide( state = state, @@ -132,7 +155,7 @@ fun TvGrid( when (it.key) { Key.DirectionRight -> { val nextProgramIndex = focusedProgramIndex + 1 - val programsBefore = programsBeforeChannel(focusedChannelIndex) + val programsBefore = programsBeforeChannel.get(focusedChannelIndex) val relativePosition = nextProgramIndex - programsBefore val channelPrograms = programs[channels[focusedChannelIndex].id].orEmpty() @@ -147,7 +170,8 @@ fun TvGrid( Key.DirectionLeft -> { val nextProgramIndex = focusedProgramIndex - 1 if (nextProgramIndex >= 0) { - val programsBefore = programsBeforeChannel(focusedChannelIndex) + val programsBefore = + programsBeforeChannel.get(focusedChannelIndex) val relativePosition = nextProgramIndex - programsBefore // val channelPrograms = // programs[channels[focusedChannel].id].orEmpty() @@ -175,9 +199,9 @@ fun TvGrid( programs[channelId]?.indexOfFirst { start in (it.startHours..= 0) { - programsBeforeChannel(focusedChannelIndex) + pIndex + programsBeforeChannel.get(focusedChannelIndex) + pIndex } else { - programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size + programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size } } } @@ -191,9 +215,9 @@ fun TvGrid( val pIndex = pro.indexOfFirst { start in (it.startHours..= 0) { - programsBeforeChannel(focusedChannelIndex) + pIndex + programsBeforeChannel.get(focusedChannelIndex) + pIndex } else { - programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size + programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size } } @@ -209,13 +233,13 @@ fun TvGrid( } } if (newIndex != null) { - val before = programsBeforeChannel(focusedChannelIndex) + val before = programsBeforeChannel.get(focusedChannelIndex) val max = before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1 val index = newIndex.coerceIn(before, max) scope.launch(ExceptionHandler()) { - state.animateToProgram(index, Alignment.Center) focusedProgramIndex = index + state.animateToProgram(index, Alignment.Center) } return@onPreviewKeyEvent true } @@ -223,6 +247,20 @@ fun TvGrid( }, ) { guideStartHour = 0f + currentTime( + layoutInfo = { + ProgramGuideItem.CurrentTime( + hoursBetween(start, LocalDateTime.now()), + ) + }, + ) { + Surface( + colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary), + modifier = Modifier, + ) { + // Empty + } + } timeline( count = MAX_HOURS.toInt(), layoutInfo = { index -> @@ -295,6 +333,33 @@ fun TvGrid( ) } } + if (program.isSeriesRecording) { + Box( + modifier = + Modifier + .padding(4.dp) + .size(16.dp) + .background(Color.Red.copy(alpha = .5f), shape = CircleShape) + .align(Alignment.BottomEnd), + ) + Box( + modifier = + Modifier + .padding(start = 4.dp, top = 4.dp, bottom = 4.dp, end = 10.dp) + .size(16.dp) + .background(Color.Red, shape = CircleShape) + .align(Alignment.BottomEnd), + ) + } else if (program.isRecording) { + Box( + modifier = + Modifier + .padding(4.dp) + .size(16.dp) + .background(Color.Red, shape = CircleShape) + .align(Alignment.BottomEnd), + ) + } } }