Organize composables

This commit is contained in:
Damontecres 2025-10-21 15:04:33 -04:00
parent 5656703080
commit efeecc4b98
No known key found for this signature in database
3 changed files with 255 additions and 153 deletions

View file

@ -0,0 +1,186 @@
package com.github.damontecres.wholphin.ui.detail.livetv
import androidx.compose.foundation.background
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.tv.material3.Button
import androidx.tv.material3.Icon
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode
import java.time.LocalDateTime
@Composable
fun ProgramDialog(
item: BaseItem?,
loading: LoadingState,
onDismissRequest: () -> Unit,
onWatch: () -> Unit,
onRecord: (series: Boolean) -> Unit,
onCancelRecord: (series: Boolean) -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
) {
val focusRequester = remember { FocusRequester() }
Box(
modifier =
Modifier
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp),
shape = RoundedCornerShape(16.dp),
).focusRequester(focusRequester),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(16.dp),
) {
when (val st = loading) {
is LoadingState.Error -> ErrorMessage(st)
LoadingState.Loading,
LoadingState.Pending,
->
CircularProgress(
Modifier
.padding(8.dp)
.size(48.dp),
)
LoadingState.Success ->
item?.let { item ->
val now = LocalDateTime.now()
val dto = item.data
val isRecording = dto.timerId.isNotNullOrBlank()
val isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank()
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Text(
text = item.name ?: "",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
)
if (dto.isSeries ?: false) {
listOfNotNull(dto.seasonEpisode, dto.episodeTitle)
.joinToString(" - ")
.ifBlank { null }
?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium,
)
}
}
dto.overview?.let { overview ->
Text(
text = overview,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
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)
}
},
) {
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

@ -0,0 +1,64 @@
package com.github.damontecres.wholphin.ui.detail.livetv
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
// .align(Alignment.BottomEnd)
@Composable
fun RecordingMarker(
isRecording: Boolean,
isSeriesRecording: Boolean,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
if (isSeriesRecording) {
val color = if (isRecording) Color.Red else Color.Gray
Box(
modifier =
Modifier
.padding(4.dp)
.size(16.dp)
.background(color, shape = CircleShape),
)
Box(
modifier =
Modifier
.padding(start = 4.dp, top = 4.dp, bottom = 4.dp, end = 10.dp)
.offset(6.dp)
.size(16.dp)
.background(color.copy(alpha = .5f), shape = CircleShape),
)
} else if (isRecording) {
Box(
modifier =
Modifier
.padding(4.dp)
.size(16.dp)
.background(Color.Red, shape = CircleShape),
)
}
}
}
@PreviewTvSpec
@Composable
private fun RecordingMarkerPreview() {
WholphinTheme {
Column {
RecordingMarker(true, false)
RecordingMarker(true, true)
RecordingMarker(false, true)
}
}
}

View file

@ -11,8 +11,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding 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.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -28,7 +26,6 @@ import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.key
@ -36,11 +33,8 @@ 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.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface import androidx.tv.material3.Surface
import androidx.tv.material3.SurfaceDefaults import androidx.tv.material3.SurfaceDefaults
@ -48,18 +42,14 @@ import androidx.tv.material3.Text
import androidx.tv.material3.contentColorFor import androidx.tv.material3.contentColorFor
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.ui.components.CircularProgress
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.enableMarquee import com.github.damontecres.wholphin.ui.enableMarquee
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
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 com.github.damontecres.wholphin.util.LoadingState
import com.github.damontecres.wholphin.util.seasonEpisode
import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader import com.google.common.cache.CacheLoader
import eu.wewox.programguide.ProgramGuide import eu.wewox.programguide.ProgramGuide
@ -180,121 +170,6 @@ fun TvGuideGrid(
} }
} }
@Composable
fun ProgramDialog(
item: BaseItem?,
loading: LoadingState,
onDismissRequest: () -> Unit,
onWatch: () -> Unit,
onRecord: (series: Boolean) -> Unit,
onCancelRecord: (series: Boolean) -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
) {
val focusRequester = remember { FocusRequester() }
Box(
modifier =
Modifier
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp),
shape = RoundedCornerShape(16.dp),
).focusRequester(focusRequester),
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.padding(16.dp),
) {
when (val st = loading) {
is LoadingState.Error -> ErrorMessage(st)
LoadingState.Loading,
LoadingState.Pending,
->
CircularProgress(
Modifier
.padding(8.dp)
.size(48.dp),
)
LoadingState.Success ->
item?.let { item ->
val now = LocalDateTime.now()
val dto = item.data
val isRecording = dto.timerId.isNotNullOrBlank()
val isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank()
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Text(
text = item.name ?: "",
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge,
)
if (dto.isSeries ?: false) {
listOfNotNull(dto.seasonEpisode, dto.episodeTitle)
.joinToString(" - ")
.ifBlank { null }
?.let {
Text(
text = it,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleMedium,
)
}
}
dto.overview?.let { overview ->
Text(
text = overview,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,
maxLines = 3,
)
}
if (dto.isSeries ?: false) {
Button(
onClick = {
if (isSeriesRecording) {
onCancelRecord.invoke(true)
} else {
onRecord.invoke(true)
}
},
) {
Text(
text = if (isSeriesRecording) "Cancel Series Recording" else "Record Series",
)
}
}
Button(
onClick = {
if (isRecording) {
onCancelRecord.invoke(false)
} else {
onRecord.invoke(false)
}
},
) {
Text(
text = if (isRecording) "Cancel Recording" else "Record Program",
)
}
if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) {
Button(
onClick = onWatch,
) {
Text(
text = "Watch live",
)
}
}
}
}
}
}
}
}
@Composable @Composable
fun TvGuideGrid( fun TvGuideGrid(
channels: List<TvChannel>, channels: List<TvChannel>,
@ -579,34 +454,11 @@ fun TvGuideGrid(
) )
} }
} }
if (program.isSeriesRecording) { RecordingMarker(
val color = if (program.isRecording) Color.Red else Color.Gray isRecording = program.isRecording,
Box( isSeriesRecording = program.isSeriesRecording,
modifier = modifier = Modifier.align(Alignment.BottomEnd),
Modifier )
.padding(4.dp)
.size(16.dp)
.background(color.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, 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),
)
}
} }
} }