ui: CENTER activates everything (clickable lacks DPAD_CENTER)

Compose clickable handles Enter/Space only; whether the TV's OK
(DPAD_CENTER) triggers click depends on the device's input stack -
worked on the API-36 host emulator, dead on the API-34 ATD and
possibly some TVs. A preview-tunnelled center handler now activates
drawer items, top-bar buttons, video and channel cards, and the
login/search buttons deterministically, exactly once (preview
consumption prevents the platform path from double-firing).
This commit is contained in:
Jeena 2026-10-11 07:44:35 +00:00
parent d469101eb0
commit b8893172d4
5 changed files with 46 additions and 4 deletions

View file

@ -62,6 +62,7 @@ import net.jeena.jtubetv.data.api.VideoChannelDto
import net.jeena.jtubetv.ui.list.ImagePrefetcher
import net.jeena.jtubetv.ui.screens.list.ContainerShape
import net.jeena.jtubetv.ui.screens.list.UnfocusedCardBg
import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
/**
* Channels, SmartTube-style: one row per LOCAL user.
@ -417,6 +418,7 @@ fun ChannelCard(
modifier = modifier
.width(rowCardWidth())
.padding(6.dp)
.dpadCenterActivatable(onClick)
.onFocusChanged { cardFocused = it.isFocused }
.onPreviewKeyEvent { ke ->
if (ke.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN &&

View file

@ -55,6 +55,7 @@ import net.jeena.jtubetv.data.api.VideoDto
import net.jeena.jtubetv.ui.AppViewModel
import net.jeena.jtubetv.ui.list.ListSource
import net.jeena.jtubetv.ui.screens.list.VideoListScreen
import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
enum class Destination(val title: String) {
Home("Home"),
@ -266,6 +267,7 @@ private fun TopBar(
else Modifier
)
.focusable()
.dpadCenterActivatable(onAccountClick)
.clickable(onClick = onAccountClick),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
@ -338,6 +340,7 @@ private fun RoundTopButton(
)
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
.focusable()
.dpadCenterActivatable(onClick)
.clickable(onClick = onClick),
contentAlignment = Alignment.Center,
) {
@ -454,6 +457,7 @@ private fun MenuDrawerItem(
}
)
.focusable()
.dpadCenterActivatable(onClick)
.clickable(onClick = onClick)
.padding(horizontal = 24.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,

View file

@ -28,6 +28,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.StateFlow
import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
/**
* Feature 0: ask for instance URL + credentials once. The password is used only for
@ -164,9 +165,6 @@ fun SetupScreen(
Text(message.value ?: localError.value!!, color = MaterialTheme.colorScheme.error)
}
Button(
// Deliberately always focusable: on TV a permanently disabled button
// is unreachable and leaves the user stuck without feedback (UX review).
// Validation happens on press with an inline message instead.
onClick = {
when {
url.value.isBlank() || username.value.isBlank() || password.value.isBlank() ->
@ -178,7 +176,20 @@ fun SetupScreen(
}
},
enabled = !busy.value,
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.dpadCenterActivatable(
onClick = {
when {
url.value.isBlank() || username.value.isBlank() || password.value.isBlank() ->
localError.value = "All fields are required"
else -> {
localError.value = null
onLogin(url.value, username.value, password.value)
}
}
},
),
) {
if (busy.value) {
CircularProgressIndicator(Modifier.width(20.dp))

View file

@ -0,0 +1,24 @@
package net.jeena.jtubetv.ui.screens.list
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.input.key.onPreviewKeyEvent
/**
* Compose `clickable` handles Enter/Space but NOT the TV's DPAD_CENTER; whether
* CENTER works depends on each device's input translation (works on some emulators,
* not on others — see the API-34 ATD case). This modifier consumes CENTER *first*
* (preview/tunneling) so the click fires exactly once, on every device.
*/
fun Modifier.dpadCenterActivatable(onClick: () -> Unit): Modifier = composed {
onPreviewKeyEvent { ke ->
if (ke.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN &&
ke.nativeKeyEvent.keyCode == android.view.KeyEvent.KEYCODE_DPAD_CENTER
) {
onClick()
true
} else false
}
}

View file

@ -69,6 +69,7 @@ fun VideoCard(
onClick = onClick,
modifier = modifier
.padding(6.dp)
.dpadCenterActivatable(onClick)
.onFocusChanged {
cardFocused = it.isFocused
if (it.isFocused) onFocused?.invoke()