Fix guide sizing and use one font across the app (0.25.0)
Guide rows no longer stretch to fill the screen, the top panel is smaller, and Barlow replaces the system font so text matches the logos. Adds the Orbit logo to the header. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Orbit TV 0.25.0 — guide sizing and one font
|
||||
|
||||
Paul's feedback on 0.24, with a photo: the grid worked, but rows were huge because they shared the free height between only two channels; the top panel was slightly big; and text didn't match the logos.
|
||||
|
||||
Changes: guide rows are a fixed 40 dp, so they don't grow with fewer channels, and the top panel is smaller. Barlow is now the default font across the app, matching the logos and designs, where most text used the system font. The header uses the new Orbit logo in place of the placeholder and "Classic". In the grid, episodes show series and number, with the full title in the panel above, and cells too narrow to read show no text.
|
||||
|
||||
Not changed: the colours in code. In Paul's photos the navy looks much brighter and bluer than its value, so the TV's picture mode or the camera may be involved. That's unconfirmed.
|
||||
|
||||
Validation: offline assembleDebug, testDebugUnitTest and lintDebug passed; 127 unit tests, zero failures or errors, no lint errors. The global font change touches every screen, and its look needs the Xiaomi test, particularly anywhere text may now wrap differently.
|
||||
|
||||
Rollback: reinstall outputs/revision-0.24.0, or revert this commit. Data formats are unchanged.
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId = "tv.orbit.classic"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 24
|
||||
versionName = "0.24.0-grid-guide"
|
||||
versionCode = 25
|
||||
versionName = "0.25.0-guide-sizing"
|
||||
}
|
||||
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.24.0\"")
|
||||
connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.25.0\"")
|
||||
request.token?.let { connection.setRequestProperty("X-Emby-Token", it) }
|
||||
if (request.method == "POST") {
|
||||
val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8)
|
||||
|
||||
@@ -46,6 +46,12 @@ 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. */
|
||||
internal fun guideTitleParts(title: String): Pair<String, String?> {
|
||||
val parts = title.split(" / ")
|
||||
return parts[0] to parts.getOrNull(1)
|
||||
}
|
||||
|
||||
/** 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))
|
||||
@@ -77,17 +83,17 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
slotsAhead = ahead
|
||||
}
|
||||
Column(modifier.fillMaxWidth().background(Navy).padding(horizontal = 14.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
||||
Row(Modifier.fillMaxWidth().height(126.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(Modifier.fillMaxWidth().height(96.dp), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
val branding = ChannelBrands.forNumber(channel.number)
|
||||
Box(Modifier.width(224.dp).fillMaxHeight().background(Panel)) {
|
||||
Box(Modifier.width(170.dp).fillMaxHeight().background(Panel)) {
|
||||
if (branding != null) Image(painterResource(branding.background), null, Modifier.matchParentSize(), contentScale = ContentScale.Crop)
|
||||
}
|
||||
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.width(112.dp).height(35.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 = 21.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text("${clock(focused.startMs)} to ${clock(focused.endMs)} • ${channel.number} ${ChannelBrands.displayName(channel)}", color = Muted, fontSize = 13.sp, maxLines = 1)
|
||||
Text(if (focused.startMs <= now) "OK Watch" else "On later", color = Muted, fontSize = 12.sp)
|
||||
Text(focused.media.item.title, color = Color.White, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, 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)
|
||||
}
|
||||
Column(verticalArrangement = Arrangement.Top) {
|
||||
OrbitButton("Test transition", Modifier.focusRequester(toolsFocus).onPreviewKeyEvent { event ->
|
||||
@@ -95,13 +101,13 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
}, onClick = { onWatch(channel.number, channel.transitionCheckOffset(System.currentTimeMillis())) })
|
||||
}
|
||||
}
|
||||
Row(Modifier.fillMaxWidth().height(24.dp).background(Panel), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("All Channels", Modifier.width(150.dp).padding(start = 8.dp), color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
Row(Modifier.fillMaxWidth().height(20.dp).background(Panel), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("All Channels", Modifier.width(130.dp).padding(start = 8.dp), color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
||||
Row(Modifier.weight(1f)) {
|
||||
repeat(GUIDE_SLOTS) { Text(clock(windowStart + it * GUIDE_SLOT_MS), Modifier.weight(1f).padding(start = 4.dp), color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold) }
|
||||
repeat(GUIDE_SLOTS) { Text(clock(windowStart + it * GUIDE_SLOT_MS), Modifier.weight(1f).padding(start = 4.dp), color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold) }
|
||||
}
|
||||
}
|
||||
Column(Modifier.fillMaxWidth().weight(1f).focusRequester(gridFocus).onFocusChanged { gridFocused = it.isFocused }.focusable().onPreviewKeyEvent { event ->
|
||||
Column(Modifier.fillMaxWidth().focusRequester(gridFocus).onFocusChanged { gridFocused = it.isFocused }.focusable().onPreviewKeyEvent { event ->
|
||||
val native = event.nativeKeyEvent
|
||||
if (native.action != KeyEvent.ACTION_DOWN) return@onPreviewKeyEvent false
|
||||
when (native.keyCode) {
|
||||
@@ -116,13 +122,13 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
}, verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||
channels.forEachIndexed { index, line ->
|
||||
Row(Modifier.fillMaxWidth().weight(1f).widthIn(max = 2000.dp), horizontalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
Row(Modifier.fillMaxWidth().height(40.dp), horizontalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
val mark = ChannelBrands.forNumber(line.number)
|
||||
Row(Modifier.width(150.dp).fillMaxHeight().background(Panel).padding(start = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(line.number.toString(), Modifier.width(34.dp), color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
|
||||
if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.width(104.dp).height(32.dp))
|
||||
Row(Modifier.width(130.dp).fillMaxHeight().background(Panel).padding(start = 6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(line.number.toString(), Modifier.width(30.dp), color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
|
||||
if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.width(88.dp).height(28.dp))
|
||||
else Text(ChannelBrands.displayName(line), color = Color.White, fontSize = 13.sp, maxLines = 1)
|
||||
}
|
||||
BoxWithConstraints(Modifier.weight(1f).fillMaxHeight()) {
|
||||
@@ -131,7 +137,15 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
val onNow = cell.show.startMs <= now
|
||||
Box(Modifier.offset(x = maxWidth * cell.startFraction).width((maxWidth * cell.widthFraction - 2.dp).coerceAtLeast(1.dp)).fillMaxHeight()
|
||||
.background(if (isFocus) Yellow else if (onNow) Blue else Panel).padding(horizontal = 6.dp), contentAlignment = Alignment.CenterStart) {
|
||||
Text(cell.show.media.item.title, color = if (isFocus) Navy else Color.White, fontSize = 14.sp, fontWeight = if (isFocus) FontWeight.SemiBold else FontWeight.Normal, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (now in windowStart until windowEnd)
|
||||
@@ -140,6 +154,6 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("▲▼ Channel ◄► Time OK Watch Back Menu", color = Muted, fontSize = 12.sp)
|
||||
Text("▲▼ Channel ◄► Time OK Watch Back Menu", color = Muted, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,8 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setContent { MaterialTheme { OrbitApp() } }
|
||||
// One typeface everywhere, matching the channel logos.
|
||||
setContent { MaterialTheme { CompositionLocalProvider(androidx.tv.material3.LocalTextStyle provides androidx.compose.ui.text.TextStyle(fontFamily = Barlow)) { OrbitApp() } } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +140,7 @@ private fun OrbitApp() {
|
||||
}
|
||||
} else Column(Modifier.fillMaxSize().padding(horizontal = 36.dp, vertical = 16.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("◉ orbit", color = Color.White, fontSize = 30.sp, fontWeight = FontWeight.Bold)
|
||||
Text(" / Classic", color = Muted, fontSize = 19.sp)
|
||||
androidx.compose.foundation.Image(androidx.compose.ui.res.painterResource(R.drawable.orbit_logo), "Orbit", Modifier.width(144.dp).height(36.dp))
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(SimpleDateFormat("EEE d MMM HH:mm", Locale.UK).format(Date(now)), color = Color.White, fontSize = 17.sp)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Text
|
||||
|
||||
private val Barlow = FontFamily(Font(R.font.barlow_regular), Font(R.font.barlow_semibold, FontWeight.SemiBold))
|
||||
internal val Barlow = FontFamily(Font(R.font.barlow_regular), Font(R.font.barlow_semibold, FontWeight.SemiBold), Font(R.font.barlow_semibold, FontWeight.Bold))
|
||||
private val TicketType = FontFamily(Font(R.font.barlow_condensed_medium))
|
||||
private val Paper = Color(0xFFF7EAC8)
|
||||
|
||||
|
||||
@@ -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.24.0")
|
||||
next.setRequestProperty("User-Agent", "OrbitTV/0.25.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()) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@@ -27,6 +27,10 @@ class GuideGridTest {
|
||||
val cells = guideCells(longOne, start, start + GUIDE_SLOT_MS)
|
||||
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("The 'Burbs" to null, guideTitleParts("The 'Burbs"))
|
||||
}
|
||||
@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