Show loading indicator when refreshing guide data

This commit is contained in:
Damontecres 2025-10-22 12:32:31 -04:00
parent be7bb65a7e
commit 36287be5d4
No known key found for this signature in database
3 changed files with 411 additions and 371 deletions

View file

@ -90,6 +90,12 @@ class LiveTvViewModel
}
}
private suspend fun fetchProgramsWithLoading(channels: List<TvChannel>) {
loading.setValueOnMain(LoadingState.Loading)
fetchPrograms(channels)
loading.setValueOnMain(LoadingState.Success)
}
private suspend fun fetchPrograms(channels: List<TvChannel>) =
mutex.withLock {
val maxStartDate = start.plusHours(MAX_HOURS).minusMinutes(1)
@ -203,7 +209,7 @@ class LiveTvViewModel
viewModelScope.launchIO(ExceptionHandler(autoToast = true)) {
if (series) {
api.liveTvApi.cancelSeriesTimer(timerId)
fetchPrograms(channels.value.orEmpty())
fetchProgramsWithLoading(channels.value.orEmpty())
} else {
api.liveTvApi.cancelTimer(timerId)
refreshProgram(programIndex, programId)
@ -221,7 +227,7 @@ class LiveTvViewModel
val d by api.liveTvApi.getDefaultTimer(programId.toServerString())
if (series) {
api.liveTvApi.createSeriesTimer(d)
fetchPrograms(channels.value.orEmpty())
fetchProgramsWithLoading(channels.value.orEmpty())
} else {
val payload =
TimerInfoDto(
@ -256,6 +262,7 @@ class LiveTvViewModel
programIndex: Int,
programId: UUID,
) = mutex.withLock {
loading.setValueOnMain(LoadingState.Loading)
val program by api.liveTvApi.getProgram(programId.toServerString())
val newProgram =
programs.value?.getOrNull(programIndex)?.copy(
@ -272,6 +279,7 @@ class LiveTvViewModel
this@LiveTvViewModel.programs.setValueOnMain(it)
}
}
loading.setValueOnMain(LoadingState.Success)
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
@ -129,74 +130,87 @@ fun ProgramDialog(
maxLines = 3,
)
}
if (dto.isSeries ?: false) {
Button(
onClick = {
if (isSeriesRecording) {
onCancelRecord.invoke(true)
} else {
onRecord.invoke(true)
}
},
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isSeriesRecording) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = Color.Red,
)
}
Text(
text = if (isSeriesRecording) "Cancel Series Recording" else "Record Series",
)
}
}
}
Button(
onClick = {
if (isRecording) {
onCancelRecord.invoke(false)
} else {
onRecord.invoke(false)
}
},
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
if (isRecording) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = Color.Red,
)
if (dto.isSeries ?: false) {
Button(
onClick = {
if (isSeriesRecording) {
onCancelRecord.invoke(true)
} else {
onRecord.invoke(true)
}
},
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isSeriesRecording) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = Color.Red,
)
}
Text(
text = if (isSeriesRecording) "Cancel Series Recording" else "Record Series",
)
}
}
}
Text(
text = if (isRecording) "Cancel Recording" else "Record Program",
)
}
}
if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) {
Button(
onClick = onWatch,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
Button(
onClick = {
if (isRecording) {
onCancelRecord.invoke(false)
} else {
onRecord.invoke(false)
}
},
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Delete",
tint = Color.Green.copy(alpha = .75f),
)
Text(
text = "Watch live",
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
if (isRecording) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = Color.Red,
)
}
Text(
text = if (isRecording) "Cancel Recording" else "Record Program",
)
}
}
}
if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) {
Button(
onClick = onWatch,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Delete",
tint = Color.Green.copy(alpha = .75f),
)
Text(
text = "Watch live",
)
}
}
}
}

View file

@ -11,6 +11,8 @@ 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
@ -43,6 +45,7 @@ import androidx.tv.material3.Text
import androidx.tv.material3.contentColorFor
import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.enableMarquee
@ -78,12 +81,12 @@ fun TvGuideGrid(
val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf())
when (val state = loading) {
is LoadingState.Error -> ErrorMessage(state, modifier)
LoadingState.Loading,
LoadingState.Pending,
-> LoadingPage(modifier)
LoadingState.Success -> {
LoadingState.Loading,
LoadingState.Success,
-> {
val context = LocalContext.current
val fetchedItem by viewModel.fetchedItem.observeAsState(null)
val loadingItem by viewModel.fetchingItem.observeAsState(LoadingState.Pending)
@ -96,6 +99,7 @@ fun TvGuideGrid(
}
Column(modifier = modifier) {
TvGuideGrid(
loading = state is LoadingState.Loading,
channels = channels,
programList = programs,
programs = programsByChannel,
@ -183,6 +187,7 @@ const val CHANNEL_COLUMN = -1
@Composable
fun TvGuideGrid(
loading: Boolean,
channels: List<TvChannel>,
programList: List<TvProgram>,
programs: Map<UUID, List<TvProgram>>,
@ -225,352 +230,365 @@ fun TvGuideGrid(
var gridHasFocus by rememberSaveable { mutableStateOf(false) }
var channelColumnFocused by rememberSaveable { mutableStateOf(false) }
ProgramGuide(
state = state,
dimensions = dimensions,
modifier =
modifier
.onFocusChanged {
gridHasFocus = it.hasFocus
}.focusable()
.onPreviewKeyEvent {
if (it.type == KeyEventType.KeyUp) {
return@onPreviewKeyEvent false
}
val newIndex =
when (it.key) {
Key.Back -> {
val pos = programsBeforeChannel.get(focusedChannelIndex)
if (focusedProgramIndex - pos > 0) {
// Not at beginning of row, so move to beginning
pos
} else {
// At beginning, so allow normal back button behavior
return@onPreviewKeyEvent false
}
}
Key.DirectionRight -> {
if (channelColumnFocused) {
channelColumnFocused = false
focusedProgramIndex
} else {
val nextProgramIndex = focusedProgramIndex + 1
val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore
val channelPrograms =
programs[channels[focusedChannelIndex].id].orEmpty()
if (relativePosition >= channelPrograms.size) {
focusManager.moveFocus(FocusDirection.Right)
null
Box(modifier = modifier) {
ProgramGuide(
state = state,
dimensions = dimensions,
modifier =
Modifier
.fillMaxSize()
.onFocusChanged {
gridHasFocus = it.hasFocus
}.focusable()
.onPreviewKeyEvent {
if (it.type == KeyEventType.KeyUp) {
return@onPreviewKeyEvent false
}
val newIndex =
when (it.key) {
Key.Back -> {
val pos = programsBeforeChannel.get(focusedChannelIndex)
if (focusedProgramIndex - pos > 0) {
// Not at beginning of row, so move to beginning
pos
} else {
nextProgramIndex
// At beginning, so allow normal back button behavior
return@onPreviewKeyEvent false
}
}
}
Key.DirectionLeft -> {
if (channelColumnFocused) {
focusManager.moveFocus(FocusDirection.Left)
null
} else {
val nextProgramIndex = focusedProgramIndex - 1
val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore
// val channelPrograms =
// programs[channels[focusedChannel].id].orEmpty()
if (relativePosition >= 0) {
nextProgramIndex
} else if (relativePosition == CHANNEL_COLUMN) {
channelColumnFocused = true
Key.DirectionRight -> {
if (channelColumnFocused) {
channelColumnFocused = false
focusedProgramIndex
} else {
Timber.w("Move left to relativePosition=$relativePosition should not occur")
val nextProgramIndex = focusedProgramIndex + 1
val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore
val channelPrograms =
programs[channels[focusedChannelIndex].id].orEmpty()
if (relativePosition >= channelPrograms.size) {
focusManager.moveFocus(FocusDirection.Right)
null
} else {
nextProgramIndex
}
}
}
Key.DirectionLeft -> {
if (channelColumnFocused) {
focusManager.moveFocus(FocusDirection.Left)
null
}
}
}
Key.DirectionUp -> {
val newFocusedChannelIndex = (focusedChannelIndex - 1)
// Timber.v("newFocusedChannelIndex=$newFocusedChannelIndex")
if (newFocusedChannelIndex < 0) {
focusManager.moveFocus(FocusDirection.Up)
null
} else if (channelColumnFocused) {
focusedChannelIndex = newFocusedChannelIndex
programsBeforeChannel.get(focusedChannelIndex)
} else {
val start = programList[focusedProgramIndex].startHours
focusedChannelIndex =
newFocusedChannelIndex.coerceAtLeast(0)
val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty()
val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else {
val pIndex = pro.indexOfFirst { it.startHours >= start }
if (pIndex >= 0) {
programsBeforeChannel.get(focusedChannelIndex) + pIndex
val nextProgramIndex = focusedProgramIndex - 1
val programsBefore =
programsBeforeChannel.get(focusedChannelIndex)
val relativePosition = nextProgramIndex - programsBefore
// val channelPrograms =
// programs[channels[focusedChannel].id].orEmpty()
if (relativePosition >= 0) {
nextProgramIndex
} else if (relativePosition == CHANNEL_COLUMN) {
channelColumnFocused = true
focusedProgramIndex
} else {
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
Timber.w("Move left to relativePosition=$relativePosition should not occur")
focusManager.moveFocus(FocusDirection.Left)
null
}
}
}
}
Key.DirectionDown -> {
// Move channel focus down
val newFocusedChannelIndex = (focusedChannelIndex + 1)
if (newFocusedChannelIndex >= channels.size) {
// If trying to move below the final channel, then move focus out of the grid
focusManager.moveFocus(FocusDirection.Down)
null
} else if (channelColumnFocused) {
// If focused on the channel column, move down a channel
focusedChannelIndex =
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
programsBeforeChannel.get(focusedChannelIndex)
} else {
// Otherwise, moving to a new row
focusedChannelIndex =
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
// Get the new row/channel's programs
val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty()
// When the currently focused program starts
val start = programList[focusedProgramIndex].startHours
// Find the first program where the start time (of the previously focused program) is in the middle of a program
val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
// Found one, so focus on it
// Get the sum of all of the previous channels' program sizes, plus the index found to convert a relative program index into absolute
programsBeforeChannel.get(focusedChannelIndex) + pIndex
Key.DirectionUp -> {
val newFocusedChannelIndex = (focusedChannelIndex - 1)
// Timber.v("newFocusedChannelIndex=$newFocusedChannelIndex")
if (newFocusedChannelIndex < 0) {
focusManager.moveFocus(FocusDirection.Up)
null
} else if (channelColumnFocused) {
focusedChannelIndex = newFocusedChannelIndex
programsBeforeChannel.get(focusedChannelIndex)
} else {
// Didn't find one, probably due to missing data
// So now first the first one that starts after the previously focused program
val pIndex = pro.indexOfFirst { it.startHours >= start }
val start = programList[focusedProgramIndex].startHours
focusedChannelIndex =
newFocusedChannelIndex.coerceAtLeast(0)
val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty()
val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else {
val pIndex = pro.indexOfFirst { it.startHours >= start }
if (pIndex >= 0) {
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else {
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
}
}
}
}
Key.DirectionDown -> {
// Move channel focus down
val newFocusedChannelIndex = (focusedChannelIndex + 1)
if (newFocusedChannelIndex >= channels.size) {
// If trying to move below the final channel, then move focus out of the grid
focusManager.moveFocus(FocusDirection.Down)
null
} else if (channelColumnFocused) {
// If focused on the channel column, move down a channel
focusedChannelIndex =
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
programsBeforeChannel.get(focusedChannelIndex)
} else {
// Otherwise, moving to a new row
focusedChannelIndex =
newFocusedChannelIndex.coerceAtMost(channels.size - 1)
// Get the new row/channel's programs
val channelId = channels[focusedChannelIndex].id
val pro = programs[channelId].orEmpty()
// When the currently focused program starts
val start = programList[focusedProgramIndex].startHours
// Find the first program where the start time (of the previously focused program) is in the middle of a program
val pIndex =
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
if (pIndex >= 0) {
// Found one, so focus on it
// Get the sum of all of the previous channels' program sizes, plus the index found to convert a relative program index into absolute
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else {
// Did not find one, so focus on the final program in the list
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
// Didn't find one, probably due to missing data
// So now first the first one that starts after the previously focused program
val pIndex = pro.indexOfFirst { it.startHours >= start }
if (pIndex >= 0) {
// Found one, so focus on it
programsBeforeChannel.get(focusedChannelIndex) + pIndex
} else {
// Did not find one, so focus on the final program in the list
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
}
}
}
}
}
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
if (channelColumnFocused) {
val channel = channels[focusedChannelIndex]
Timber.v("Clicked on %s", channel)
onClickChannel.invoke(focusedChannelIndex, channel)
} else {
val program = programList[focusedProgramIndex]
Timber.v("Clicked on %s", program)
onClickProgram.invoke(focusedProgramIndex, program)
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
if (channelColumnFocused) {
val channel = channels[focusedChannelIndex]
Timber.v("Clicked on %s", channel)
onClickChannel.invoke(focusedChannelIndex, channel)
} else {
val program = programList[focusedProgramIndex]
Timber.v("Clicked on %s", program)
onClickProgram.invoke(focusedProgramIndex, program)
}
null
}
null
}
else -> {
null
else -> {
null
}
}
}
if (newIndex != null) {
if (newIndex != null) {
// Timber.v("newIndex=$newIndex")
if (newIndex >= 0) {
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()) {
focusedProgramIndex = index
state.animateToProgram(index, Alignment.Center)
if (newIndex >= 0) {
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()) {
focusedProgramIndex = index
state.animateToProgram(index, Alignment.Center)
}
return@onPreviewKeyEvent true
} else {
Timber.w("newIndex is <0: $newIndex")
return@onPreviewKeyEvent true
}
return@onPreviewKeyEvent true
} else {
Timber.w("newIndex is <0: $newIndex")
return@onPreviewKeyEvent true
}
}
return@onPreviewKeyEvent false
},
) {
guideStartHour = 0f
currentTime(
layoutInfo = {
ProgramGuideItem.CurrentTime(
hoursBetween(start, LocalDateTime.now()),
)
},
return@onPreviewKeyEvent false
},
) {
Surface(
colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary.copy(alpha = .25f)),
modifier = Modifier,
) {
// Empty
}
}
timeline(
count = MAX_HOURS.toInt(),
layoutInfo = { index ->
ProgramGuideItem.Timeline(
startHour = index.toFloat(),
endHour = index + 1f,
)
},
) { index ->
Box(
modifier =
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
) {
val differentDay =
start.toLocalDate() !=
start
.plusHours(index.toLong())
.toLocalDate()
val time =
DateUtils.formatDateTime(
context,
start
.plusHours(index.toLong())
.toInstant(OffsetDateTime.now().offset)
.epochSecond * 1000,
DateUtils.FORMAT_SHOW_TIME or if (differentDay) DateUtils.FORMAT_SHOW_WEEKDAY else 0,
guideStartHour = 0f
currentTime(
layoutInfo = {
ProgramGuideItem.CurrentTime(
hoursBetween(start, LocalDateTime.now()),
)
Text(
text = time.toString(),
modifier = Modifier.background(MaterialTheme.colorScheme.background),
)
}
}
programs(
count = programList.size,
layoutInfo = { programIndex ->
val program = programList[programIndex]
val channelIndex = channels.indexOfFirst { it.id == program.channelId }
ProgramGuideItem.Program(channelIndex, program.startHours, program.endHours)
},
) { programIndex ->
val program = programList[programIndex]
val focused =
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
val background =
if (focused) {
MaterialTheme.colorScheme.inverseSurface
} else {
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
}
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
Box(
modifier =
Modifier
// .scale(if (focused) 1.1f else 1f)
.padding(2.dp)
.fillMaxSize()
.background(
background,
shape = RoundedCornerShape(4.dp),
),
},
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
Surface(
colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary.copy(alpha = .25f)),
modifier = Modifier,
) {
// Empty
}
}
timeline(
count = MAX_HOURS.toInt(),
layoutInfo = { index ->
ProgramGuideItem.Timeline(
startHour = index.toFloat(),
endHour = index + 1f,
)
},
) { index ->
Box(
modifier =
Modifier
.fillMaxSize()
.padding(4.dp),
.background(MaterialTheme.colorScheme.surface),
) {
val differentDay =
start.toLocalDate() !=
start
.plusHours(index.toLong())
.toLocalDate()
val time =
DateUtils.formatDateTime(
context,
start
.plusHours(index.toLong())
.toInstant(OffsetDateTime.now().offset)
.epochSecond * 1000,
DateUtils.FORMAT_SHOW_TIME or if (differentDay) DateUtils.FORMAT_SHOW_WEEKDAY else 0,
)
Text(
text = program.name ?: program.id.toString(),
color = textColor,
maxLines = 1,
modifier = Modifier.enableMarquee(focused),
text = time.toString(),
modifier = Modifier.background(MaterialTheme.colorScheme.background),
)
listOfNotNull(
program.seasonEpisode?.let { "S${it.season} E${it.episode}" },
program.subtitle,
).joinToString(" - ")
.ifBlank { null }
?.let {
Text(
text = it,
color = textColor,
modifier = Modifier,
)
}
}
RecordingMarker(
isRecording = program.isRecording,
isSeriesRecording = program.isSeriesRecording,
modifier = Modifier.align(Alignment.BottomEnd),
}
programs(
count = programList.size,
layoutInfo = { programIndex ->
val program = programList[programIndex]
val channelIndex = channels.indexOfFirst { it.id == program.channelId }
ProgramGuideItem.Program(channelIndex, program.startHours, program.endHours)
},
) { programIndex ->
val program = programList[programIndex]
val focused =
gridHasFocus && !channelColumnFocused && programIndex == focusedProgramIndex
val background =
if (focused) {
MaterialTheme.colorScheme.inverseSurface
} else {
MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)
}
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
Box(
modifier =
Modifier
// .scale(if (focused) 1.1f else 1f)
.padding(2.dp)
.fillMaxSize()
.background(
background,
shape = RoundedCornerShape(4.dp),
),
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier =
Modifier
.fillMaxSize()
.padding(4.dp),
) {
Text(
text = program.name ?: program.id.toString(),
color = textColor,
maxLines = 1,
modifier = Modifier.enableMarquee(focused),
)
listOfNotNull(
program.seasonEpisode?.let { "S${it.season} E${it.episode}" },
program.subtitle,
).joinToString(" - ")
.ifBlank { null }
?.let {
Text(
text = it,
color = textColor,
modifier = Modifier,
)
}
}
RecordingMarker(
isRecording = program.isRecording,
isSeriesRecording = program.isSeriesRecording,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
}
channels(
count = channels.size,
layoutInfo = { channelIndex ->
ProgramGuideItem.Channel(channelIndex)
},
) { channelIndex ->
val channel = channels[channelIndex]
val focused =
gridHasFocus && channelColumnFocused && focusedChannelIndex == channelIndex
val background =
if (focused) {
MaterialTheme.colorScheme.inverseSurface
} else {
MaterialTheme.colorScheme.surface
}
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
Box(
modifier =
Modifier
.fillMaxSize()
.background(
background,
shape = RoundedCornerShape(4.dp),
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(4.dp)
.fillMaxSize(),
) {
Text(
text = channel.number ?: channel.name ?: channelIndex.toString(),
color = textColor,
modifier = Modifier,
)
AsyncImage(
model = channel.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxHeight(.66f),
)
}
}
}
topCorner {
Box(
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
)
}
}
channels(
count = channels.size,
layoutInfo = { channelIndex ->
ProgramGuideItem.Channel(channelIndex)
},
) { channelIndex ->
val channel = channels[channelIndex]
val focused =
gridHasFocus && channelColumnFocused && focusedChannelIndex == channelIndex
val background =
if (focused) {
MaterialTheme.colorScheme.inverseSurface
} else {
MaterialTheme.colorScheme.surface
}
val textColor = MaterialTheme.colorScheme.contentColorFor(background)
Box(
modifier =
Modifier
.fillMaxSize()
.background(
background,
shape = RoundedCornerShape(4.dp),
),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(4.dp)
.fillMaxSize(),
) {
Text(
text = channel.number ?: channel.name ?: channelIndex.toString(),
color = textColor,
modifier = Modifier,
)
AsyncImage(
model = channel.imageUrl,
contentDescription = null,
modifier = Modifier.fillMaxHeight(.66f),
)
}
}
}
topCorner {
Box(
if (loading) {
CircularProgress(
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surface),
.background(
MaterialTheme.colorScheme.background.copy(alpha = .5f),
shape = CircleShape,
).size(64.dp)
.padding(16.dp)
.align(Alignment.BottomEnd),
)
}
}