Show duration & ends at time on next up card (#1690)

## Description
Adds the duration & ends at time to the next up card

### Related issues
Closes #1664

### Testing
<!-- Describe how this change was tested and on what device(s) -->

## Screenshots
<img width="660" height="241" alt="image"
src="https://github.com/user-attachments/assets/7551843e-4f53-43fc-af2a-3e503ba6d03c"
/>

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-07-13 01:30:54 -04:00 • committed by GitHub
parent f74306a12c
commit 077b7e4570
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 27 deletions

View file

@ -10,10 +10,8 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@ -21,6 +19,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -29,7 +28,10 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -47,10 +49,17 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.AspectRatios
import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.dot
import com.github.damontecres.wholphin.ui.formatDuration
import com.github.damontecres.wholphin.ui.formatTime
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.Clock
import com.github.damontecres.wholphin.ui.util.LocalClock
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.toJavaDuration
/**
* Layout for showing the next up episode during playback
@ -64,6 +73,7 @@ fun NextUpEpisode(
timeLeft: Duration?,
modifier: Modifier = Modifier,
aspectRatio: Float = AspectRatios.WIDE,
runtime: Duration? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
val focusRequester = remember { FocusRequester() }
@ -122,9 +132,36 @@ fun NextUpEpisode(
text = description ?: "",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 4,
overflow = TextOverflow.Ellipsis,
modifier = Modifier,
)
runtime?.let {
val context = LocalContext.current
val resources = LocalResources.current
val now by LocalClock.current.now
val text =
remember(now, runtime) {
buildAnnotatedString {
append(resources.formatDuration(runtime))
dot()
val endsAt = now.plus(runtime.toJavaDuration())
val endTimeStr = formatTime(context, endsAt)
append(resources.getString(R.string.ends_at, endTimeStr))
}
}
Text(
text = text,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.padding(top = 4.dp)
.align(Alignment.Start),
)
}
}
}
}
@ -189,34 +226,41 @@ fun NextUpCard(
@Composable
private fun NextUpEpisodePreview() {
WholphinTheme(true) {
val theme = MaterialTheme.colorScheme
val previewHandler =
AsyncImagePreviewHandler {
ColorImage(Color.Blue.toArgb())
ColorImage(theme.surfaceVariant.toArgb())
}
Box(
modifier = Modifier.fillMaxSize().background(Color.Black),
) {
CompositionLocalProvider(
LocalAsyncImagePreviewHandler provides previewHandler,
LocalClock provides Clock(),
) {
NextUpEpisode(
title = "Episode Title",
description =
"This is the description of the episode. It might be long. " +
"It might be very, very long and overflow the available space. " +
"But it just keeps going and going.",
imageUrl = "",
onClick = {},
aspectRatio = AspectRatios.WIDE,
timeLeft = 30.seconds,
runtime = 32.minutes + 20.seconds,
modifier =
Modifier
.padding(8.dp)
.fillMaxHeight(.4f)
.fillMaxWidth(.66f)
.align(Alignment.BottomCenter)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
shape = RoundedCornerShape(8.dp),
),
)
}
CompositionLocalProvider(LocalAsyncImagePreviewHandler provides previewHandler) {
NextUpEpisode(
title = "Episode Title",
description =
"This is the description of the episode. It might be long. " +
"It might be very, very long and overflow the available space. " +
"But it just keeps going and going.",
imageUrl = "",
onClick = {},
aspectRatio = AspectRatios.FOUR_THREE,
timeLeft = 30.seconds,
modifier =
Modifier
.padding(16.dp)
.height(200.dp)
.width(400.dp)
// .fillMaxWidth(.4f)
// .align(Alignment.BottomCenter)
.background(
MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
shape = RoundedCornerShape(8.dp),
),
)
}
}
}

View file

@ -94,6 +94,7 @@ import com.github.damontecres.wholphin.util.Media3SubtitleOverride
import io.github.peerless2012.ass.media.widget.AssSubtitleView
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@ -600,6 +601,7 @@ fun PlaybackPageContent(
viewModel.playNextUp()
},
timeLeft = if (autoPlayEnabled) timeLeft.seconds else null,
runtime = it.data.runTimeTicks?.ticks,
modifier =
Modifier
.padding(8.dp)