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.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
@ -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.onFocusChanged
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
@ -36,11 +33,8 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Surface
import androidx.tv.material3.SurfaceDefaults
@ -48,18 +42,14 @@ import androidx.tv.material3.Text
import androidx.tv.material3.contentColorFor
import androidx.tv.material3.surfaceColorAtElevation
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.LoadingPage
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.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
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.CacheLoader
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
fun TvGuideGrid(
channels: List<TvChannel>,
@ -579,34 +454,11 @@ fun TvGuideGrid(
)
}
}
if (program.isSeriesRecording) {
val color = if (program.isRecording) Color.Red else Color.Gray
Box(
modifier =
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),
)
}
RecordingMarker(
isRecording = program.isRecording,
isSeriesRecording = program.isSeriesRecording,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
}