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:
parent
d469101eb0
commit
b8893172d4
5 changed files with 46 additions and 4 deletions
|
|
@ -62,6 +62,7 @@ import net.jeena.jtubetv.data.api.VideoChannelDto
|
||||||
import net.jeena.jtubetv.ui.list.ImagePrefetcher
|
import net.jeena.jtubetv.ui.list.ImagePrefetcher
|
||||||
import net.jeena.jtubetv.ui.screens.list.ContainerShape
|
import net.jeena.jtubetv.ui.screens.list.ContainerShape
|
||||||
import net.jeena.jtubetv.ui.screens.list.UnfocusedCardBg
|
import net.jeena.jtubetv.ui.screens.list.UnfocusedCardBg
|
||||||
|
import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channels, SmartTube-style: one row per LOCAL user.
|
* Channels, SmartTube-style: one row per LOCAL user.
|
||||||
|
|
@ -417,6 +418,7 @@ fun ChannelCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(rowCardWidth())
|
.width(rowCardWidth())
|
||||||
.padding(6.dp)
|
.padding(6.dp)
|
||||||
|
.dpadCenterActivatable(onClick)
|
||||||
.onFocusChanged { cardFocused = it.isFocused }
|
.onFocusChanged { cardFocused = it.isFocused }
|
||||||
.onPreviewKeyEvent { ke ->
|
.onPreviewKeyEvent { ke ->
|
||||||
if (ke.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN &&
|
if (ke.nativeKeyEvent.action == android.view.KeyEvent.ACTION_DOWN &&
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ import net.jeena.jtubetv.data.api.VideoDto
|
||||||
import net.jeena.jtubetv.ui.AppViewModel
|
import net.jeena.jtubetv.ui.AppViewModel
|
||||||
import net.jeena.jtubetv.ui.list.ListSource
|
import net.jeena.jtubetv.ui.list.ListSource
|
||||||
import net.jeena.jtubetv.ui.screens.list.VideoListScreen
|
import net.jeena.jtubetv.ui.screens.list.VideoListScreen
|
||||||
|
import net.jeena.jtubetv.ui.screens.list.dpadCenterActivatable
|
||||||
|
|
||||||
enum class Destination(val title: String) {
|
enum class Destination(val title: String) {
|
||||||
Home("Home"),
|
Home("Home"),
|
||||||
|
|
@ -266,6 +267,7 @@ private fun TopBar(
|
||||||
else Modifier
|
else Modifier
|
||||||
)
|
)
|
||||||
.focusable()
|
.focusable()
|
||||||
|
.dpadCenterActivatable(onAccountClick)
|
||||||
.clickable(onClick = onAccountClick),
|
.clickable(onClick = onAccountClick),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.spacedBy(8.dp),
|
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)
|
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
|
||||||
.focusable()
|
.focusable()
|
||||||
|
.dpadCenterActivatable(onClick)
|
||||||
.clickable(onClick = onClick),
|
.clickable(onClick = onClick),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
|
@ -454,6 +457,7 @@ private fun MenuDrawerItem(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.focusable()
|
.focusable()
|
||||||
|
.dpadCenterActivatable(onClick)
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(horizontal = 24.dp, vertical = 14.dp),
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
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
|
* 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)
|
Text(message.value ?: localError.value!!, color = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
Button(
|
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 = {
|
onClick = {
|
||||||
when {
|
when {
|
||||||
url.value.isBlank() || username.value.isBlank() || password.value.isBlank() ->
|
url.value.isBlank() || username.value.isBlank() || password.value.isBlank() ->
|
||||||
|
|
@ -178,7 +176,20 @@ fun SetupScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
enabled = !busy.value,
|
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) {
|
if (busy.value) {
|
||||||
CircularProgressIndicator(Modifier.width(20.dp))
|
CircularProgressIndicator(Modifier.width(20.dp))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -69,6 +69,7 @@ fun VideoCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(6.dp)
|
.padding(6.dp)
|
||||||
|
.dpadCenterActivatable(onClick)
|
||||||
.onFocusChanged {
|
.onFocusChanged {
|
||||||
cardFocused = it.isFocused
|
cardFocused = it.isFocused
|
||||||
if (it.isFocused) onFocused?.invoke()
|
if (it.isFocused) onFocused?.invoke()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue