diff --git a/VERIFICATION-0.65.md b/VERIFICATION-0.65.md
new file mode 100644
index 0000000..018218c
--- /dev/null
+++ b/VERIFICATION-0.65.md
@@ -0,0 +1,12 @@
+# Orbit TV 0.65.0: now marker and new icons
+
+- Guide: a thin bar along the foot of what's on now, ending at the current time on every row, and a small tick in the time header. It doesn't cross any title. Both go away when the guide moves to later times.
+- Icons: Cartoons has its own (comic burst), Documentaries is a film camera on a tripod, All Channels has the Orbit crescent and ring inside the TV.
+- App icon and TV banner now use the real Orbit mark. The old placeholder vector is removed.
+- Sources: `design/interface/build-new-icons.py`; the first icon set is left as it was.
+
+Checked: build, lint and 252 unit tests pass (new test for the now position). Icons, app icon and banner checked by eye on a contact sheet at full and small size.
+
+Not checked on the TV: the marker on screen, and how the launcher shows the new icon and banner. The launcher may cache the old art until it refreshes.
+
+Catalogue and schedule code unchanged.
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 3a25ab0..2db47cd 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 = 65
- versionName = "0.64.0-film-clash-fix"
+ versionCode = 66
+ versionName = "0.65.0-now-marker-new-icons"
}
buildFeatures { compose = true }
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 581f284..7cb1a2b 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,7 +2,7 @@
-
+
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 7378d8e..7b84b1f 100644
--- a/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt
+++ b/android/app/src/main/java/tv/orbit/classic/EmbyClient.kt
@@ -66,7 +66,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.64.0\"")
+ connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.65.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 54f134f..56a110e 100644
--- a/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt
+++ b/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt
@@ -71,6 +71,10 @@ internal fun guideWatchableCells(cells: List, nowMs: Long): List= windowEnd) null else (nowMs - windowStart).toFloat() / (windowEnd - windowStart)
+
/** Below this a title can't fit, so the block shows it's cut short rather than sitting empty. */
private val GuideTitleMinWidth = 34.dp
@@ -108,6 +112,7 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif
BackHandler(onBack = onBack)
val windowStart = guideWindowStart(now, slotsAhead)
val windowEnd = windowStart + GUIDE_SLOTS * GUIDE_SLOT_MS
+ val nowFraction = guideNowFraction(now, windowStart, windowEnd)
val channel = channels[row.coerceIn(channels.indices)]
val focused = channel.at(maxOf(cursor, now))
fun keepCursorInWindow(time: Long) {
@@ -136,8 +141,11 @@ 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, 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) }
+ BoxWithConstraints(Modifier.weight(1f).fillMaxHeight()) {
+ Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
+ 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) }
+ }
+ if (nowFraction != null) Box(Modifier.align(Alignment.BottomStart).offset(x = maxWidth * nowFraction - 1.dp).size(2.dp, 5.dp).background(Color.White))
}
}
BoxWithConstraints(Modifier.fillMaxWidth().weight(1f)) {
@@ -180,11 +188,14 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif
val isFocus = gridFocused && index == row && cell.show.startMs == focused.startMs
val onNow = cell.show.startMs <= now
val cellWidth = maxWidth * cell.widthFraction - 3.dp
+ // 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 (onNow) Blue else Panel).padding(horizontal = 6.dp), contentAlignment = Alignment.CenterStart) {
+ .background(if (isFocus) Yellow else if (onNow) Blue else Panel), contentAlignment = Alignment.CenterStart) {
val runsOn = cell.show.endMs > windowEnd
val ink = if (isFocus) Navy else Color.White
- Row(verticalAlignment = Alignment.CenterVertically) {
+ if (elapsed > 0.dp) Box(Modifier.align(Alignment.BottomStart).width(elapsed).height(3.dp).background(ink))
+ 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)
diff --git a/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt b/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt
index f599b8b..1c13846 100644
--- a/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt
+++ b/android/app/src/main/java/tv/orbit/classic/HomeScreen.kt
@@ -27,7 +27,7 @@ internal enum class ChannelCategory(val label: String, val icon: Int) {
Entertainment("Entertainment", R.drawable.orbit_icon_entertainment),
Movies("Movies", R.drawable.orbit_icon_movies),
Comedy("Comedy", R.drawable.orbit_icon_comedy),
- Cartoons("Cartoons", R.drawable.orbit_icon_kids),
+ Cartoons("Cartoons", R.drawable.orbit_icon_cartoons),
Kids("Kids", R.drawable.orbit_icon_kids),
Documentaries("Documentaries", R.drawable.orbit_icon_documentaries),
Sport("Sport", R.drawable.orbit_icon_sport)
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 62f2bad..30d83dc 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.64.0")
+ next.setRequestProperty("User-Agent", "OrbitTV/0.65.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_app_icon.webp b/android/app/src/main/res/drawable-nodpi/orbit_app_icon.webp
new file mode 100644
index 0000000..ed7eac0
Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_app_icon.webp differ
diff --git a/android/app/src/main/res/drawable-nodpi/orbit_banner.png b/android/app/src/main/res/drawable-nodpi/orbit_banner.png
index 7fd1f0f..e28bebd 100644
Binary files a/android/app/src/main/res/drawable-nodpi/orbit_banner.png and b/android/app/src/main/res/drawable-nodpi/orbit_banner.png differ
diff --git a/android/app/src/main/res/drawable-nodpi/orbit_icon_all.webp b/android/app/src/main/res/drawable-nodpi/orbit_icon_all.webp
index 7039062..9584bf1 100644
Binary files a/android/app/src/main/res/drawable-nodpi/orbit_icon_all.webp and b/android/app/src/main/res/drawable-nodpi/orbit_icon_all.webp differ
diff --git a/android/app/src/main/res/drawable-nodpi/orbit_icon_cartoons.webp b/android/app/src/main/res/drawable-nodpi/orbit_icon_cartoons.webp
new file mode 100644
index 0000000..303ff23
Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/orbit_icon_cartoons.webp differ
diff --git a/android/app/src/main/res/drawable-nodpi/orbit_icon_documentaries.webp b/android/app/src/main/res/drawable-nodpi/orbit_icon_documentaries.webp
index 04d3b5d..d007843 100644
Binary files a/android/app/src/main/res/drawable-nodpi/orbit_icon_documentaries.webp and b/android/app/src/main/res/drawable-nodpi/orbit_icon_documentaries.webp differ
diff --git a/android/app/src/main/res/drawable/orbit_icon.xml b/android/app/src/main/res/drawable/orbit_icon.xml
deleted file mode 100644
index d060114..0000000
--- a/android/app/src/main/res/drawable/orbit_icon.xml
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
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 d2b51fe..0cbf854 100644
--- a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt
+++ b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt
@@ -96,4 +96,12 @@ class GuideGridTest {
assertEquals(1, guideFirstRow(5, 0, 6, 11))
assertEquals(5, guideFirstRow(10, 0, 6, 11))
}
+
+ @Test
+ fun nowOnlyShowsWhileItIsInTheWindow() {
+ assertEquals(0.25f, guideNowFraction(1_250, 1_000, 2_000)!!, 0.001f)
+ assertEquals(0f, guideNowFraction(1_000, 1_000, 2_000)!!, 0.001f)
+ assertNull(guideNowFraction(999, 1_000, 2_000))
+ assertNull(guideNowFraction(2_000, 1_000, 2_000))
+ }
}
diff --git a/design/interface/build-new-icons.py b/design/interface/build-new-icons.py
new file mode 100644
index 0000000..57924e5
--- /dev/null
+++ b/design/interface/build-new-icons.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+"""New category icons and the app's launcher art. The first icon set in icons/ stays as it was."""
+import os, subprocess
+from pathlib import Path
+from PIL import Image
+HERE = Path(__file__).parent
+ICONS = HERE / 'icons'
+RES = HERE.parents[1] / 'android/app/src/main/res/drawable-nodpi'
+C, A = '#E8EDF5', '#7FB2FF'
+S = f'fill="none" stroke="{C}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"'
+T = f'fill="none" stroke="{A}" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"'
+CRESCENT = 'M335 132C141 114 66 384 257 475C143 363 185 207 335 132Z'
+ICON = {
+ # The TV now carries the Orbit crescent and ring.
+ 'all': f''
+ f'',
+ # A camera on a tripod: someone out filming the real world.
+ 'documentaries': f'',
+ # A comic burst, so Cartoons can't be confused with the Kids rocket.
+ 'cartoons': f'',
+}
+def render(svg, out):
+ path = ICONS / out
+ path.write_text(svg)
+ png = path.with_suffix('.png')
+ subprocess.run(['vectorcraft-cli', 'convert', str(path), str(png)], check=True, stdout=subprocess.DEVNULL)
+ return png
+for name, body in ICON.items():
+ png = render(f'', f'orbit_icon_{name}_v2.svg')
+ Image.open(png).convert('RGBA').resize((96, 96), Image.LANCZOS).save(RES / f'orbit_icon_{name}.webp', lossless=True)
+SILVER = ''
+BG = ''
+MARK = f''
+png = render(f'', 'orbit_app_icon.svg')
+Image.open(png).convert('RGB').resize((512, 512), Image.LANCZOS).save(RES / 'orbit_app_icon.webp', quality=92)
+png = render(f'', 'orbit_banner.svg')
+Image.open(png).convert('RGB').resize((640, 360), Image.LANCZOS).save(RES / 'orbit_banner.png')
+for f in ICONS.glob('*_v2.png'): f.unlink()
+for n in ('orbit_app_icon', 'orbit_banner'): (ICONS / f'{n}.png').unlink(missing_ok=True)
diff --git a/design/interface/icons/orbit_app_icon.svg b/design/interface/icons/orbit_app_icon.svg
new file mode 100644
index 0000000..8d7773a
--- /dev/null
+++ b/design/interface/icons/orbit_app_icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/design/interface/icons/orbit_banner.svg b/design/interface/icons/orbit_banner.svg
new file mode 100644
index 0000000..27762a7
--- /dev/null
+++ b/design/interface/icons/orbit_banner.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/design/interface/icons/orbit_icon_all_v2.svg b/design/interface/icons/orbit_icon_all_v2.svg
new file mode 100644
index 0000000..aed020c
--- /dev/null
+++ b/design/interface/icons/orbit_icon_all_v2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/design/interface/icons/orbit_icon_cartoons_v2.svg b/design/interface/icons/orbit_icon_cartoons_v2.svg
new file mode 100644
index 0000000..68ce1b1
--- /dev/null
+++ b/design/interface/icons/orbit_icon_cartoons_v2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/design/interface/icons/orbit_icon_documentaries_v2.svg b/design/interface/icons/orbit_icon_documentaries_v2.svg
new file mode 100644
index 0000000..7f9bcf2
--- /dev/null
+++ b/design/interface/icons/orbit_icon_documentaries_v2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file