Looks: channel logos and icons read in Daybreak; screenshot comparison of every look
pauljd/davidson-ci: Davidson CI / quality (push) Failing after 1m49s
pauljd/davidson-ci: Davidson CI / ai-review (push) Failing after 1m37s

Ported from release 0.108.42. design/themes holds the comparison (12 screens in all six looks, rendered off the box).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-11-06 03:35:10 +00:00
co-authored by Claude Opus 5.5
parent 205126d244
commit 8f0d840983
16 changed files with 31 additions and 21 deletions
@@ -197,7 +197,7 @@ private fun CatchUpTile(entry: CatchUpEntry, nowMs: Long, on: Boolean, modifier:
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))
if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.logoPlate().width(60.dp).height(18.dp))
else Text(ChannelBrands.displayName(entry.channel), color = ink, fontSize = 11.sp, maxLines = 1)
Spacer(Modifier.weight(1f))
CatchUpMark()
@@ -19,7 +19,7 @@ internal fun ChannelTuningBackdrop(number: Int, modifier: Modifier = Modifier, a
artwork()
if (branding != null) {
Image(painterResource(branding.logo), null,
Modifier.align(Alignment.TopStart).padding(start = 40.dp, top = 32.dp).width(200.dp).height(62.dp))
Modifier.align(Alignment.TopStart).padding(start = 40.dp, top = 32.dp).logoPlate().width(200.dp).height(62.dp))
}
if (number == TOTS_NUMBER && totsMood(System.currentTimeMillis()).evening)
Image(painterResource(R.drawable.orbit_tots_sleepy), null, Modifier.align(Alignment.Center).size(180.dp))
@@ -383,7 +383,7 @@ internal fun DriveInScreen(session: EmbySampleSession, plan: ChannelPlan?, visit
// A booked event with an approved takeover shows its board; everything else, the Drive-In's own mark.
val takeover = selected?.presentation?.let { DriveInTakeovers[it] }
if (takeover != null) Image(painterResource(takeover), selected.title, Modifier.width(160.dp).height(50.dp), alignment = Alignment.CenterStart)
else Image(painterResource(R.drawable.orbit_drive_in_epg), "Orbit Drive-In", Modifier.width(110.dp).height(34.dp), alignment = Alignment.CenterStart)
else Image(painterResource(R.drawable.orbit_drive_in_epg), "Orbit Drive-In", Modifier.logoPlate().width(110.dp).height(34.dp), alignment = Alignment.CenterStart)
if (selected == null) {
Text("Tonight's bill is on its way.", color = Ink, fontFamily = Barlow, fontSize = 20.sp)
return@Column
@@ -160,7 +160,7 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
val branding = ChannelBrands.forNumber(channel.number)
// The picture stays on the tuned channel while the cursor browses, so moving rows never re-tunes.
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.width(96.dp).height(30.dp))
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.logoPlate().width(96.dp).height(30.dp))
else Text(ChannelBrands.displayName(channel), color = Ink, fontSize = 16.sp)
val (focusedName, focusedDetail) = if (kidsAsleep(takeoverOf(channel.number, focused))) "Asleep till 6am" to "Back for Kids in the morning" else guideTitleParts(focused.media.item.title)
Text(focusedName, color = Ink, fontFamily = Anton, fontSize = 20.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
@@ -221,7 +221,7 @@ internal fun GuideGrid(channels: List<SampleChannel>, selectedNumber: Int, modif
Row(Modifier.width(130.dp).fillMaxHeight().background(if (index == row) Blue else Panel, androidx.compose.foundation.shape.RoundedCornerShape(4.dp)).padding(start = 6.dp), verticalAlignment = Alignment.CenterVertically) {
Text(line.number.toString(), Modifier.width(30.dp), color = Yellow, fontFamily = BarlowCondensed, fontSize = 13.sp)
if (takeover != null) TakeoverMark(takeover, Modifier.width(92.dp), height = 18.dp, textSize = 13.sp)
else if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.width(88.dp).height(28.dp))
else if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.logoPlate().width(88.dp).height(28.dp))
else Text(ChannelBrands.displayName(line), color = Ink, fontSize = 13.sp, maxLines = 1)
}
BoxWithConstraints(Modifier.weight(1f).fillMaxHeight()) {
@@ -85,7 +85,7 @@ internal fun GuidePicture(session: EmbySampleSession, channel: SampleChannel, li
val branding = ChannelBrands.forNumber(channel.number)
if (!ready) Box(Modifier.matchParentSize().background(Panel)) {
ProgrammeArtwork(session, channel, current)
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.align(androidx.compose.ui.Alignment.Center).width(150.dp).height(47.dp))
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.align(androidx.compose.ui.Alignment.Center).logoPlate().width(150.dp).height(47.dp))
}
}
}
@@ -78,7 +78,7 @@ internal fun HomeScreen(channels: List<SampleChannel>, modifier: Modifier = Modi
homeCategories(channels.map { it.number }).forEachIndexed { index, category ->
HomeCard(Modifier.fillMaxWidth().height(31.dp).focusProperties { if (index == 0 && nav != null) up = nav }, if (index == 0) firstFocus else null, onClick = { onCategory(category) }) { focused ->
Row(Modifier.fillMaxSize().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Image(painterResource(category.icon), null, Modifier.size(18.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
Image(painterResource(category.icon), null, Modifier.size(18.dp), colorFilter = ColorFilter.tint(if (focused) OnAccent else Ink))
Text(category.label.uppercase(java.util.Locale.UK), color = if (focused) OnAccent else Ink, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, letterSpacing = .5.sp, maxLines = 1)
}
}
@@ -186,7 +186,7 @@ private fun OrbitApp() {
if (kidsArcade) NavItem("Arcade", section == "Arcade", null, below) { section = "Arcade" }
}
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Image(painterResource(R.drawable.orbit_logo), "Orbit", Modifier.width(144.dp).height(36.dp))
Image(painterResource(R.drawable.orbit_logo), "Orbit", Modifier.logoPlate().width(144.dp).height(36.dp))
Spacer(Modifier.weight(1f))
Column(horizontalAlignment = Alignment.End) {
Text(SimpleDateFormat("HH:mm", Locale.UK).format(Date(now)), color = Ink, fontFamily = BarlowCondensed, fontSize = 20.sp)
@@ -197,7 +197,7 @@ private fun OrbitApp() {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { ProfileAvatar(profile.picture, 28.dp, ringed = false) }
}
if (realChannelAvailable && !Profiles.kids) HomeCard(Modifier.size(38.dp, 32.dp).focusProperties { if (section == "Drive-In") down = driveInFocus }, onClick = { section = "Connect Emby" }) { focused ->
Image(painterResource(R.drawable.orbit_icon_gear), "Settings", Modifier.fillMaxSize().padding(5.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
Image(painterResource(R.drawable.orbit_icon_gear), "Settings", Modifier.fillMaxSize().padding(5.dp), colorFilter = ColorFilter.tint(if (focused) OnAccent else Ink))
}
}
}
@@ -233,7 +233,7 @@ private fun ChannelsStep(onNext: () -> Unit) {
.background(if (focused) Navy else Panel, corner).border(if (focused) 3.dp else 0.dp, if (focused) Yellow else Color.Transparent, corner)
.clickable { ticked = if (on) ticked - programming.number else ticked + programming.number }.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
val brand = ChannelBrands.forNumber(programming.number)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.width(84.dp).height(26.dp)) else Text(programming.name, color = Ink, fontSize = 13.sp)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.logoPlate().width(84.dp).height(26.dp)) else Text(programming.name, color = Ink, fontSize = 13.sp)
Spacer(Modifier.weight(1f))
Box(Modifier.size(20.dp).background(if (on) themed(Color(0xFF6EDCAA)) { Mint } else themed(Color(0xFF283C64)) { Blue }, RoundedCornerShape(5.dp)), contentAlignment = Alignment.Center) {
if (on) Text("✓", color = OnAccent, fontSize = 13.sp, fontWeight = FontWeight.Bold)
@@ -278,7 +278,7 @@ private fun ChannelsStep(onNext: () -> Unit) {
Row(Modifier.fillMaxWidth().height(40.dp).background(if (busy) Navy else Panel, RoundedCornerShape(8.dp)).border(if (busy) 2.dp else 0.dp, if (busy) Yellow else Color.Transparent, RoundedCornerShape(8.dp))
.padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
val brand = ChannelBrands.forNumber(number)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.width(80.dp).height(24.dp))
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.logoPlate().width(80.dp).height(24.dp))
Text(brand?.displayName ?: "$number", color = Ink, fontSize = 14.sp, modifier = Modifier.weight(1f))
Text(when {
count == null && busy -> "Matching…"
@@ -371,7 +371,7 @@ private fun GameCard(game: ArcadeGame, modifier: Modifier, focus: FocusRequester
.onFocusChanged { focused = it.isFocused; if (it.isFocused) onFocus() }
.background(if (focused) Navy else Panel, corner).border(if (focused) 3.dp else 1.dp, if (focused) Yellow else Blue, corner)
.clickable(onClick = onPlay).padding(horizontal = 10.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Canvas(Modifier.size(34.dp)) { gameIcon(game) }
Canvas(Modifier.logoPlate().size(34.dp)) { gameIcon(game) }
Box(Modifier.width(1.dp).fillMaxHeight(.7f).background(Muted.copy(alpha = .4f)))
Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(game.title.uppercase().takeIf { game == ArcadeGame.OrbMan } ?: game.title, color = Ink, fontFamily = Marquee, fontSize = 17.sp, lineHeight = 20.sp, maxLines = 1)
@@ -518,7 +518,7 @@ internal fun OrbitArcade(modifier: Modifier, onPlaying: (Boolean) -> Unit) {
Row(Modifier.weight(1f).fillMaxHeight().onFocusChanged { focused = it.isFocused; if (it.isFocused) chosen = game }
.background(if (focused) Navy else Panel, corner).border(if (focused) 3.dp else 0.dp, if (focused) Yellow else Color.Transparent, corner)
.clickable { play(game) }.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Canvas(Modifier.size(30.dp)) { gameIcon(game) }
Canvas(Modifier.logoPlate().size(30.dp)) { gameIcon(game) }
Column {
Text(game.title, color = Ink, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
Text(bestLine(game)?.let { "Your best: $it" } ?: "Played recently", color = Muted, fontSize = 10.sp, maxLines = 1)
@@ -167,7 +167,7 @@ internal fun WhosWatching(onClose: () -> Unit) {
}
Box(Modifier.fillMaxSize()) {
Column(Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(28.dp)) {
Image(painterResource(R.drawable.orbit_logo), "Orbit", Modifier.width(150.dp).height(38.dp))
Image(painterResource(R.drawable.orbit_logo), "Orbit", Modifier.logoPlate().width(150.dp).height(38.dp))
Text("Who's watching?", color = Ink, fontFamily = Anton, fontSize = 40.sp)
Row(horizontalArrangement = Arrangement.spacedBy(56.dp)) {
// Kids first, so a child's OK goes straight to their own.
@@ -260,7 +260,7 @@ internal fun ProfilesDetail(firstSwitch: FocusRequester, modifier: Modifier) {
.clickable { Profiles.update(context, kids.copy(channels = if (on) kids.channels - programming.number else kids.channels + programming.number)) }
.padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically) {
val brand = ChannelBrands.forNumber(programming.number)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.width(84.dp).height(26.dp)) else Text(programming.name, color = Ink, fontSize = 13.sp)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.logoPlate().width(84.dp).height(26.dp)) else Text(programming.name, color = Ink, fontSize = 13.sp)
Spacer(Modifier.weight(1f))
Box(Modifier.size(22.dp).background(if (on) themed(Color(0xFF6EDCAA)) { Mint } else themed(Color(0xFF283C64)) { Blue }, RoundedCornerShape(5.dp)), contentAlignment = Alignment.Center) {
if (on) Text("✓", color = OnAccent, fontSize = 14.sp, fontWeight = FontWeight.Bold)
@@ -82,7 +82,7 @@ internal fun PromoOverlay(session: EmbySampleSession, item: WhatsOnItem) {
Box(Modifier.fillMaxSize().background(Navy)) {
StarrySky()
Column(Modifier.align(Alignment.Center).padding(top = 10.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp)) {
ChannelBrands.forNumber(item.channel)?.let { Image(painterResource(it.logo), it.displayName, Modifier.width(150.dp).height(46.dp)) }
ChannelBrands.forNumber(item.channel)?.let { Image(painterResource(it.logo), it.displayName, Modifier.logoPlate().width(150.dp).height(46.dp)) }
Text(promoHeadline(item, now), color = Yellow, fontFamily = Anton, fontSize = 34.sp)
Box(Modifier.width(500.dp).height(190.dp).clip(RoundedCornerShape(12.dp)).background(Panel)) {
if (picture != null) Image(picture.asImageBitmap(), null, Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
@@ -150,7 +150,7 @@ private fun RailItem(section: SettingsSection, selected: Boolean, modifier: Modi
}.onFocusChanged { focused = it.isFocused; if (it.isFocused) onSelect() }.clip(RailCorner)
.background(if (focused) Yellow else if (selected) Blue else Panel).clickable(onClick = onSelect).padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Image(painterResource(section.icon), null, Modifier.size(26.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
Image(painterResource(section.icon), null, Modifier.size(26.dp), colorFilter = ColorFilter.tint(if (focused) OnAccent else Ink))
Text(section.label, color = if (focused) OnAccent else Ink, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
}
}
@@ -184,7 +184,7 @@ private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boole
Text(channelBlock(programming.number), Modifier.padding(top = 6.dp), color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
Row(Modifier.fillMaxWidth().clip(RailCorner).background(Panel).padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text(programming.number.toString(), Modifier.width(30.dp), color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.width(88.dp).height(28.dp))
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.logoPlate().width(88.dp).height(28.dp))
Text(brand?.displayName ?: programming.name, Modifier.weight(1f), color = Ink, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
if (programming.suggestionRules.isNotEmpty()) OrbitButton("Titles", enabled = !busy, onClick = { onSuggestions(programming) })
OrbitButton(if (on) "On" else "Off", Modifier.width(64.dp).then(if (programming == all.first()) Modifier.focusRequester(firstSwitch) else Modifier), selected = on, enabled = !busy, onClick = { onSwitch(programming, !on) })
@@ -61,7 +61,7 @@ internal fun SqueezePanel(session: EmbySampleSession, channel: SampleChannel, cu
val (name, detail) = guideTitleParts(next.media.item.title)
val seconds = ((next.startMs - nowMs) / 1000).coerceAtLeast(0)
Box(Modifier.fillMaxSize().graphicsLayer { alpha = shown }) {
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.padding(start = 36.dp, top = 14.dp).width(120.dp).height(38.dp))
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.padding(start = 36.dp, top = 14.dp).logoPlate().width(120.dp).height(38.dp))
// Beside the picture: what's next.
Column(Modifier.align(Alignment.TopEnd).padding(top = 60.dp, end = 36.dp).width(360.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
Text(if (keep) "KEEP WATCHING" else "NEXT ON ${(brand?.displayName ?: channel.name).removePrefix("Orbit ").uppercase()}", color = Yellow, fontFamily = Anton, fontSize = 22.sp, maxLines = 1)
@@ -6,6 +6,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Orbit's looks (Paul, 5 November 2026, design/first-run/themes.png): six palettes, chosen at the start of onboarding or in
@@ -122,3 +126,9 @@ internal val NightMuted = OrbitLook.Night.palette.muted
internal val NightDeep = OrbitLook.Night.palette.deep
internal val NightSky = OrbitLook.Night.palette.sky
internal val NightMint = OrbitLook.Night.palette.mint
/**
* The channels' logos and Orbit's own are white artwork made for dark screens. In a light look each sits on a small navy
* plate, like a channel's on-screen bug, so it keeps its own colours and still reads. In the dark looks nothing changes.
*/
internal fun Modifier.logoPlate(): Modifier = if (OrbitTheme.palette.light) background(NightNavy, RoundedCornerShape(4.dp)) else this
@@ -54,8 +54,8 @@ private fun Tag(text: String, colour: Color = tagColour(text), size: Int = 10) {
@Composable
private fun Logo(channel: Int, width: Int, height: Int) {
val brand = ChannelBrands.forNumber(channel)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.width(width.dp).height(height.dp), alignment = Alignment.CenterStart)
else if (channel == DRIVE_IN_NUMBER) Image(painterResource(R.drawable.orbit_drive_in_epg), "Orbit Drive-In", Modifier.width(width.dp).height(height.dp), alignment = Alignment.CenterStart)
if (brand != null) Image(painterResource(brand.logo), brand.displayName, Modifier.logoPlate().width(width.dp).height(height.dp), alignment = Alignment.CenterStart)
else if (channel == DRIVE_IN_NUMBER) Image(painterResource(R.drawable.orbit_drive_in_epg), "Orbit Drive-In", Modifier.logoPlate().width(width.dp).height(height.dp), alignment = Alignment.CenterStart)
}
private fun dayAndTime(startMs: Long, nowMs: Long): String {
Binary file not shown.