Files
SleppyOwl/app/src/main/kotlin/ru/stenfredd/movies/ui/TourScreen.kt
2026-10-14 14:23:47 +03:00

142 lines
6.2 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ru.stenfredd.movies.ui
import android.content.Context
import android.view.KeyEvent as AndroidKeyEvent
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.nativeKeyCode
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.tv.material3.Text
import java.util.UUID
import kotlinx.coroutines.delay
import org.json.JSONArray
import ru.stenfredd.movies.playback.api.StreamSelection
import ru.stenfredd.movies.ui.player.PlayerScreen
import ru.stenfredd.movies.ui.player.TourInfo
import timber.log.Timber
/**
* One file of the debug tour with the tracks to select. The list is `assets/tour.json` of the debug build,
* generated by `tools/tour/build_tour.py` (a minimal set of files covering every format feature on the server).
*/
class TourEntry(val id: UUID, val title: String, val audio: Int?, val subtitle: Int?, val label: String) {
companion object {
private const val ASSET = "tour.json"
fun available(context: Context): Boolean = runCatching { ASSET in context.assets.list("").orEmpty() }.getOrDefault(false)
fun load(context: Context): List<TourEntry> = runCatching {
val json = JSONArray(context.assets.open(ASSET).bufferedReader().use { it.readText() })
(0 until json.length()).map { i ->
val o = json.getJSONObject(i)
TourEntry(
id = UUID.fromString(o.getString("id").replace(Regex("(.{8})(.{4})(.{4})(.{4})(.{12})"), "$1-$2-$3-$4-$5")),
title = o.optString("title"),
audio = if (o.isNull("audio")) null else o.getInt("audio"),
subtitle = if (o.isNull("subtitle")) null else o.getInt("subtitle"),
label = o.optString("label"),
)
}
}.onFailure { Timber.w(it, "Tour list not loaded") }.getOrDefault(emptyList())
}
}
/**
* Plays the tour files one by one, each from the middle: ↓ next file, ↑ next playback variant (see [PlayerScreen]).
*
* ↓ first only shows which file is next, the file starts after [START_DELAY_MS] without further keys: skipping
* several files in a row would otherwise start and tear down a player on every press and choke the TV.
* While waiting, ↓/↑ move forward/back through the list.
*/
@Composable
fun TourScreen(startIndex: Int, onExit: () -> Unit) {
val context = LocalContext.current
val entries = remember { TourEntry.load(context) }
var index by rememberSaveable { mutableIntStateOf(startIndex.coerceIn(0, (entries.size - 1).coerceAtLeast(0))) }
var playing by rememberSaveable { mutableIntStateOf(index) }
if (entries.isEmpty()) {
Box(Modifier.fillMaxSize()) { Text("Список файлов для отладки пуст (assets/tour.json)", Modifier.align(Alignment.Center)) }
return
}
LaunchedEffect(index) {
if (index == playing) return@LaunchedEffect
delay(START_DELAY_MS)
playing = index
}
val entry = entries[index]
if (index != playing) {
TourPending(index + 1, entries.size, entry, onMove = { step -> index = (index + step).mod(entries.size) }, onExit = onExit)
return
}
key(index) {
PlayerScreen(
Screen.Player(
itemId = entry.id,
engine = null,
startPositionMs = 0,
streams = StreamSelection(entry.audio, entry.subtitle),
startFraction = 0.5,
),
onExit = onExit,
tour = TourInfo(index + 1, entries.size, entry.title, entry.label) { index = (index + 1) % entries.size },
)
}
}
private const val START_DELAY_MS = 2000L
/** Black screen with the file that will start: nothing is loaded yet. */
@Composable
private fun TourPending(number: Int, total: Int, entry: TourEntry, onMove: (Int) -> Unit, onExit: () -> Unit) {
val focus = remember { FocusRequester() }
LaunchedEffect(Unit) { runCatching { focus.requestFocus() } }
BackHandler(onBack = onExit)
Box(
Modifier
.fillMaxSize()
.background(Color.Black)
.focusRequester(focus)
.onPreviewKeyEvent { e ->
if (e.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
when (e.key.nativeKeyCode) {
AndroidKeyEvent.KEYCODE_DPAD_DOWN -> onMove(1)
AndroidKeyEvent.KEYCODE_DPAD_UP -> onMove(-1)
else -> return@onPreviewKeyEvent false
}
true
}
.focusable(),
) {
Column(Modifier.align(Alignment.Center).padding(48.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Text("Отладка: файл $number из $total", fontSize = 20.sp, color = Color(0xFF80CBC4))
Text(entry.title, fontSize = 28.sp, color = Color.White)
Text(entry.label, fontSize = 16.sp, color = Color.LightGray)
Text("Запуск через 2 с · ↓ дальше · ↑ назад", fontSize = 14.sp, color = Color.Gray, modifier = Modifier.padding(top = 24.dp))
}
}
}