mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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
This commit is contained in:
parent
693398536e
commit
5c5db4c1ef
3 changed files with 79 additions and 26 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -74,11 +74,10 @@ class LiveTvViewModel
|
|||
) : ViewModel() {
|
||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
|
||||
lateinit var guideStart: LocalDateTime
|
||||
private set
|
||||
private lateinit var channelsIdToIndex: Map<UUID, Int>
|
||||
private val mutex = Mutex()
|
||||
|
||||
val guideTimes = MutableLiveData<List<LocalDateTime>>(buildGuideTimes())
|
||||
val channels = MutableLiveData<List<TvChannel>>()
|
||||
val channelProgramCount = mutableMapOf<UUID, Int>()
|
||||
val programs = MutableLiveData<FetchedPrograms>()
|
||||
|
|
@ -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.coerceAtMost(channels.size))
|
||||
fetchPrograms(guideStart, channels, 0..<initial.coerceAtMost(channels.size))
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
this@LiveTvViewModel.channels.value = channels
|
||||
|
|
@ -161,21 +160,35 @@ class LiveTvViewModel
|
|||
}
|
||||
// Now load the full range
|
||||
if (channels.size > initial) {
|
||||
fetchPrograms(channels, 0..<range.coerceAtMost(channels.size))
|
||||
fetchPrograms(guideStart, channels, 0..<range.coerceAtMost(channels.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildGuideTimes() =
|
||||
buildList {
|
||||
val start = LocalDateTime.now().roundDownToHalfHour()
|
||||
add(start)
|
||||
if (start.minute == 30) {
|
||||
add(start.plusMinutes(30))
|
||||
}
|
||||
repeat(MAX_HOURS.toInt() - 1) {
|
||||
add(last().plusHours(1))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchProgramsWithLoading(
|
||||
channels: List<TvChannel>,
|
||||
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<TvChannel>,
|
||||
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<TvProgram>,
|
||||
val programsByChannel: Map<UUID, List<TvProgram>>,
|
||||
)
|
||||
|
||||
fun LocalDateTime.roundDownToHalfHour(): LocalDateTime {
|
||||
val min = minute % 30L
|
||||
return minusMinutes(min).truncatedTo(ChronoUnit.MINUTES)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Int?>(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<TvChannel>,
|
||||
programs: FetchedPrograms,
|
||||
channelProgramCount: Map<UUID, Int>,
|
||||
start: LocalDateTime,
|
||||
guideTimes: List<LocalDateTime>,
|
||||
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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue