From 5c5db4c1ef66ac1aad1c05ef60071b7c58aaa63b Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Sat, 20 Dec 2025 11:54:41 -0500 Subject: [PATCH] TV Guide title clean up & align start to half hours (#517) ## Description - Cleans up program titles & subtitles by removing newline characters - Align the start of the guide to half hour, rounded down, ie 9:52->9:30 or 11:05->11:00 - Add some visual indication that a program has started before the guide start time - This is done by adding a `<` prefix to the title and using sharp 90 degree corners at the start instead of rounded ### Related issues Closes #434 Closes #435 Closes #436 --- .../wholphin/ui/detail/livetv/Components.kt | 32 +++++++++++++-- .../ui/detail/livetv/LiveTvViewModel.kt | 40 ++++++++++++++----- .../wholphin/ui/detail/livetv/TvGuideGrid.kt | 33 ++++++++------- 3 files changed, 79 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt index 41b7b114..18d193ee 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/Components.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow @@ -20,9 +21,11 @@ import androidx.tv.material3.Text import androidx.tv.material3.contentColorFor import androidx.tv.material3.surfaceColorAtElevation import coil3.compose.AsyncImage +import java.time.LocalDateTime @Composable fun Program( + guideStart: LocalDateTime, program: TvProgram, focused: Boolean, colorCode: Boolean, @@ -38,14 +41,37 @@ fun Program( MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) } val textColor = MaterialTheme.colorScheme.contentColorFor(background) + val startedBeforeGuide = program.start.isBefore(guideStart) + val shape = + remember(startedBeforeGuide) { + val cornerSize = 4.dp + if (startedBeforeGuide) { + RoundedCornerShape( + topEnd = cornerSize, + bottomEnd = cornerSize, + topStart = 0.dp, + bottomStart = 0.dp, + ) + } else { + RoundedCornerShape(cornerSize) + } + } + val title = + remember(startedBeforeGuide) { + if (startedBeforeGuide) { + "< " + } else { + "" + } + (program.name ?: program.id.toString()) + } Box( modifier = modifier .padding(2.dp) .fillMaxSize() .background( - background, - shape = RoundedCornerShape(4.dp), + color = background, + shape = shape, ), ) { Column( @@ -56,7 +82,7 @@ fun Program( .padding(4.dp), ) { Text( - text = program.name ?: program.id.toString(), + text = title, color = textColor, fontSize = 16.sp, maxLines = 1, 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 b9c74ce6..621dd7da 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 @@ -74,11 +74,10 @@ class LiveTvViewModel ) : ViewModel() { val loading = MutableLiveData(LoadingState.Pending) - lateinit var guideStart: LocalDateTime - private set private lateinit var channelsIdToIndex: Map private val mutex = Mutex() + val guideTimes = MutableLiveData>(buildGuideTimes()) val channels = MutableLiveData>() val channelProgramCount = mutableMapOf() val programs = MutableLiveData() @@ -106,7 +105,7 @@ class LiveTvViewModel } fun init() { - guideStart = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS) + val guideStart = guideTimes.value!!.first() viewModelScope.launch( Dispatchers.IO + LoadingExceptionHandler( @@ -153,7 +152,7 @@ class LiveTvViewModel // Initially, quickly load the first 10 channels (only some are visible immediately), then below will load more // This makes the guide appear faster, and load more usable data in the background val initial = 10 - fetchPrograms(channels, 0.. initial) { - fetchPrograms(channels, 0.., range: IntRange, ) { loading.setValueOnMain(LoadingState.Loading) - fetchPrograms(channels, range) + val guideStart = guideTimes.value!!.first() + fetchPrograms(guideStart, channels, range) loading.setValueOnMain(LoadingState.Success) } private suspend fun fetchPrograms( + guideStart: LocalDateTime, channels: List, range: IntRange, ) = mutex.withLock { @@ -216,7 +229,11 @@ class LiveTvViewModel } else { null } - + val name = (dto.seriesName ?: dto.name)?.replace(Regex("[\n\r]"), "") + val subtitle = + dto.episodeTitle + .takeIf { dto.isSeries ?: false } + ?.replace(Regex("[\n\r]"), "") val p = TvProgram( id = dto.id, @@ -230,8 +247,8 @@ class LiveTvViewModel ).coerceAtLeast(0f), endHours = hoursBetween(guideStart, dto.endDate!!), duration = dto.runTimeTicks!!.ticks, - name = dto.seriesName ?: dto.name, - subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false }, + name = name, + subtitle = subtitle, overview = dto.overview, officialRating = dto.officialRating, seasonEpisode = @@ -573,3 +590,8 @@ data class FetchedPrograms( val programs: List, val programsByChannel: Map>, ) + +fun LocalDateTime.roundDownToHalfHour(): LocalDateTime { + val min = minute % 30L + return minusMinutes(min).truncatedTo(ChronoUnit.MINUTES) +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt index 8fee8995..68c62063 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt @@ -105,6 +105,7 @@ fun TvGuideGrid( LoadingState.Success, -> { val context = LocalContext.current + val guideTimes by viewModel.guideTimes.observeAsState(listOf()) val fetchedItem by viewModel.fetchedItem.observeAsState(null) val loadingItem by viewModel.fetchingItem.observeAsState(LoadingState.Pending) var showItemDialog by remember { mutableStateOf(null) } @@ -156,7 +157,7 @@ fun TvGuideGrid( channels = channels, programs = programs, channelProgramCount = viewModel.channelProgramCount, - start = viewModel.guideStart, + guideTimes = guideTimes, onClickChannel = { index, channel -> viewModel.navigationManager.navigateTo( Destination.Playback( @@ -273,7 +274,7 @@ fun TvGuideGridContent( channels: List, programs: FetchedPrograms, channelProgramCount: Map, - start: LocalDateTime, + guideTimes: List, onClickChannel: (Int, TvChannel) -> Unit, onClickProgram: (Int, TvProgram) -> Unit, onFocus: (RowColumn) -> Unit, @@ -284,6 +285,7 @@ fun TvGuideGridContent( val focusManager = LocalFocusManager.current val state = rememberSaveableProgramGuideState() val scope = rememberCoroutineScope() + val guideStart = guideTimes.first() var focusedItem by rememberPosition(RowColumn(0, 0)) val focusedChannelIndex = focusedItem.row @@ -506,7 +508,7 @@ fun TvGuideGridContent( currentTime( layoutInfo = { ProgramGuideItem.CurrentTime( - hoursBetween(start, LocalDateTime.now()), + hoursBetween(guideStart, LocalDateTime.now()), ) }, ) { @@ -523,11 +525,18 @@ fun TvGuideGridContent( } } timeline( - count = MAX_HOURS.toInt(), + count = guideTimes.size, layoutInfo = { index -> + val start = guideTimes[index] + val end = + if (index < guideTimes.lastIndex) { + guideTimes[index + 1] + } else { + start.plusHours(1) + } ProgramGuideItem.Timeline( - startHour = index.toFloat(), - endHour = index + 1f, + startHour = hoursBetween(guideStart, start), + endHour = hoursBetween(guideStart, end), ) }, ) { index -> @@ -545,16 +554,12 @@ fun TvGuideGridContent( shape = RoundedCornerShape(4.dp), ), ) { - val differentDay = - start.toLocalDate() != - start - .plusHours(index.toLong()) - .toLocalDate() + val guideTime = guideTimes[index] + val differentDay = guideTime.toLocalDate() != guideStart.toLocalDate() val time = DateUtils.formatDateTime( context, - start - .plusHours(index.toLong()) + guideTime .toInstant(OffsetDateTime.now().offset) .epochSecond * 1000, DateUtils.FORMAT_SHOW_TIME or if (differentDay) DateUtils.FORMAT_SHOW_WEEKDAY else 0, @@ -584,7 +589,7 @@ fun TvGuideGridContent( val program = programs.programs[programIndex] val focused = gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex - Program(program, focused, preferences.colorCodePrograms, Modifier) + Program(guideStart, program, focused, preferences.colorCodePrograms, Modifier) } channels(