diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 515c0760..49b8bb5f 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 = 53 - versionName = "0.52.0-grid-rework" + versionCode = 54 + versionName = "0.53.0-guide-scroll-hint" } 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 974dc03b..d20c6875 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.52.0\"") + connection.setRequestProperty("X-Emby-Authorization", "Emby Client=\"Orbit TV\", Device=\"Orbit TV\", DeviceId=\"$deviceId\", Version=\"0.53.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 fce2f1c2..d272f0db 100644 --- a/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt +++ b/android/app/src/main/java/tv/orbit/classic/GuideGrid.kt @@ -73,12 +73,13 @@ internal fun guideCellTime(startMs: Long, endMs: Long, nowMs: Long) = when { private val GuideChannelColumn = Color(0xFF0A1B3C) private val GuidePast = Color(0xFF0D2247) -/** The first row to draw, moving only as far as it takes to keep the chosen row on screen. */ +/** 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 { val last = (count - visible).coerceAtLeast(0) + val margin = if (visible >= 3) 1 else 0 return when { - row < first -> row - row >= first + visible -> row - visible + 1 + row < first + margin -> row - margin + row > first + visible - 1 - margin -> row - visible + 1 + margin else -> first }.coerceIn(0, last) } @@ -101,6 +102,7 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif var cursor by remember { mutableLongStateOf(now) } var slotsAhead by remember { mutableIntStateOf(0) } var firstRow by remember { mutableIntStateOf(0) } + var rowsShown by remember { mutableIntStateOf(0) } val gridFocus = remember { FocusRequester() } var gridFocused by remember { mutableStateOf(true) } LaunchedEffect(Unit) { gridFocus.requestFocus() } @@ -142,6 +144,7 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif BoxWithConstraints(Modifier.fillMaxWidth().weight(1f)) { val visibleRows = ((maxHeight + 3.dp) / 41.dp).toInt().coerceAtLeast(1) firstRow = guideFirstRow(row, firstRow, visibleRows, channels.size) + rowsShown = minOf(visibleRows, channels.size) 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 @@ -193,6 +196,6 @@ internal fun GuideGrid(channels: List, selectedNumber: Int, modif } } } - Text("▲▼ Channel ◄► Time Hold ◄► Day OK Watch Back Home", color = Muted, fontSize = 11.sp) + 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) } } 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 c587d6c0..c94af2f2 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.52.0") + next.setRequestProperty("User-Agent", "OrbitTV/0.53.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/test/java/tv/orbit/classic/GuideGridTest.kt b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt index 7932760c..cf1df1bd 100644 --- a/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt +++ b/android/app/src/test/java/tv/orbit/classic/GuideGridTest.kt @@ -86,9 +86,9 @@ class GuideGridTest { @Test fun theGridScrollsOnlyWhenTheChosenRowLeavesTheScreen() { assertEquals(0, guideFirstRow(3, 0, 8, 25)) - assertEquals(1, guideFirstRow(8, 0, 8, 25)) + assertEquals(2, guideFirstRow(8, 0, 8, 25)) assertEquals(5, guideFirstRow(9, 5, 8, 25)) - assertEquals(4, guideFirstRow(4, 5, 8, 25)) + assertEquals(3, guideFirstRow(4, 5, 8, 25)) assertEquals(17, guideFirstRow(24, 10, 8, 25)) } @@ -97,4 +97,12 @@ class GuideGridTest { assertEquals(0, guideFirstRow(2, 0, 8, 3)) assertEquals(0, guideFirstRow(0, 4, 8, 3)) } + + @Test + fun theNextChannelIsAlwaysInSight() { + assertEquals(1, guideFirstRow(1, 2, 6, 11)) + assertEquals(0, guideFirstRow(0, 1, 6, 11)) + assertEquals(1, guideFirstRow(5, 0, 6, 11)) + assertEquals(5, guideFirstRow(10, 0, 6, 11)) + } }