ui: BACK = up the hierarchy (channel->channels->home->exit guard)

The BACK-opens-drawer behavior made navigation loops with the drawer
focus restore; replaced with the TV-standard: BACK leaves the player,
closes the drawer, leaves a channel drilldown, or goes Home; from Home
it is the double-press exit guard. Drawer opens via the account
button / LEFT on a leftmost card.
This commit is contained in:
Jeena 2026-10-11 07:28:19 +00:00
parent d55c265469
commit 828bfdafa9
2 changed files with 36 additions and 23 deletions

View file

@ -49,6 +49,7 @@ import androidx.tv.material3.Border
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import coil.compose.AsyncImage
import android.util.Log
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import net.jeena.jtubetv.AppGraph
@ -149,6 +150,11 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
val known = all.map { it.name to it.host }.toSet()
all += page.first.filter { c -> (c.name to c.host) !in known }
start += 100
android.util.Log.w(
"JTube",
"channels page " + (start / 100) + ": total=" + total + " all=" + all.size +
" locals=" + all.count { it.host == host },
)
allChannels = all
rebuildRows(host)
}
@ -156,6 +162,8 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
// stats (videos + plays) for every local channel: throttled parallel-ish
val locals = all.filter { it.host == host }
Log.w("JTube", "pages done: all=" + all.size + " locals=" + locals.size +
" userChannels=" + userChannels.size)
channelStatsLoop(locals, host)
} catch (e: Exception) {
error = e.message ?: "Could not load channels"
@ -171,7 +179,9 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
for (channel in channels) {
val key = channel.name + "@" + channel.host
if (statsMap.containsKey(key)) continue
val cs = repo.channelStats(key).getOrNull() ?: continue
val cs = repo.channelStats(key).getOrNull()
if (cs == null) { android.util.Log.w("JTube", "stats FAIL " + key); continue }
android.util.Log.w("JTube", "stats " + key + " videos=" + cs.videos + " plays=" + cs.plays)
statsMap[key] = cs
stats = statsMap.toMap()
rebuildRows(host)
@ -230,6 +240,14 @@ class ChannelsViewModel(private val store: TokenStore) : ViewModel() {
.thenByDescending { g -> g.channels.sumOf { statsMap[it.name + "@" + it.host]?.videos ?: 0 } }
.thenByDescending { g -> g.channels.sumOf { it.followersCount } },
)
Log.w(
"JTube",
"order now: " + userRows.take(6).joinToString(" | ") { g ->
g.ownerName + "(" + g.channels.size + "ch, " +
g.channels.sumOf { statsMap[it.name + "@" + it.host]?.videos ?: 0 } + "v" +
(if (g.channels.any { it.name in ownNames }) ", OWN" else "") + ")"
},
)
}
private fun ownerKey(c: VideoChannelDto) = c.ownerAccount?.name ?: ""

View file

@ -76,7 +76,8 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
val epoch by viewModel.sessionEpoch.collectAsState()
var destination by rememberSaveable { mutableStateOf(Destination.Home) }
var channelDetails by rememberSaveable { mutableStateOf<String?>(null) } // handle
var playingVideo by rememberSaveable { mutableStateOf<String?>(null) } // shortUUID
// NOT saveable: launching the app should never resume a half-watched player session.
var playingVideo by remember { mutableStateOf<String?>(null) } // shortUUID
var restoreToken by rememberSaveable { mutableStateOf(0) } // increments when playback ends
var drawerOpen by rememberSaveable { mutableStateOf(false) }
val context = androidx.compose.ui.platform.LocalContext.current
@ -89,35 +90,29 @@ fun HomeScreen(viewModel: AppViewModel = androidx.lifecycle.viewmodel.compose.vi
if (playingVideo == null && restoreToken >= 1) restoreToken++
}
// BACK order: player (own BackHandler) → drawer close → channel drilldown →
// BACK (on content) opens the drawer; a double-BACK with the drawer open exits.
BackHandler(enabled = playingVideo == null && channelDetails == null) {
val now = System.currentTimeMillis()
if (drawerOpen) {
// BACK order (TV standard): player (own handler) → drawer close → channel
// drilldown → non-Home destination → Home; from Home: double-press exits.
BackHandler(enabled = drawerOpen && playingVideo == null) {
drawerOpen = false
runCatching { searchFocus.requestFocus() }
}
BackHandler(enabled = !drawerOpen && playingVideo == null && channelDetails != null) {
channelDetails = null
}
BackHandler(enabled = !drawerOpen && playingVideo == null && channelDetails == null) {
if (destination != Destination.Home) {
channelDetails = null
destination = Destination.Home
} else {
val now = System.currentTimeMillis()
if (now - lastBackAt < 2500) {
(context as? android.app.Activity)?.finish()
return@BackHandler
}
lastBackAt = now
android.widget.Toast.makeText(context, "Press BACK again to exit", android.widget.Toast.LENGTH_SHORT).show()
} else {
lastBackAt = now
drawerOpen = true
}
}
BackHandler(enabled = playingVideo == null && channelDetails != null && !drawerOpen) {
channelDetails = null
}
BackHandler(enabled = drawerOpen && playingVideo == null) {
val now = System.currentTimeMillis()
if (now - lastBackAt < 2500) {
(context as? android.app.Activity)?.finish()
return@BackHandler
}
lastBackAt = now
drawerOpen = false
runCatching { searchFocus.requestFocus() }
}
Box(
modifier = Modifier