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). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
904020da3c
commit
f8a36a73e5
2 changed files with 28 additions and 60 deletions
|
|
@ -190,64 +190,28 @@ private fun InfoColumn(
|
||||||
}
|
}
|
||||||
|
|
||||||
val descFocus = remember { FocusRequester() }
|
val descFocus = remember { FocusRequester() }
|
||||||
val firstLinkFocus = remember { FocusRequester() }
|
// The description owns the focus for the whole panel: UP/DOWN scroll it and are
|
||||||
// Focus the description so the D-pad can scroll it right away (issue #23):
|
// always consumed, so scrolling can never strand focus somewhere it can't come
|
||||||
// a description with no links used to have nothing focusable, so a long
|
// back from (issue #23 review: "it loses focus and it's impossible to scroll up").
|
||||||
// text could never be scrolled down with the remote. Fall back to the
|
|
||||||
// first link when there is no description text.
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
kotlinx.coroutines.delay(150)
|
kotlinx.coroutines.delay(150)
|
||||||
runCatching {
|
runCatching { descFocus.requestFocus() }
|
||||||
if (description.isNotBlank()) descFocus.requestFocus()
|
|
||||||
else firstLinkFocus.requestFocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
if (description.isNotBlank()) {
|
if (description.isBlank()) {
|
||||||
|
SidebarFallback("No description.", onDismiss)
|
||||||
|
} else {
|
||||||
DescriptionScroll(
|
DescriptionScroll(
|
||||||
annotated = annotated,
|
annotated = annotated,
|
||||||
focusRequester = descFocus,
|
focusRequester = descFocus,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
onLeaveBottom = {
|
// Links stay inline in the running text (issue #23 review) instead of
|
||||||
if (links.isNotEmpty()) {
|
// being repeated in a list underneath; OK opens the first one.
|
||||||
runCatching { firstLinkFocus.requestFocus() }.isSuccess
|
onActivate = { links.firstOrNull()?.let { open(it) } },
|
||||||
} else false
|
|
||||||
},
|
|
||||||
modifier = Modifier.weight(1f, fill = false),
|
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,
|
annotated: androidx.compose.ui.text.AnnotatedString,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onLeaveBottom: () -> Boolean,
|
onActivate: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scroll = androidx.compose.foundation.rememberScrollState()
|
val scroll = androidx.compose.foundation.rememberScrollState()
|
||||||
|
|
@ -281,19 +245,23 @@ private fun DescriptionScroll(
|
||||||
if (ke.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
if (ke.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
||||||
val step = (viewportPx * 0.85f).coerceAtLeast(240f)
|
val step = (viewportPx * 0.85f).coerceAtLeast(240f)
|
||||||
when (ke.nativeKeyEvent.keyCode) {
|
when (ke.nativeKeyEvent.keyCode) {
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN ->
|
// UP/DOWN are always consumed: they scroll while there is room and
|
||||||
if (scroll.value < scroll.maxValue) {
|
// otherwise do nothing, so focus stays here and the text can always
|
||||||
scope.launch { scroll.animateScrollBy(step) }
|
// be scrolled back up (issue #23 review).
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN -> {
|
||||||
|
if (scroll.value < scroll.maxValue) scope.launch { scroll.animateScrollBy(step) }
|
||||||
true
|
true
|
||||||
} else onLeaveBottom()
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_UP ->
|
KeyEvent.KEYCODE_DPAD_UP -> {
|
||||||
if (scroll.value > 0) {
|
if (scroll.value > 0) scope.launch { scroll.animateScrollBy(-step) }
|
||||||
scope.launch { scroll.animateScrollBy(-step) }
|
|
||||||
true
|
true
|
||||||
} else true // nothing above the description; stay put
|
}
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
KeyEvent.KEYCODE_DPAD_LEFT -> {
|
||||||
onDismiss(); true
|
onDismiss(); true
|
||||||
}
|
}
|
||||||
|
KeyEvent.KEYCODE_DPAD_CENTER, KeyEvent.KEYCODE_ENTER -> {
|
||||||
|
onActivate(); true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -646,9 +646,9 @@ private fun PlayerSurface(
|
||||||
closeSidebar()
|
closeSidebar()
|
||||||
},
|
},
|
||||||
onOpenVideo = onOpenVideo,
|
onOpenVideo = onOpenVideo,
|
||||||
modifier = Modifier
|
// Full height from the very top — the old top padding left a black
|
||||||
.align(Alignment.CenterEnd)
|
// strip above the panel (issues #23/#24).
|
||||||
.padding(top = 48.dp),
|
modifier = Modifier.align(Alignment.TopEnd),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue