From abf00ea3dd59e30e9edba9fa46d7749c741b25d5 Mon Sep 17 00:00:00 2001 From: Pauljd Date: Mon, 2 Nov 2026 09:37:54 +0000 Subject: [PATCH] Simplify the guide: no finished programmes, no empty blocks, no time line (0.55.0) Co-Authored-By: Claude Opus 5.5 (1M context) --- android/app/build.gradle.kts | 4 +- .../main/java/tv/orbit/classic/EmbyClient.kt | 2 +- .../main/java/tv/orbit/classic/GuideGrid.kt | 51 +++++++++--------- .../tv/orbit/classic/SecureMediaDataSource.kt | 2 +- .../main/res/drawable-nodpi/orbit_x_bug.webp | Bin 1894 -> 1880 bytes .../main/res/drawable-nodpi/orbit_x_epg.webp | Bin 3610 -> 3550 bytes .../tv/orbit/classic/ChannelScheduleTest.kt | 22 ++++++-- .../java/tv/orbit/classic/GuideGridTest.kt | 9 ---- 8 files changed, 48 insertions(+), 42 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 5ab94eb..bd31b8d 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -9,8 +9,8 @@ android { applicationId = "tv.orbit.classic" minSdk = 26 targetSdk = 36 - versionCode = 55 - versionName = "0.54.0-logo-fixes" + versionCode = 56 + versionName = "0.55.0-guide-distilled" } buildFeatures { compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 } diff --git a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt index 3d80698..a6d38ae 100644 --- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt +++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt @@ -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.54.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.55.0\"") request.token?.let { connection.setRequestProperty("X-Emby-Token", it) } if (request.method == "POST") { val bytes = request.body.orEmpty().toByteArray(Charsets.UTF_8) diff --git a/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt b/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt index d272f0d..54f134f 100644 --- a/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt +++ b/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt @@ -61,17 +61,20 @@ internal fun guideDayPrefix(timeMs: Long, nowMs: Long): String { internal fun guideDayJump(cursorMs: Long, nowMs: Long, forward: Boolean): Long = (maxOf(cursorMs, nowMs) + if (forward) 24 * 60 * 60_000L else -24 * 60 * 60_000L).coerceIn(nowMs, nowMs + GUIDE_MAX_SLOTS_AHEAD * GUIDE_SLOT_MS - 1) -internal enum class GuideCellTime { Past, Now, Later } - -/** Only the programme actually on air is lit, so neighbouring programmes can't read as one. */ -internal fun guideCellTime(startMs: Long, endMs: Long, nowMs: Long) = when { - endMs <= nowMs -> GuideCellTime.Past - startMs <= nowMs -> GuideCellTime.Now - else -> GuideCellTime.Later +/** + * What the grid draws: finished programmes go, and whatever's on now fills back to the start of the window. + * Every block is then something you can watch or look forward to. + */ +internal fun guideWatchableCells(cells: List, nowMs: Long): List { + val left = cells.filter { it.show.endMs > nowMs } + val first = left.firstOrNull() ?: return left + return listOf(first.copy(startFraction = 0f, widthFraction = first.startFraction + first.widthFraction)) + left.drop(1) } +/** Below this a title can't fit, so the block shows it's cut short rather than sitting empty. */ +private val GuideTitleMinWidth = 34.dp + private val GuideChannelColumn = Color(0xFF0A1B3C) -private val GuidePast = Color(0xFF0D2247) /** The first row to draw. It moves a row before the edge, so there's always a sign of the next channel. */ internal fun guideFirstRow(row: Int, first: Int, visible: Int, count: Int): Int { @@ -84,10 +87,6 @@ internal fun guideFirstRow(row: Int, first: Int, visible: Int, count: Int): Int }.coerceIn(0, last) } -/** 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)) @@ -136,7 +135,7 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif } } Row(Modifier.fillMaxWidth().height(20.dp).background(Panel), verticalAlignment = Alignment.CenterVertically) { - Text(guideDayPrefix(windowStart, now).trim().ifEmpty { title }, Modifier.width(136.dp).padding(start = 8.dp), color = Color.White, fontSize = 12.sp, fontWeight = FontWeight.SemiBold) + Text(guideDayPrefix(windowStart, now).trim().ifEmpty { "$title, today" }, Modifier.width(136.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 = 12.sp, fontWeight = FontWeight.SemiBold) } } @@ -177,25 +176,29 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif else Text(ChannelBrands.displayName(line), color = Color.White, fontSize = 13.sp, maxLines = 1) } BoxWithConstraints(Modifier.weight(1f).fillMaxHeight()) { - guideCells(line, windowStart, windowEnd).forEach { cell -> + guideWatchableCells(guideCells(line, windowStart, windowEnd), now).forEach { cell -> val isFocus = gridFocused && index == row && cell.show.startMs == focused.startMs - val time = guideCellTime(cell.show.startMs, cell.show.endMs, now) - Box(Modifier.offset(x = maxWidth * cell.startFraction).width((maxWidth * cell.widthFraction - 3.dp).coerceAtLeast(1.dp)).fillMaxHeight() - .background(if (isFocus) Yellow else when (time) { GuideCellTime.Past -> GuidePast; GuideCellTime.Now -> Blue; GuideCellTime.Later -> 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) { - Text(guideTitleParts(cell.show.media.item.title).first, color = if (isFocus) Navy else if (time == GuideCellTime.Past) Muted else Color.White, fontSize = 13.sp, fontWeight = if (isFocus || time == GuideCellTime.Now) FontWeight.SemiBold else FontWeight.Normal, maxLines = 1, overflow = TextOverflow.Ellipsis) + val onNow = cell.show.startMs <= now + val cellWidth = maxWidth * cell.widthFraction - 3.dp + Box(Modifier.offset(x = maxWidth * cell.startFraction).width(cellWidth.coerceAtLeast(1.dp)).fillMaxHeight() + .background(if (isFocus) Yellow else if (onNow) Blue else Panel).padding(horizontal = 6.dp), contentAlignment = Alignment.CenterStart) { + val runsOn = cell.show.endMs > windowEnd + val ink = if (isFocus) Navy else Color.White + Row(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) } } } - guideNowFraction(now, windowStart, windowEnd)?.let { fraction -> - Box(Modifier.offset(x = maxWidth * fraction).width(1.dp).fillMaxHeight().background(Color.White.copy(alpha = .4f))) - } } } } } } - Text("▲▼ Channel ◄► Time Hold ◄► Day OK Watch Back Home${if (rowsShown < channels.size) " • Channels ${firstRow + 1} to ${firstRow + rowsShown} of ${channels.size}" else ""}", color = Muted, fontSize = 11.sp) + Row(Modifier.fillMaxWidth()) { + Text("▲▼ Channel ◄► Time Hold ◄► Day OK Watch", Modifier.weight(1f), color = Muted, fontSize = 11.sp) + if (rowsShown < channels.size) Text("Channels ${firstRow + 1} to ${firstRow + rowsShown} of ${channels.size}", color = Muted, fontSize = 11.sp) + } } } diff --git a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt index 7c5d7d2..f3d55b1 100644 --- a/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt +++ b/android/app/src/main/java/tv/orbit/classic/SecureMediaDataSource.kt @@ -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.54.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.55.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()) { diff --git a/android/app/src/main/res/drawable-nodpi/orbit_x_bug.webp b/android/app/src/main/res/drawable-nodpi/orbit_x_bug.webp index 56cbfbc0476439e7d0f50e914a0fc44d7c7b994c..6648c5a21909fbc8454219d65800e21da505724c 100644 GIT binary patch literal 1880 zcmV-e2dDT_Nk&Fc2LJ$9MM6+kP&il$0000G0001w001ul06|PpNdE!=00E!{Nw#U* zsxjug_t~~>+qP}nwr$(CZQHhO@3S!bWAjC_mDP2Mvl%J}3_I zdN3d56=j7E!<>H5oST#tI+b8_Zw#^30S9gf&J+jif4~fQ4FuL#Ug!aHqlbYxBxyd2 zJS^!JLiL#q`(Y}|@5je!?=i+;O~wfCX1|r^z|?v6N3mrcL^f7YXgeZZ8$jS-W%_1x zJ4Bhj8WF2L=Yf>z^}%i(3^DVDd+xW+P#EA$^n1#+KJRKkClRQ8XPjS@Yda!so#`LH z=J+End3AJa9reZq0`U1-3YDck7|N_B8d{fbEOPj5 zxHTE0f3Kum2N0=+Ba~?oLT3REduCWOS_aNmp3<)q;q|bMa?RbDMTWfM!C^4k*j!4v zuED9pLAIa@E!BxdSm60Hz-b%VNJXJ%2btA?!AM_8n-W`DfKkqPWhKmLZ~m+bm0Fxo zea4~Fb7j(TtvtXmyT5$;utYixHg76ZDs(+wd4c6#8JR_tp25t6!ALv3c-{06=!i>{ zOG%AM@T!Ykxh6w~d)1iM4VdGMmoAzaqS2`{?$0TenQ|RUtiJOO1t$EmJdUB}I`ENa z4x1c;(|Kf1WqRdc<@$Iyvx+mD@Wrb(ntz6?^zG$OKXKy15rWgD+2FS{pd*L>Lxt8R zRvgA~J6(1AombzkS0)08PS=6uRHSltZqz_}7_T~v)`zAg8N!VoI=7UK@~Oaz%FAMA zLe*g*x`n_-AI*l-LuVRE+7=)BGnKli2HFND*?CN}o=p7y4?n&SL!0O~rG&7^w%=kZT09H^qASwg^0I(SVodGJ405AYP zF&2qKq9Gv@nO!6x0|c=EODn7Q*Lf^n|C?{O^nmd`{`1sZ%m?c4?H^zdKo97j*E+yI zW_^G^OZ_qbV?G4GX#eH^q4)*Tlx?CPq<$Czp`G%AFhQLRwI7Ld$mu@C+q_yaj=f>&S&pZ z@P%rxW-w19U+JJ6x$gsny!6`(z&f{~PuDzH8?{oU;;I98Qa8{_~0{`{OS zf9>0j|8Cja(Xw#k?*}A z5tC(b9l!=x5%UNEK(#jkXkDYEt3zL@eU5R=-Rezp^09hESyB$xXIDfIsc7Grq~4Zg8g2w%}ZXlUyK7%G-} zDg)2|u7jI}`7I-cKSbR1-T5~c8&6z1demN_cTagS7=uV$8Kg5O=DiL6-|sLm6wF#W z)#JYqu>DZ5*KKlG)Z*CeBQ>6tczuc*zYa@7Q<@c+-f$D;dT3An`cH8Dc)3;k6_Hdc z5C8c`Y{3sN-Iws1oC;)D>x_^7?M&w4whI->7LE^_e%o^>>59aN>=PrJQA4-id>^li zin~>9j+Zg1fzBT? zihq`Ysqdy{Jtu|kYhg9cdP~D14_1K6h!r)l7_6z+66y4^%flBFBqK zlGO=nc;?yR!2g@aOpE|-K4hof(g$gVFJgbkg!E(-6x*jhx#ornXL~dGQB)<#`?}G& SU=!LUK>jyv_VwZb0001u;HAz0 literal 1894 zcmV-s2buU%Nk&Fq2LJ$9MM6+kP&il$0000G0001w001ul06|PpNZS*p(#@uqk zyu*AI5}k%Qf2NU_<~*cAqDu)zU0e6)(HH|dckk@E5ZGSDL@$^d^$gto?YDQ}!IuC0 ztFOl5*=N@B?KCNU*&C;_#~6c^i5w)A2GfXJr8iodWnW}xC5iSUQnUTRY5!SC$77wp zDCtNd+3&oN7PP3bTNO>l?|9^K+cd)f0+%UW*wl7W`du$P`U`zI8#6AB#Sj*kx5&YGi&j7nkvzr#Ju=f+fs`~PJaxyKM*)s z#gablfaSv(Drgl#bpcMB-qMV+&cGkjf-WJD5BsR7rK__@(>q>lfl<+USjBX#c`h`v z6_seMYAnJEv#tQAqFK}VDyEwd^1z_oC(^FOau%S&74tX3jLLxxloa$5o&(0Iqq#HH zdF4Dni^Jz#-jYbQ&SPmwX)@kI<^|T9-=0;JPRGoHLA!(I+&3Bm)dq(Bq95K(bS2pr zxpPaJhG+HjNH<`KE9Txb9->jhxk{hiK>DI1o_*Jy3iSQBbRNx?I`Nh1rwxYSRByEX zDAoS&*JjxofB8$K0jQZ=W{#!j&Tgr`E_$O26yNMM~#J%Q}o!2i^7flXpL^ z`TYQ*Q@h!uRQJM!B-N^ftivdOYPu#(+^Az<@xm|KfM8RL#k=s#!$Ncmft}u245vPK zOCPncM(ihPD4uy00|O53YE}%JK^eAE5g z^56W=Xb%(5?_EGVz<#EF%=Q5E0RDCTFVX|p1NJl61NFPohyE}6Z-CF)5BmS&z5u^f zf9~f1e;E84{WEnqVuAfvz!GIfEB1ou)tD1x%muafQR5sAk4R}^6Kig7u+z{Q&=B@oYAdQUtcIVKfgF6?VuJX8h0z$~8 z0L&_<`0(q*v*3v?Jw6ZhP;eFS#^NnjqzN!x(u0WF%gcN?<2X{)A)WQR4>B-{Sbw3_ z&kLyjZ|FtF)I?})H9p`PN6|vY58Z_I^E;29;WgQUd2x$*tKE8JezzhSp=CMp2;nw&-i~ZOkkNxFcTvF-!4g;sg&TZeK zleY)>@uZlJBp>xHjfF*fKmN=#Vmz6s6_qk>Fc;p`va@n&IYi!G2gi%-t5siD5e?Qj z_E$E(-~QW1bCxU@S;4+HmOG|5rwq4ONZ4NS4?rwC85xFO#jiv>kR#@HVj~fBr+50k z&%JPbE6vA5<|NTOVej69gf`+ zcXy$%^#R|8oQhES+V{K){9$-wY$z;)J?l<|=eC{4z;~4br{8)9K)%@zfE5tc%TJJh zHiyY5|2uy!UG6#Qtp@(;XKQ3$EavW%jre15)eT7usm0hq-Y8n;fp@C=Y#$C>eo2sOE}K^{KZSQrk{d$q`b=EX7KJ9W2O6QDW@ko5JNsI zzx+UN=vg3}5HwN2np(GfK{n9cCrRP+>gD<^wmCC>0qpK(_C&6>DT4>eD=oJ^ChI%j zWdFcZJ1w;X#492iaOvI7*xSZe=|_Db=K0}Ah={9@Y`p6ygCJ?~b`*Z+nddT-!!h4G zQ+F?4zDG%`mr9=Wt^RYoLXS8*E!(^g^!cn5f&>?b7nVoe`m$Z_k7MqHpKcx@rPA@7 zk%T$EF4wTQ- ggZ@HiMwF4SVdSS^YAdh6Zu{5EK_j*AcbotK04;aKKmY&$ diff --git a/android/app/src/main/res/drawable-nodpi/orbit_x_epg.webp b/android/app/src/main/res/drawable-nodpi/orbit_x_epg.webp index 29b98f3b4b796ef019d4cef235afa0e1ee760de3..69e0cb84c5d2f9a1a25f108fb544678fc60b9148 100644 GIT binary patch literal 3550 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 literal 3610 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 now }.show.media) } - @Test fun onlyTheProgrammeOnAirIsLit() { - assertEquals(GuideCellTime.Past, guideCellTime(100, 200, 200)) - assertEquals(GuideCellTime.Now, guideCellTime(100, 200, 100)) - assertEquals(GuideCellTime.Now, guideCellTime(100, 200, 199)) - assertEquals(GuideCellTime.Later, guideCellTime(100, 200, 99)) + + @Test fun theGridOnlyDrawsWhatYouCanStillWatch() { + val channel = ScheduledChannel(loop, topUpPlan(null, ScheduleCursor(), pool, now)!!) + val start = guideWindowStart(now + 40 * minute, 0) + val at = start + 50 * minute + val cells = guideWatchableCells(guideCells(channel, start, start + 120 * minute), at) + assertTrue(cells.all { it.show.endMs > at }) + assertEquals(0f, cells.first().startFraction, 0f) + assertEquals(1f, cells.last().startFraction + cells.last().widthFraction, .001f) + assertTrue(cells.zipWithNext().all { (a, b) -> kotlin.math.abs(a.startFraction + a.widthFraction - b.startFraction) < .001f }) + } + + @Test fun aWindowInTheFutureIsLeftAlone() { + val channel = ScheduledChannel(loop, topUpPlan(null, ScheduleCursor(), pool, now)!!) + val start = guideWindowStart(now + 2 * day, 0) + val cells = guideCells(channel, start, start + 120 * minute) + assertEquals(cells.drop(1), guideWatchableCells(cells, now).drop(1)) } } diff --git a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt index 0ca3811..d2b51fe 100644 --- a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt @@ -31,15 +31,6 @@ class GuideGridTest { 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))