Catch Up opens on the channel's ident; focus as on What's On (from 0.108.23)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -192,20 +192,21 @@ internal fun CatchUpScreen(channels: List<SampleChannel>, modifier: Modifier = M
|
||||
@Composable
|
||||
private fun CatchUpTile(entry: CatchUpEntry, nowMs: Long, on: Boolean, modifier: Modifier) {
|
||||
val corner = RoundedCornerShape(6.dp)
|
||||
val ink = if (on) Navy else Color.White
|
||||
Column(modifier.height(92.dp).background(if (on) Yellow else Panel, corner).padding(horizontal = 10.dp, vertical = 7.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
// Focused as on What's On: navy inside a gold ring, so the words stay crisp rather than washed out in gold.
|
||||
val ink = Color.White
|
||||
Column(modifier.height(92.dp).background(if (on) Navy else Panel, corner).border(if (on) 3.dp else 0.dp, if (on) Yellow else Color.Transparent, corner).padding(horizontal = 10.dp, vertical = 7.dp), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val mark = ChannelBrands.forNumber(entry.channel.number)
|
||||
if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.width(60.dp).height(18.dp))
|
||||
else Text(ChannelBrands.displayName(entry.channel), color = ink, fontSize = 11.sp, maxLines = 1)
|
||||
Spacer(Modifier.weight(1f))
|
||||
CatchUpMark(if (on) Navy else CatchUpGreen)
|
||||
CatchUpMark()
|
||||
}
|
||||
val (name, detail) = guideTitleParts(entry.show.media.item.title)
|
||||
Text(name, color = ink, fontSize = 14.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(detail ?: "${entry.show.media.durationMs / 60_000} min", color = if (on) Navy else Muted, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(detail ?: "${entry.show.media.durationMs / 60_000} min", color = Muted, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("${guideDayPrefix(entry.show.startMs, nowMs).trim().ifEmpty { "Ended" }.let { if (it == "Ended") "Ended ${clock(entry.show.endMs)}" else "$it ${clock(entry.show.startMs)}" }}",
|
||||
color = if (on) Navy else Muted, fontSize = 11.sp, maxLines = 1)
|
||||
color = if (on) Yellow else Muted, fontSize = 11.sp, maxLines = 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
|
||||
/** Catch Up: moves the channel's replay clock by this much (a skip, or time spent paused). */
|
||||
onShift: (Long) -> Unit = {},
|
||||
/** Catch Up: back to the channel live. */
|
||||
onLive: () -> Unit = {}) {
|
||||
onLive: () -> Unit = {},
|
||||
/** Catch Up: held still at its start while the channel's ident plays over it, then plays. */
|
||||
holdStart: Boolean = false) {
|
||||
val context = LocalContext.current
|
||||
val logoSetting = remember { OrbitSettings(context).channelLogo }
|
||||
var failure by remember { mutableStateOf<String?>(null) }
|
||||
@@ -114,9 +116,11 @@ internal fun EmbyPlaybackDialog(session: EmbySampleSession, plan: EmbyPlaybackPl
|
||||
setMediaItem(channelMediaItem(plan))
|
||||
if (scheduled != null) seekTo(scheduled.programme.offsetAt(scheduled.nowMs()))
|
||||
trackSelectionParameters = trackSelectionParameters.buildUpon().setPreferredAudioLanguage(audioPreference?.value ?: "en").build()
|
||||
prepare(); playWhenReady = true
|
||||
prepare(); playWhenReady = !holdStart
|
||||
}
|
||||
}
|
||||
val wasHeld = remember { booleanArrayOf(holdStart) }
|
||||
LaunchedEffect(holdStart) { if (!holdStart && wasHeld[0]) { wasHeld[0] = false; player.play() } }
|
||||
LaunchedEffect(player) { onPlayer?.invoke(player) }
|
||||
fun textTracks() = player.currentTracks.groups.filter { it.type == C.TRACK_TYPE_TEXT && it.isSupported }
|
||||
/** The languages of the programme's sound tracks, for [subtitleChoice]. */
|
||||
|
||||
@@ -230,15 +230,17 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
// Runs along the foot of what's on now, so the time reads down the rows without crossing a title.
|
||||
val elapsed = if (onNow && nowFraction != null) (maxWidth * (nowFraction - cell.startFraction)).coerceIn(0.dp, cellWidth.coerceAtLeast(0.dp)) else 0.dp
|
||||
Box(Modifier.offset(x = maxWidth * cell.startFraction).width(cellWidth.coerceAtLeast(1.dp)).fillMaxHeight()
|
||||
.background(if (isFocus) Yellow else if (past) catchUpDim() else if (onNow) Panel else GuideLater, androidx.compose.foundation.shape.RoundedCornerShape(4.dp)), contentAlignment = Alignment.CenterStart) {
|
||||
.background(if (isFocus) Navy else if (past) catchUpDim() else if (onNow) Panel else GuideLater, androidx.compose.foundation.shape.RoundedCornerShape(4.dp))
|
||||
// Focused as on What's On: navy inside a gold ring, the title crisp in white.
|
||||
.border(if (isFocus) 2.dp else 0.dp, if (isFocus) Yellow else Color.Transparent, androidx.compose.foundation.shape.RoundedCornerShape(4.dp)), contentAlignment = Alignment.CenterStart) {
|
||||
val runsOn = cell.show.endMs > windowEnd
|
||||
val ink = if (isFocus) Navy else if (past) Muted else Color.White
|
||||
if (elapsed > 0.dp) Box(Modifier.align(Alignment.BottomStart).width(elapsed).height(3.dp).background(if (isFocus) Navy else Yellow))
|
||||
val ink = if (isFocus) Color.White else if (past) Muted else Color.White
|
||||
if (elapsed > 0.dp) Box(Modifier.align(Alignment.BottomStart).width(elapsed).height(3.dp).background(Yellow))
|
||||
Row(Modifier.padding(horizontal = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(if (cellWidth >= GuideTitleMinWidth) guideTitleParts(cell.show.media.item.title).first else "…", Modifier.weight(1f, fill = false), color = ink, fontSize = 13.sp,
|
||||
fontWeight = if (isFocus || onNow) FontWeight.SemiBold else FontWeight.Normal, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (runsOn && cellWidth >= GuideTitleMinWidth) Text(" ►", color = ink, fontSize = 9.sp)
|
||||
if (past && cellWidth >= GuideTitleMinWidth + 14.dp) { Spacer(Modifier.width(4.dp)); CatchUpMark(if (isFocus) Navy else CatchUpGreen) }
|
||||
if (past && cellWidth >= GuideTitleMinWidth + 14.dp) { Spacer(Modifier.width(4.dp)); CatchUpMark() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@ internal fun IdentOverlay(session: EmbySampleSession, channel: SampleChannel, ne
|
||||
/** Filling time after a programme ended early: says when the next one starts, and keeps to a still picture if idents are off. */
|
||||
early: Boolean = false,
|
||||
/** Entering a block or event with its own ident: that film instead of the channel's, with anything drawn over it. */
|
||||
block: BlockIdent? = null) {
|
||||
block: BlockIdent? = null,
|
||||
/** In place of "NEXT" over the title, such as Catch Up's. */
|
||||
heading: String? = null) {
|
||||
val context = LocalContext.current
|
||||
val mood = if (channel.number == TOTS_NUMBER) totsMood(next.startMs) else null
|
||||
val settings = remember { OrbitSettings(context) }
|
||||
@@ -131,7 +133,7 @@ internal fun IdentOverlay(session: EmbySampleSession, channel: SampleChannel, ne
|
||||
}
|
||||
// A booked event's own lower third says what's next instead.
|
||||
if (block?.overlay != BlockOverlay.TonightsEvent || eventTitle(next) == null) Column(Modifier.align(Alignment.BottomStart).padding(start = 40.dp, bottom = 32.dp).background(Navy.copy(alpha = .72f)).padding(horizontal = 14.dp, vertical = 8.dp)) {
|
||||
Text(if (early) "NEXT AT ${clock(next.startMs)}" else "NEXT", color = Color(0xFFD5DCEA), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
Text(heading ?: if (early) "NEXT AT ${clock(next.startMs)}" else "NEXT", color = Color(0xFFD5DCEA), fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
Text(guideTitleParts(next.media.item.title).first, color = Color.White, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,22 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC
|
||||
delay((held.startMs + IDENT_HOLD_MS - System.currentTimeMillis()).coerceAtLeast(0))
|
||||
if (heldIdent === held) heldIdent = null
|
||||
}
|
||||
// Catch Up opens on the channel's ident (Paul, 5 November 2026) while the programme loads silently underneath, held at its
|
||||
// start, then cuts to it once both the ident has played and the programme's picture is up. The replay clock stands still meanwhile.
|
||||
val catchUpFor = remember { if (clockOffsetMs < 0 && identsOn) current.startMs else null }
|
||||
var catchUpIdentDone by remember { mutableStateOf(catchUpFor == null) }
|
||||
var catchUpPicture by remember { mutableStateOf(catchUpFor == null) }
|
||||
LaunchedEffect(Unit) {
|
||||
if (catchUpFor == null) return@LaunchedEffect
|
||||
delay(identLength(channel.number, catchUpFor)); catchUpIdentDone = true
|
||||
// However the programme fares, the ident never stays up long.
|
||||
delay(15_000); catchUpPicture = true
|
||||
}
|
||||
LaunchedEffect(plan == null, clockOffsetMs < 0) {
|
||||
if (plan != null || clockOffsetMs >= 0) return@LaunchedEffect
|
||||
var last = android.os.SystemClock.elapsedRealtime()
|
||||
while (true) { delay(500); val at = android.os.SystemClock.elapsedRealtime(); onShift(-(at - last)); last = at }
|
||||
}
|
||||
// One window for the whole visit, so a changeover never shows what's behind it.
|
||||
val window = remember { PlaybackWindow() }
|
||||
Dialog(onDismissRequest = { window.onBack() }, properties = DialogProperties(usePlatformDefaultWidth = false,
|
||||
@@ -149,7 +165,8 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC
|
||||
onHandBack = { running ->
|
||||
if (handoff != null && clockOffsetMs == 0L) { handoff.returned = LiveStream(channel.number, current.startMs, plan, running); true } else false
|
||||
},
|
||||
window = window, onShift = onShift, onLive = onLive, onFirstFrame = { if (heldIdent?.startMs == current.startMs) heldIdent = null },
|
||||
window = window, onShift = onShift, onLive = onLive, holdStart = !catchUpIdentDone,
|
||||
onFirstFrame = { if (heldIdent?.startMs == current.startMs) heldIdent = null; catchUpPicture = true },
|
||||
onEndedEarly = {
|
||||
val at = System.currentTimeMillis() + clockOffsetMs
|
||||
if (channel is ScheduledChannel && next.startMs == current.endMs && finishedEarly(at, current.endMs) && heldIdent == null) {
|
||||
@@ -183,6 +200,9 @@ internal fun ScheduledChannelPlayer(session: EmbySampleSession, channel: SampleC
|
||||
IdentOverlay(session, channel, held, length, startedLate, early, heldAs)
|
||||
}
|
||||
}
|
||||
if (catchUpFor != null && problem == null && (!catchUpIdentDone || !catchUpPicture)) key("catch-up-ident") {
|
||||
IdentOverlay(session, channel, current, identLength(channel.number, catchUpFor), 0, heading = "CATCH UP")
|
||||
}
|
||||
introPlayback?.let { IntroOverlay(it) }
|
||||
if (SleepTimer.warning) SleepWarning(Modifier.align(Alignment.TopCenter).padding(top = 44.dp))
|
||||
} }
|
||||
|
||||
Reference in New Issue
Block a user