Move episode numbers to the guide panel, test the now line (0.26.0)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Orbit TV 0.26.0 — guide titles and now line
|
||||
|
||||
Includes 0.25, which wasn't installed on its own. From Paul's review: episode numbers leave the grid and appear only in the panel above, as the series name then "S1 E4 • Name". Grid cells show one line, cut with an ellipsis inside their width. The now line's position is now a tested function: it follows the clock, leaves when the window scrolls past now, and returns to the same place when scrolling back.
|
||||
|
||||
Validation: offline assembleDebug, testDebugUnitTest and lintDebug passed, with tests for the line and for title splitting. On-screen truncation and the line while scrolling need the Xiaomi test.
|
||||
|
||||
Rollback: reinstall outputs/revision-0.24.0, or revert the commits. Data formats are unchanged.
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 25
|
||||
versionName = "0.25.0-guide-sizing"
|
||||
versionCode = 26
|
||||
versionName = "0.26.0-guide-titles"
|
||||
}
|
||||
buildFeatures { compose = true }
|
||||
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
||||
|
||||
@@ -41,7 +41,7 @@ internal class HttpsEmbyTransport(internal val deviceId: String = UUID.randomUUI
|
||||
connection.connectTimeout = 10_000
|
||||
connection.readTimeout = request.readTimeoutMs ?: if (request.path == "/Sessions/Logout") 5_000 else 15_000
|
||||
connection.setRequestProperty("Accept", "application/json")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.25.0\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.26.0\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -46,12 +46,16 @@ internal fun guideCells(channel: SampleChannel, windowStart: Long, windowEnd: Lo
|
||||
|
||||
internal fun guideWindowStart(nowMs: Long, slotsAhead: Int) = Math.floorDiv(nowMs, GUIDE_SLOT_MS) * GUIDE_SLOT_MS + slotsAhead * GUIDE_SLOT_MS
|
||||
|
||||
/** Episodes arrive as "Series / S1 E4 / Name". The grid shows the series and the number; the panel above has the rest. */
|
||||
/** Episodes arrive as "Series / S1 E4 / Name". The grid shows only the series; the panel above has the episode. */
|
||||
internal fun guideTitleParts(title: String): Pair<String, String?> {
|
||||
val parts = title.split(" / ")
|
||||
return parts[0] to parts.getOrNull(1)
|
||||
return parts[0] to parts.drop(1).joinToString(" • ").ifBlank { null }
|
||||
}
|
||||
|
||||
/** Where the now line sits across the window, or null when now isn't in view. */
|
||||
internal fun guideNowFraction(nowMs: Long, windowStart: Long, windowEnd: Long): Float? =
|
||||
if (nowMs in windowStart until windowEnd) (nowMs - windowStart).toFloat() / (windowEnd - windowStart) else null
|
||||
|
||||
/** Cursor time after moving one programme along a channel, never earlier than now. */
|
||||
internal fun guideStep(channel: SampleChannel, cursorMs: Long, nowMs: Long, forward: Boolean): Long {
|
||||
val show = channel.at(maxOf(cursorMs, nowMs))
|
||||
@@ -91,7 +95,9 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.width(90.dp).height(28.dp))
|
||||
else Text(ChannelBrands.displayName(channel), color = Color.White, fontSize = 16.sp)
|
||||
Text(focused.media.item.title, color = Color.White, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
val (focusedName, focusedDetail) = guideTitleParts(focused.media.item.title)
|
||||
Text(focusedName, color = Color.White, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (focusedDetail != null) Text(focusedDetail, color = Color.White, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("${clock(focused.startMs)} to ${clock(focused.endMs)} • ${channel.number} ${ChannelBrands.displayName(channel)}", color = Muted, fontSize = 12.sp, maxLines = 1)
|
||||
Text(if (focused.startMs <= now) "OK Watch" else "On later", color = Muted, fontSize = 11.sp)
|
||||
}
|
||||
@@ -139,17 +145,13 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
.background(if (isFocus) Yellow else if (onNow) Blue else Panel).padding(horizontal = 6.dp), contentAlignment = Alignment.CenterStart) {
|
||||
// Too narrow for a readable word, so the panel above carries the name.
|
||||
if (cell.widthFraction >= .09f) {
|
||||
val (name, detail) = guideTitleParts(cell.show.media.item.title)
|
||||
val ink = if (isFocus) Navy else Color.White
|
||||
Column {
|
||||
Text(name, color = ink, fontSize = 13.sp, fontWeight = if (isFocus) FontWeight.SemiBold else FontWeight.Normal, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (detail != null) Text(detail, color = if (isFocus) Navy else Muted, fontSize = 11.sp, maxLines = 1)
|
||||
}
|
||||
Text(guideTitleParts(cell.show.media.item.title).first, color = if (isFocus) Navy else Color.White, fontSize = 13.sp, fontWeight = if (isFocus) FontWeight.SemiBold else FontWeight.Normal, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (now in windowStart until windowEnd)
|
||||
Box(Modifier.offset(x = maxWidth * ((now - windowStart).toFloat() / (windowEnd - windowStart))).width(2.dp).fillMaxHeight().background(Color.White))
|
||||
guideNowFraction(now, windowStart, windowEnd)?.let { fraction ->
|
||||
Box(Modifier.offset(x = maxWidth * fraction).width(2.dp).fillMaxHeight().background(Color.White))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ internal class SecureMediaDataSource(private val session: EmbySampleSession) : B
|
||||
next.instanceFollowRedirects = false
|
||||
next.connectTimeout = 10_000
|
||||
next.readTimeout = 20_000
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.25.0")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.26.0")
|
||||
next.setRequestProperty("Accept-Encoding", "identity")
|
||||
session.mediaHeaders(address).forEach { (key, value) -> next.setRequestProperty(key, value) }
|
||||
if (dataSpec.position != 0L || dataSpec.length != C.LENGTH_UNSET.toLong()) {
|
||||
|
||||
@@ -28,9 +28,18 @@ class GuideGridTest {
|
||||
assertEquals(1, cells.size); assertEquals(1f, cells[0].widthFraction, .001f)
|
||||
}
|
||||
@Test fun episodeTitlesSplitIntoSeriesAndNumberAndFilmsStayWhole() {
|
||||
assertEquals("Dragon Ball Z" to "S1 E4", guideTitleParts("Dragon Ball Z / S1 E4 / Piccolo's Plan"))
|
||||
assertEquals("Dragon Ball Z" to "S1 E4 • Piccolo's Plan", guideTitleParts("Dragon Ball Z / S1 E4 / Piccolo's Plan"))
|
||||
assertEquals("The 'Burbs" to null, guideTitleParts("The 'Burbs"))
|
||||
}
|
||||
@Test fun theNowLineFollowsTheClockAndLeavesWhenTheWindowScrollsPastIt() {
|
||||
assertEquals(0f, guideNowFraction(start, start, end)!!, .001f)
|
||||
assertEquals(.25f, guideNowFraction(start + GUIDE_SLOT_MS, start, end)!!, .001f)
|
||||
assertNull(guideNowFraction(end, start, end))
|
||||
val scrolled = guideWindowStart(start + minute, 4)
|
||||
assertNull(guideNowFraction(start + minute, scrolled, scrolled + GUIDE_SLOTS * GUIDE_SLOT_MS))
|
||||
val back = guideWindowStart(start + minute, 0)
|
||||
assertEquals(minute.toFloat() / (GUIDE_SLOTS * GUIDE_SLOT_MS), guideNowFraction(start + minute, back, back + GUIDE_SLOTS * GUIDE_SLOT_MS)!!, .001f)
|
||||
}
|
||||
@Test fun theWindowStartsOnTheHalfHourAndMovesBySlots() {
|
||||
assertEquals(0L, start % GUIDE_SLOT_MS)
|
||||
assertEquals(start + 2 * GUIDE_SLOT_MS, guideWindowStart(start + minute, 2))
|
||||
|
||||
Reference in New Issue
Block a user