From d5040f8035148e11246257a7d59c2eedb9f4eb91 Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 15 Oct 2026 14:20:52 +0000 Subject: [PATCH] player: sidebar starts at the top, links stay in the text (#23, #24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The info and comments panels sat under a hard-coded 48dp top padding, leaving a black strip above them; they now fill the height from the very top (#24, and the same note on #23). The info panel no longer repeats the description's links in a list underneath — they stay inline in the running text and OK opens the first one. UP/DOWN are always consumed while scrolling, so the panel can no longer strand focus in a place it can't scroll back from (#23 review). --- .../jeena/jtubetv/ui/player/PlayerSidebar.kt | 82 ++++++------------- .../jeena/jtubetv/ui/screens/PlayerScreen.kt | 6 +- 2 files changed, 28 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/net/jeena/jtubetv/ui/player/PlayerSidebar.kt b/app/src/main/java/net/jeena/jtubetv/ui/player/PlayerSidebar.kt index 6ca82bb..343995e 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/player/PlayerSidebar.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/player/PlayerSidebar.kt @@ -190,64 +190,28 @@ private fun InfoColumn( } val descFocus = remember { FocusRequester() } - val firstLinkFocus = remember { FocusRequester() } - // Focus the description so the D-pad can scroll it right away (issue #23): - // a description with no links used to have nothing focusable, so a long - // text could never be scrolled down with the remote. Fall back to the - // first link when there is no description text. + // The description owns the focus for the whole panel: UP/DOWN scroll it and are + // always consumed, so scrolling can never strand focus somewhere it can't come + // back from (issue #23 review: "it loses focus and it's impossible to scroll up"). LaunchedEffect(Unit) { kotlinx.coroutines.delay(150) - runCatching { - if (description.isNotBlank()) descFocus.requestFocus() - else firstLinkFocus.requestFocus() - } + runCatching { descFocus.requestFocus() } } Column(Modifier.fillMaxSize()) { - if (description.isNotBlank()) { + if (description.isBlank()) { + SidebarFallback("No description.", onDismiss) + } else { DescriptionScroll( annotated = annotated, focusRequester = descFocus, onDismiss = onDismiss, - onLeaveBottom = { - if (links.isNotEmpty()) { - runCatching { firstLinkFocus.requestFocus() }.isSuccess - } else false - }, + // Links stay inline in the running text (issue #23 review) instead of + // being repeated in a list underneath; OK opens the first one. + onActivate = { links.firstOrNull()?.let { open(it) } }, modifier = Modifier.weight(1f, fill = false), ) } - if (links.isNotEmpty()) { - Text( - text = "Links", - color = TextDim, - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(start = 20.dp, top = 10.dp, bottom = 2.dp), - ) - LazyColumn(Modifier.heightIn(max = 220.dp)) { - links.forEachIndexed { index, url -> - item(key = "l" + index + url) { - SidebarRow( - onLeft = onDismiss, - onUp = if (index == 0 && description.isNotBlank()) { - { runCatching { descFocus.requestFocus() } } - } else null, - atTop = index == 0 && description.isBlank(), - atBottom = index == links.size - 1, - focusRequester = if (index == 0) firstLinkFocus else null, - onClick = { open(url) }, - ) { - Text( - text = linkLabel(url), - color = TextMain, - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - ) - } - } - } - } - } } } @@ -263,7 +227,7 @@ private fun DescriptionScroll( annotated: androidx.compose.ui.text.AnnotatedString, focusRequester: FocusRequester, onDismiss: () -> Unit, - onLeaveBottom: () -> Boolean, + onActivate: () -> Unit, modifier: Modifier = Modifier, ) { val scroll = androidx.compose.foundation.rememberScrollState() @@ -281,19 +245,23 @@ private fun DescriptionScroll( if (ke.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false val step = (viewportPx * 0.85f).coerceAtLeast(240f) when (ke.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_DOWN -> - if (scroll.value < scroll.maxValue) { - scope.launch { scroll.animateScrollBy(step) } - true - } else onLeaveBottom() - KeyEvent.KEYCODE_DPAD_UP -> - if (scroll.value > 0) { - scope.launch { scroll.animateScrollBy(-step) } - true - } else true // nothing above the description; stay put + // UP/DOWN are always consumed: they scroll while there is room and + // otherwise do nothing, so focus stays here and the text can always + // be scrolled back up (issue #23 review). + KeyEvent.KEYCODE_DPAD_DOWN -> { + if (scroll.value < scroll.maxValue) scope.launch { scroll.animateScrollBy(step) } + true + } + KeyEvent.KEYCODE_DPAD_UP -> { + if (scroll.value > 0) scope.launch { scroll.animateScrollBy(-step) } + true + } KeyEvent.KEYCODE_DPAD_LEFT -> { onDismiss(); true } + KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> { + onActivate(); true + } else -> false } } diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt index 34b8b40..310584c 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/PlayerScreen.kt @@ -646,9 +646,9 @@ private fun PlayerSurface( closeSidebar() }, onOpenVideo = onOpenVideo, - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(top = 48.dp), + // Full height from the very top — the old top padding left a black + // strip above the panel (issues #23/#24). + modifier = Modifier.align(Alignment.TopEnd), ) } }