Settings in five sections; every Arcade game hosted by the cast (from release 0.108.51)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,6 @@ internal fun LockDetail(firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
else { step = LockStep.NewPin; note = "Those didn't match. Choose it again." }
|
||||
}, onCancel = { step = LockStep.None }) }
|
||||
}
|
||||
Text("Forgotten the PIN? Signing out in Emby account clears it, along with this box's schedules.", color = Muted, fontSize = 11.sp)
|
||||
Text("Forgotten the PIN? Signing out in Settings › Account clears it, along with this box's schedules.", color = Muted, fontSize = 11.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,6 +190,8 @@ internal fun BlockDropScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Un
|
||||
}, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Column(Modifier.width(190.dp).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("BLOCK DROP", color = Honey, fontFamily = Marquee, fontSize = 34.sp)
|
||||
// Drift hosts (Paul, 6 November 2026), dozing beside the board.
|
||||
Canvas(Modifier.size(54.dp)) { drawOrbChaser(center, size.minDimension * .36f, OrbCharacter.Drift, OrbDir.Down, scared = false, returning = false, blink = false, wobble = 0f, eyesShut = !game.alive) }
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("SCORE", color = NightMuted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
Text(game.score.toString(), color = Color.White, fontFamily = Marquee, fontSize = 32.sp)
|
||||
|
||||
@@ -124,7 +124,8 @@ internal fun BrickField(game: BrickGame, modifier: Modifier) = Canvas(modifier)
|
||||
if (b.hits > 1) drawRoundRect(colour, Offset(o.x + (b.left + 1.5f) * k, o.y + (b.top + 1.5f) * k), Size((b.right - b.left - 3f) * k, (BRICK_HEIGHT - 3f) * k), CornerRadius(1.5f * k))
|
||||
}
|
||||
drawRoundRect(NightYellow, Offset(o.x + (game.paddleX - PADDLE_W / 2) * k, o.y + PADDLE_Y * k), Size(PADDLE_W * k, 6 * k), CornerRadius(3 * k))
|
||||
drawCircle(Color.White, BALL_R * k, Offset(o.x + game.ballX * k, o.y + game.ballY * k))
|
||||
// Luna is the ball (Paul, 6 November 2026), drawn a little larger than she bounces, so her face can be seen.
|
||||
drawOrbChaser(Offset(o.x + game.ballX * k, o.y + game.ballY * k), BALL_R * k * 2.2f, OrbCharacter.Luna, OrbDir.Down, scared = false, returning = false, blink = false, wobble = 0f)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -271,7 +271,7 @@ internal enum class OrbCharacter(val title: String, val colour: Color, val line:
|
||||
|
||||
internal val OrbChaserColours = OrbCharacter.entries.map { it.colour }
|
||||
|
||||
private fun starPath(centre: Offset, outer: Float, inner: Float, turn: Float) = Path().apply {
|
||||
internal fun starPath(centre: Offset, outer: Float, inner: Float, turn: Float) = Path().apply {
|
||||
for (k in 0 until 10) {
|
||||
val a = -PI / 2 + k * PI / 5 + turn
|
||||
val rad = if (k % 2 == 0) outer else inner
|
||||
|
||||
@@ -65,9 +65,11 @@ internal data class PairsGame(val cards: List<Int>, val matched: Set<Int> = empt
|
||||
}
|
||||
}
|
||||
|
||||
/** The ten channels whose logos make the pairs. */
|
||||
// A family game, so After Dark isn't among the cards.
|
||||
private val PairsChannels = listOf(101, 102, 201, 203, 301, 302, 309, 401, 601, 612)
|
||||
/**
|
||||
* The ten pairs: six of Orbit's channels and four of the cast (Paul, 6 November 2026), whose cards hold a number below 100:
|
||||
* the cast member's place in [OrbCharacter], or 0 for Ollie. A family game, so After Dark isn't among the cards.
|
||||
*/
|
||||
private val PairsChannels = listOf(101, 201, 301, 401, 601, 612, 0, 1, 2, 4)
|
||||
|
||||
private val Marquee = FontFamily(Font(R.font.anton_regular))
|
||||
private val Honey = Color(0xFFFFD84A)
|
||||
@@ -78,7 +80,12 @@ private fun PairsCard(channel: Int, up: Boolean, matched: Boolean, selected: Boo
|
||||
Box(modifier.graphicsLayer { rotationY = 180f * (1 - turn); cameraDistance = 12 * density }
|
||||
.background(if (turn > .5f) Color(0xFF0B1B44) else NightPanel, RoundedCornerShape(8.dp))
|
||||
.border(if (selected) 3.dp else 1.dp, if (selected) NightYellow else if (matched) Honey else NightBlue, RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center) {
|
||||
if (turn > .5f) ChannelBrands.forNumber(channel)?.let { Image(painterResource(it.logo), it.displayName, Modifier.fillMaxSize().padding(10.dp).alpha(if (matched) .7f else 1f)) }
|
||||
if (turn > .5f && channel < 100) androidx.compose.foundation.Canvas(Modifier.fillMaxSize().padding(8.dp).alpha(if (matched) .7f else 1f)) {
|
||||
val r = size.minDimension * .36f
|
||||
if (channel == 0) drawOrbMascot(center, r * .8f)
|
||||
else drawOrbChaser(center, r, OrbCharacter.entries[channel - 1], OrbDir.Down, scared = false, returning = false, blink = false, wobble = 0f)
|
||||
}
|
||||
else if (turn > .5f) ChannelBrands.forNumber(channel)?.let { Image(painterResource(it.logo), it.displayName, Modifier.fillMaxSize().padding(10.dp).alpha(if (matched) .7f else 1f)) }
|
||||
else Text("O", color = NightMuted.copy(alpha = .5f), fontFamily = Marquee, fontSize = 26.sp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,8 @@ private fun CardFace(card: PlayingCard?, empty: String = "", modifier: Modifier
|
||||
Text(card.label, color = colour, fontSize = 15.sp, fontWeight = FontWeight.Bold)
|
||||
Text("♠♥♦♣"[card.suit].toString(), Modifier.align(Alignment.Center), color = colour, fontSize = 26.sp)
|
||||
}
|
||||
else -> Text("O", Modifier.align(Alignment.Center), color = NightMuted.copy(alpha = .5f), fontFamily = Marquee, fontSize = 20.sp)
|
||||
// Ollie on every card back (Paul, 6 November 2026: each game hosted by one of the cast).
|
||||
else -> androidx.compose.foundation.Canvas(Modifier.fillMaxSize()) { drawOrbMascot(center, size.minDimension * .24f) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import kotlin.math.sin
|
||||
* First launch for a new user (Paul, 5 November 2026, from the approved design; Emby first, the other servers later). Welcome
|
||||
* with Ollie, choose the server, sign in (Orbit's own sign-in), pick the channels while Drift sets them up and shows what went
|
||||
* where, who watches (the Adult PIN and Kids), and You're on air. It runs once, for a box that has never been signed in.
|
||||
* Settings › Emby account can show it again as a tour, which changes nothing. Ollie rolls in and blinks; Drift bobs and blinks;
|
||||
* Settings › Account can show it again as a tour, which changes nothing. Ollie rolls in and blinks; Drift bobs and blinks;
|
||||
* at the end the cast floats right through the stars. All still with Reduce motion on.
|
||||
*/
|
||||
internal enum class OnboardStep(val label: String) { Welcome("Welcome"), Look("Look"), Server("Server"), SignIn("Sign in"), Channels("Channels"), WhoWatches("Who watches"), AllSet("All set") }
|
||||
@@ -162,7 +162,7 @@ private fun LookStep(onNext: () -> Unit) {
|
||||
LaunchedEffect(Unit) { first.requestFocus() }
|
||||
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically)) {
|
||||
Text("Choose your look", color = Ink, fontFamily = Anton, fontSize = 32.sp)
|
||||
Text(if (Onboarding.tour) "Try one: it goes back to yours when the tour ends." else "It changes straight away. You can change it any time in Settings › Look.", color = Muted, fontSize = 15.sp)
|
||||
Text(if (Onboarding.tour) "Try one: it goes back to yours when the tour ends." else "It changes straight away. You can change it any time in Settings › On screen.", color = Muted, fontSize = 15.sp)
|
||||
LookPicker(first, cardWidth = 200.dp)
|
||||
OrbitButton("Next", Modifier.padding(top = 6.dp).width(200.dp), centred = true, onClick = onNext)
|
||||
}
|
||||
@@ -345,7 +345,7 @@ private fun WhoWatchesStep(onNext: () -> Unit) {
|
||||
}
|
||||
note?.let { Text(it, color = Yellow, fontSize = 14.sp) }
|
||||
OrbitButton("Next", Modifier.padding(top = 8.dp).width(200.dp), centred = true, onClick = onNext)
|
||||
Text("You can skip this: with no PIN, Adult opens without one. Change both in Settings › Profiles.", color = Muted, fontSize = 12.sp)
|
||||
Text("You can skip this: with no PIN, Adult opens without one. Change both in Settings › Family.", color = Muted, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,22 +23,18 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.FilterQuality
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
@@ -156,27 +152,29 @@ internal fun snakeLine(game: SnakeGame, before: List<SnakeCell>?, progress: Floa
|
||||
(from.x + (cell.x - from.x) * progress) to (from.y + (cell.y - from.y) * progress)
|
||||
}
|
||||
|
||||
/**
|
||||
* Comet Trail (Paul, 6 November 2026, from the approved design/next): Snake, played as Blaze. He eats stars and his tail
|
||||
* grows a puff for each, fading from honey to the sky behind him. Caught, he turns the cast's frightened blue.
|
||||
*/
|
||||
@Composable
|
||||
private fun SnakeField(game: SnakeGame, modifier: Modifier, before: List<SnakeCell>? = null, progress: Float = 1f) {
|
||||
val head = ImageBitmap.imageResource(R.drawable.snake_head)
|
||||
val dead = ImageBitmap.imageResource(R.drawable.snake_dead)
|
||||
val blob = ImageBitmap.imageResource(R.drawable.snake_blob)
|
||||
val apple = ImageBitmap.imageResource(R.drawable.snake_apple)
|
||||
Canvas(modifier) {
|
||||
val cell = minOf(size.width / game.width, size.height / game.height)
|
||||
val left = (size.width - cell * game.width) / 2
|
||||
val top = (size.height - cell * game.height) / 2
|
||||
for (y in 0 until game.height) for (x in 0 until game.width) if ((x + y) % 2 == 0)
|
||||
drawRect(Ink.copy(alpha = .025f), Offset(left + x * cell, top + y * cell), Size(cell, cell))
|
||||
fun sprite(image: ImageBitmap, x: Float, y: Float, scale: Float) {
|
||||
val side = (cell * scale).toInt()
|
||||
val offset = (cell - side) / 2
|
||||
drawImage(image, dstOffset = IntOffset((left + x * cell + offset).toInt(), (top + y * cell + offset).toInt()), dstSize = IntSize(side, side), filterQuality = FilterQuality.Medium)
|
||||
}
|
||||
sprite(apple, game.food.x.toFloat(), game.food.y.toFloat(), 1.05f)
|
||||
fun at(x: Float, y: Float) = Offset(left + (x + .5f) * cell, top + (y + .5f) * cell)
|
||||
drawPath(starPath(at(game.food.x.toFloat(), game.food.y.toFloat()), cell * .5f, cell * .22f, 0f), Color(0xFFFFDE59))
|
||||
val line = snakeLine(game, before, progress)
|
||||
line.drop(1).asReversed().forEach { (x, y) -> sprite(blob, x, y, 1f) }
|
||||
line.first().let { (x, y) -> sprite(if (game.alive) head else dead, x, y, 1.25f) }
|
||||
val tail = line.drop(1)
|
||||
tail.forEachIndexed { i, (x, y) ->
|
||||
// Nearest the head: honey and full; at the end: small and faded into the sky.
|
||||
val k = 1f - i / (tail.size + 1f)
|
||||
drawCircle(lerp(Color(0xFF6E4A2A), Color(0xFFFFE066), k).copy(alpha = .45f + .55f * k), cell * (.24f + .2f * k), at(x, y))
|
||||
}
|
||||
val dir = when (game.heading) { SnakeDirection.Up -> OrbDir.Up; SnakeDirection.Down -> OrbDir.Down; SnakeDirection.Left -> OrbDir.Left; SnakeDirection.Right -> OrbDir.Right }
|
||||
line.first().let { (x, y) -> drawOrbChaser(at(x, y), cell * .55f, OrbCharacter.Blaze, dir, scared = !game.alive, returning = false, blink = false, wobble = 0f) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +232,7 @@ private fun SnakeScreen(best: Int, onBest: (Int) -> Unit, onLeave: () -> Unit) {
|
||||
direction != null
|
||||
}, horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Column(Modifier.width(190.dp).fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Text("SNAKE", color = Honey, fontFamily = Marquee, fontSize = 40.sp)
|
||||
Text("COMET TRAIL", color = Honey, fontFamily = Marquee, fontSize = 30.sp, lineHeight = 32.sp)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text("SCORE", color = Muted, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 2.sp)
|
||||
Text(game.score.toString(), color = Ink, fontFamily = Marquee, fontSize = 36.sp)
|
||||
@@ -311,11 +309,11 @@ private enum class ArcadeGame(val title: String, val tagline: String, val blurb:
|
||||
Blast("Blast Off", "Fit the shapes, clear the lines", "Fit three shapes at a time on the board; full rows and columns clear. No room left and it's over."),
|
||||
Sky("Skydoku", "Find each of the cast a spot", "Put each of the cast in their colour: one per row and column, none touching. Three hearts, a puzzle every day."),
|
||||
Hive("BumbleHive", "Link, match, clear the hive", "Help Bumble fire flowers into the honeycomb, and clear the hive before it reaches you."),
|
||||
Snake("Snake", "Grow, turn, beat your best", "Eat, grow, and don't hit the walls or yourself."),
|
||||
Blocks("Block Drop", "Stack the blocks, keep it tidy", "Turn and drop the falling blocks, and clear full rows before the stack reaches the top."),
|
||||
Bricks("Brick Break", "Bounce, break, clear the screen", "Keep the ball in play with your paddle and break every brick in the wall."),
|
||||
Pairs("Pairs", "Find the matching pairs", "Turn two cards at a time and find every pair of Orbit's channels in as few moves as you can."),
|
||||
Patience("Patience", "Classic solitaire on Orbit", "The classic card game: build each suit home from ace to king.")
|
||||
Snake("Comet Trail", "Snake, as Blaze's tail", "Help Blaze eat the stars: each one grows his tail. Don't hit the walls or your own trail."),
|
||||
Blocks("Block Drop", "Drift's stacking game", "Turn and drop the falling blocks, and clear full rows before the stack reaches the top."),
|
||||
Bricks("Brick Break", "Luna is the ball", "Keep the ball in play with your paddle and break every brick in the wall."),
|
||||
Pairs("Pairs", "Match the cast and the channels", "Turn two cards at a time and find every pair of the cast and Orbit's channels, in as few moves as you can."),
|
||||
Patience("Patience", "Solitaire, with Ollie on every card", "The classic card game: build each suit home from ace to king.")
|
||||
}
|
||||
|
||||
/** The newest game, flagged as the arcade's pick. */
|
||||
@@ -333,20 +331,10 @@ private fun androidx.compose.ui.graphics.drawscope.DrawScope.gameIcon(game: Arca
|
||||
// All six of the cast, two rows of three.
|
||||
ArcadeGame.Sky -> { val b = s * .3f
|
||||
for (i in 0..5) { val at = c + Offset((i % 3 - 1f) * b * 1.05f, (i / 3 - .5f) * b * 1.05f); drawRoundRect(SkyColours[i].copy(alpha = .4f), at - Offset(b / 2, b / 2), Size(b, b), androidx.compose.ui.geometry.CornerRadius(b * .15f)); drawSkyFace(i, at, b * .4f) } }
|
||||
ArcadeGame.Snake -> {
|
||||
listOf(-.32f, -.16f, 0f).forEachIndexed { i, x -> drawCircle(Color(0xFF3DDC97), s * .1f, c + Offset(x * s, (if (i % 2 == 0) .12f else .04f) * s)) }
|
||||
val head = c + Offset(.2f * s, -.06f * s)
|
||||
drawCircle(Color(0xFF3DDC97), s * .14f, head)
|
||||
drawCircle(Ink, s * .05f, head + Offset(-s * .05f, -s * .03f)); drawCircle(Ink, s * .05f, head + Offset(s * .05f, -s * .03f))
|
||||
drawCircle(Navy, s * .025f, head + Offset(-s * .05f, -s * .03f)); drawCircle(Navy, s * .025f, head + Offset(s * .05f, -s * .03f))
|
||||
}
|
||||
ArcadeGame.Blocks -> { val b = s * .2f
|
||||
listOf(Offset(0f, -1f), Offset(-1f, 0f), Offset(0f, 0f), Offset(1f, 0f)).forEach { o -> drawRect(Color(0xFF4C7DFF), c + Offset(o.x * b - b / 2 + 1, o.y * b - 1), Size(b - 2, b - 2)) } }
|
||||
ArcadeGame.Bricks -> {
|
||||
for (row in 0..1) for (col in 0..2) drawRect(Color(0xFFFF6BA8), Offset(c.x - s * .4f + col * s * .27f + (row % 2) * s * .1f, c.y - s * .35f + row * s * .16f), Size(s * .24f, s * .12f))
|
||||
drawCircle(Ink, s * .06f, c + Offset(s * .05f, s * .12f))
|
||||
drawRect(Color(0xFF6EF0FF), Offset(c.x - s * .18f, c.y + s * .3f), Size(s * .36f, s * .06f))
|
||||
}
|
||||
// Each game's host from the cast (Paul, 6 November 2026).
|
||||
ArcadeGame.Snake -> drawOrbChaser(c + Offset(s * .1f, 0f), s * .3f, OrbCharacter.Blaze, OrbDir.Right, false, false, false, 0f)
|
||||
ArcadeGame.Blocks -> drawOrbChaser(c, s * .3f, OrbCharacter.Drift, OrbDir.Down, false, false, false, 0f)
|
||||
ArcadeGame.Bricks -> drawOrbChaser(c, s * .3f, OrbCharacter.Luna, OrbDir.Down, false, false, false, 0f)
|
||||
ArcadeGame.Pairs -> for (i in 0..1) {
|
||||
val at = Offset(c.x - s * .36f + i * s * .38f, c.y - s * .3f)
|
||||
drawRoundRect(Color(0xFF1E3E8C), at, Size(s * .34f, s * .5f), androidx.compose.ui.geometry.CornerRadius(s * .04f))
|
||||
|
||||
@@ -9,7 +9,7 @@ main{width:min(92vw,380px)}h1{font-size:30px;margin:0 0 6px}p{color:#b7cbe8}inpu
|
||||
padding:12px;border-radius:12px;border:2px solid #194c91;background:#12356b;color:#fff;margin:12px 0}button{width:100%;padding:14px;border:0;border-radius:12px;
|
||||
background:#ffde59;color:#071632;font-size:18px;font-weight:700}.note{color:#ffde59}</style>"""
|
||||
fun join(note: String?) = """<!doctype html><html><head><title>Orbit</title>$STYLE</head><body><main><h1>Orbit</h1>
|
||||
<p>Enter the code shown on the TV in Settings › Channels › Create a channel › On your phone.</p>${note?.let { "<p class='note'>${esc(it)}</p>" }.orEmpty()}
|
||||
<p>Enter the code shown on the TV in Settings › Channels › Create your own › On your phone.</p>${note?.let { "<p class='note'>${esc(it)}</p>" }.orEmpty()}
|
||||
<form method="post" action="/join"><input name="code" inputmode="numeric" maxlength="4" autocomplete="one-time-code" autofocus><button>Join</button></form></main></body></html>"""
|
||||
fun pin(k: String?, code: String?, wrong: Boolean) = """<!doctype html><html><head><title>Orbit</title>$STYLE</head><body><main><h1>Adult PIN</h1>
|
||||
<p>The code worked. Making channels is for adults, so now enter your <b>Adult PIN</b>: the four digits that unlock After Dark on the TV.</p>
|
||||
|
||||
@@ -191,7 +191,7 @@ internal fun WhosWatching(onClose: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Text("Kids sees only the channels and games chosen for it." + if (AfterDarkLock.pinSet) " Adult needs the PIN." else " Set a PIN in Settings › Profiles to keep Adult for grown-ups.",
|
||||
Text("Kids sees only the channels and games chosen for it." + if (AfterDarkLock.pinSet) " Adult needs the PIN." else " Set a PIN in Settings › Family to keep Adult for grown-ups.",
|
||||
color = Muted, fontSize = 15.sp)
|
||||
}
|
||||
asking?.let { p ->
|
||||
|
||||
@@ -2,7 +2,11 @@ package tv.orbit.classic
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
@@ -118,14 +122,17 @@ internal class OrbitSettings(context: Context) {
|
||||
set(value) { saved.edit().putBoolean("reduce_motion", value).apply() }
|
||||
}
|
||||
|
||||
internal enum class SettingsSection(val label: String, val icon: Int) {
|
||||
Channels("Channels", R.drawable.orbit_icon_all),
|
||||
Viewing("Watching", R.drawable.orbit_icon_entertainment),
|
||||
Arrivals("New arrivals", R.drawable.orbit_icon_movies),
|
||||
Director("Director's Chair", R.drawable.orbit_icon_entertainment),
|
||||
Profiles("Profiles", R.drawable.orbit_icon_profile),
|
||||
Look("Look", R.drawable.orbit_icon_all),
|
||||
Account("Emby account", R.drawable.orbit_icon_profile),
|
||||
/**
|
||||
* Settings in five plain sections (Paul, 6 November 2026, from the approved design/next): what was Watching and Look is
|
||||
* On screen; Director's Chair and New arrivals open from cards on Channels; Profiles and Wind Down are Family; phones (and
|
||||
* other rooms, with Multiroom) have their own section.
|
||||
*/
|
||||
internal enum class SettingsSection(val label: String, val line: String, val icon: Int) {
|
||||
Channels("Channels", "Turn on, make your own, suggestions", R.drawable.orbit_icon_all),
|
||||
OnScreen("On screen", "Look, subtitles, the cast, motion", R.drawable.orbit_icon_entertainment),
|
||||
Family("Family", "Profiles, PIN, Kids, Wind Down", R.drawable.orbit_icon_profile),
|
||||
Rooms("Rooms and phones", "Other TVs and paired phones", R.drawable.orbit_icon_movies),
|
||||
Account("Account", "Server, sign-in, version", R.drawable.orbit_icon_profile),
|
||||
}
|
||||
|
||||
/** The heading a channel number sits under, matching the blocks in the guide. */
|
||||
@@ -145,14 +152,17 @@ private val RailCorner = RoundedCornerShape(6.dp)
|
||||
@Composable
|
||||
private fun RailItem(section: SettingsSection, selected: Boolean, modifier: Modifier, onRight: () -> Unit, onSelect: () -> Unit) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Row(modifier.fillMaxWidth().height(46.dp).onPreviewKeyEvent { event ->
|
||||
Row(modifier.fillMaxWidth().height(58.dp).onPreviewKeyEvent { event ->
|
||||
// Straight to the section's first switch. Left alone, right could land on the top bar instead.
|
||||
if (event.key == Key.DirectionRight) { if (event.type == KeyEventType.KeyDown) onRight(); true } else false
|
||||
}.onFocusChanged { focused = it.isFocused; if (it.isFocused) onSelect() }.clip(RailCorner)
|
||||
.background(if (focused) Yellow else if (selected) Blue else Panel).clickable(onClick = onSelect).padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Image(painterResource(section.icon), null, Modifier.size(26.dp), colorFilter = ColorFilter.tint(if (focused) OnAccent else Ink))
|
||||
Text(section.label, color = if (focused) OnAccent else Ink, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
||||
Column {
|
||||
Text(section.label, color = if (focused) OnAccent else Ink, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
||||
Text(section.line, color = if (focused) OnAccent.copy(alpha = .75f) else Muted, fontSize = 10.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +178,33 @@ private fun SettingLine(title: String, detail: String, button: @Composable () ->
|
||||
}
|
||||
}
|
||||
|
||||
/** A switch: a pill, green and to the right when on. Focus rings it in the look's accent. */
|
||||
@Composable
|
||||
internal fun OnOff(on: Boolean, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
val track = if (on) themed(Color(0xFF6EDCAA)) { Mint } else themed(Color(0xFF283C64)) { Blue }
|
||||
Box(modifier.onFocusChanged { focused = it.isFocused }.size(64.dp, 34.dp).border(if (focused) 3.dp else 0.dp, if (focused) Yellow else Color.Transparent, RoundedCornerShape(17.dp))
|
||||
.padding(3.dp).clip(RoundedCornerShape(14.dp)).background(track.copy(alpha = if (enabled) 1f else .5f)).clickable(enabled = enabled, onClick = onClick)
|
||||
.semantics { stateDescription = if (on) "On" else "Off" }, contentAlignment = if (on) Alignment.CenterEnd else Alignment.CenterStart) {
|
||||
Box(Modifier.padding(horizontal = 4.dp).size(20.dp).clip(androidx.compose.foundation.shape.CircleShape).background(Color.White))
|
||||
}
|
||||
}
|
||||
|
||||
/** A card above the channel list: one of the cast, what it opens, and a line on what it's for. */
|
||||
@Composable
|
||||
private fun ChannelCard(title: String, line: String, who: String, modifier: Modifier, onClick: () -> Unit) {
|
||||
var focused by remember { mutableStateOf(false) }
|
||||
Row(modifier.onFocusChanged { focused = it.isFocused }.clip(RoundedCornerShape(10.dp)).background(if (focused) Navy else Panel)
|
||||
.border(if (focused) 3.dp else 0.dp, if (focused) Yellow else Color.Transparent, RoundedCornerShape(10.dp)).clickable(onClick = onClick).padding(10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
ProfileAvatar(who, 40.dp)
|
||||
Column {
|
||||
Text(title, color = Ink, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
||||
Text(line, color = Muted, fontSize = 11.sp, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boolean,
|
||||
onSwitch: (ChannelProgramming, Boolean) -> Unit, onSuggestions: (ChannelProgramming) -> Unit, firstSwitch: FocusRequester, list: LazyListState, modifier: Modifier,
|
||||
@@ -177,7 +214,6 @@ private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boole
|
||||
Text("${all.count { isOn(it.number) }} of ${all.size} channels on", color = Ink, fontSize = 18.sp, fontWeight = FontWeight.SemiBold)
|
||||
Text("A channel that's on runs its full schedule. Once it's been on, turning it off leaves a short repeat.", color = Muted, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
message?.let { Text(it, color = Yellow, fontSize = 13.sp, maxLines = 3, overflow = TextOverflow.Ellipsis) }
|
||||
if (onCreate != null) OrbitButton("Create a channel", enabled = !busy, onClick = onCreate)
|
||||
LazyColumn(Modifier.weight(1f), state = list, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
items(all, key = { it.number }) { programming ->
|
||||
val on = isOn(programming.number)
|
||||
@@ -191,7 +227,7 @@ private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boole
|
||||
Text(brand?.displayName ?: programming.name, Modifier.weight(1f), color = Ink, fontSize = 14.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
if (programming.suggestionRules.isNotEmpty()) OrbitButton("Titles", enabled = !busy, onClick = { onSuggestions(programming) })
|
||||
CustomChannels.of(programming.number)?.let { yours -> OrbitButton("Edit", enabled = !busy, onClick = { onEdit(yours) }) }
|
||||
OrbitButton(if (on) "On" else "Off", Modifier.width(64.dp).then(if (programming == all.first()) Modifier.focusRequester(firstSwitch) else Modifier), selected = on, enabled = !busy, onClick = { onSwitch(programming, !on) })
|
||||
OnOff(on, Modifier.then(if (programming == all.first()) Modifier.focusRequester(firstSwitch) else Modifier), enabled = !busy, onClick = { onSwitch(programming, !on) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +236,7 @@ private fun ChannelsDetail(busy: Boolean, message: String?, isOn: (Int) -> Boole
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Boolean) -> Unit, firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
private fun ViewingDetail(firstSwitch: FocusRequester, modifier: Modifier, top: @Composable () -> Unit = {}) {
|
||||
val context = LocalContext.current
|
||||
val settings = remember { OrbitSettings(context) }
|
||||
var subtitlesOn by remember { mutableStateOf(settings.subtitlesOn) }
|
||||
@@ -213,31 +249,33 @@ private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Bo
|
||||
var castOn by remember { mutableStateOf(settings.castOn) }
|
||||
var specialIdents by remember { mutableStateOf(settings.specialIdents) }
|
||||
var blockIdentSound by remember { mutableStateOf(settings.blockIdentSound) }
|
||||
var confirming by remember { mutableStateOf(false) }
|
||||
Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
top()
|
||||
Heading("Watching")
|
||||
SettingLine("Subtitles", "Turn on English subtitles whenever you tune in, if the programme has them. You can still change them while watching, by pressing right.") {
|
||||
OrbitButton(if (subtitlesOn) "On" else "Off", Modifier.width(64.dp).focusRequester(firstSwitch), selected = subtitlesOn, onClick = { subtitlesOn = !subtitlesOn; settings.subtitlesOn = subtitlesOn })
|
||||
OnOff(subtitlesOn, Modifier, onClick = { subtitlesOn = !subtitlesOn; settings.subtitlesOn = subtitlesOn })
|
||||
}
|
||||
SettingLine("Channel logo", "Show the channel's small logo in the corner during programmes.") {
|
||||
OrbitButton(if (channelLogo) "On" else "Off", Modifier.width(64.dp), selected = channelLogo, onClick = { channelLogo = !channelLogo; settings.channelLogo = channelLogo })
|
||||
OnOff(channelLogo, Modifier, onClick = { channelLogo = !channelLogo; settings.channelLogo = channelLogo })
|
||||
}
|
||||
Heading("Orbit between programmes")
|
||||
SettingLine("Idents", "Play each channel's ident between programmes. When off, Tots shows a still picture instead.") {
|
||||
OrbitButton(if (idents) "On" else "Off", Modifier.width(64.dp), selected = idents, onClick = { idents = !idents; settings.idents = idents })
|
||||
OnOff(idents, Modifier, onClick = { idents = !idents; settings.idents = idents })
|
||||
}
|
||||
SettingLine("Ident sound", "Let Tots play its jingle, at most once every 30 minutes.") {
|
||||
OrbitButton(if (identSound) "On" else "Off", Modifier.width(64.dp), selected = identSound, onClick = { identSound = !identSound; settings.identSound = identSound })
|
||||
OnOff(identSound, Modifier, onClick = { identSound = !identSound; settings.identSound = identSound })
|
||||
}
|
||||
SettingLine("Special idents", "Give named blocks and events their own ident as they begin, such as Riot Laughs or PPV Sunday. Off uses each channel's usual ident.") {
|
||||
OrbitButton(if (specialIdents) "On" else "Off", Modifier.width(64.dp), selected = specialIdents, onClick = { specialIdents = !specialIdents; settings.specialIdents = specialIdents })
|
||||
OnOff(specialIdents, Modifier, onClick = { specialIdents = !specialIdents; settings.specialIdents = specialIdents })
|
||||
}
|
||||
SettingLine("Special ident sound", "Play the short sound with each special ident. Off keeps them silent.") {
|
||||
OrbitButton(if (blockIdentSound) "On" else "Off", Modifier.width(64.dp), selected = blockIdentSound, onClick = { blockIdentSound = !blockIdentSound; settings.blockIdentSound = blockIdentSound })
|
||||
OnOff(blockIdentSound, Modifier, onClick = { blockIdentSound = !blockIdentSound; settings.blockIdentSound = blockIdentSound })
|
||||
}
|
||||
SettingLine("Squeeze-back", "Over a programme's last minute, the credits shrink into a corner and what's next on the channel fills the screen beside them.") {
|
||||
OrbitButton(if (squeezeBack) "On" else "Off", Modifier.width(64.dp), selected = squeezeBack, onClick = { squeezeBack = !squeezeBack; settings.squeezeBack = squeezeBack })
|
||||
OnOff(squeezeBack, Modifier, onClick = { squeezeBack = !squeezeBack; settings.squeezeBack = squeezeBack })
|
||||
}
|
||||
SettingLine("The cast", "Show Ollie, Zip, Blaze, Drift and Luna in the squeeze-back and promos.") {
|
||||
OrbitButton(if (castOn) "On" else "Off", Modifier.width(64.dp), selected = castOn, onClick = { castOn = !castOn; settings.castOn = castOn })
|
||||
OnOff(castOn, Modifier, onClick = { castOn = !castOn; settings.castOn = castOn })
|
||||
}
|
||||
var promoNote by remember { mutableStateOf<String?>(null) }
|
||||
SettingLine("Promos", promoNote ?: "Now and then a promo for tonight's highlights plays in place of a channel's ident. See one now; it closes by itself.") {
|
||||
@@ -247,11 +285,23 @@ private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Bo
|
||||
})
|
||||
}
|
||||
SettingLine("Drive-In curtains", "Play the curtain intro, with its fanfare, as you go into a screening. Back skips it.") {
|
||||
OrbitButton(if (driveInIntro) "On" else "Off", Modifier.width(64.dp), selected = driveInIntro, onClick = { driveInIntro = !driveInIntro; settings.driveInIntro = driveInIntro })
|
||||
OnOff(driveInIntro, Modifier, onClick = { driveInIntro = !driveInIntro; settings.driveInIntro = driveInIntro })
|
||||
}
|
||||
Heading("Comfort")
|
||||
SettingLine("Reduce motion", "Leave out moving presentation, such as the Drive-In's curtains.") {
|
||||
OrbitButton(if (reduceMotion) "On" else "Off", Modifier.width(64.dp), selected = reduceMotion, onClick = { reduceMotion = !reduceMotion; settings.reduceMotion = reduceMotion })
|
||||
OnOff(reduceMotion, Modifier, onClick = { reduceMotion = !reduceMotion; settings.reduceMotion = reduceMotion })
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/** Family: who's watching and what Kids can see, then bedtime. */
|
||||
@Composable
|
||||
private fun FamilyDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Boolean) -> Unit, firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
var confirming by remember { mutableStateOf(false) }
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ProfilesDetail(firstSwitch, Modifier.weight(1f))
|
||||
Heading("Bedtime")
|
||||
if (totsOn) {
|
||||
if (confirming) SettingLine(if (windingDown) "Go back to Tots' usual schedule now?" else "Start Wind Down on Tots?", if (windingDown) "The programme on Tots changes straight away." else "Calm shows begin when the current programme ends, and run until morning unless you stop them.") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
@@ -261,12 +311,29 @@ private fun ViewingDetail(totsOn: Boolean, windingDown: Boolean, onWindDown: (Bo
|
||||
OrbitButton("Yes", onClick = { confirming = false; onWindDown(!windingDown) })
|
||||
}
|
||||
} else SettingLine("Wind Down Now", "Calm shows on Tots at any time of day. Tots' usual schedule stays saved.") {
|
||||
OrbitButton(if (windingDown) "On" else "Off", Modifier.width(64.dp), selected = windingDown, onClick = { confirming = true })
|
||||
OnOff(windingDown, Modifier, onClick = { confirming = true })
|
||||
}
|
||||
}
|
||||
} else SettingLine("Wind Down", "Turn Tots on in Channels to use Wind Down: calm shows on Tots, with Luna.") {}
|
||||
}
|
||||
}
|
||||
|
||||
/** Rooms and phones: the phones that can make channels, and (with Multiroom) the other TVs in the home. */
|
||||
@Composable
|
||||
private fun RoomsDetail(firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Heading("Phones")
|
||||
if (PhoneLink.phones.isEmpty()) SettingLine("No phones yet", "Channels › Create your own › On your phone shows a code to scan.") {}
|
||||
else PhoneLink.phones.forEachIndexed { i, (id, name) -> SettingLine(name, "Can make and edit your channels, and tune this TV.") {
|
||||
OrbitButton("Remove", if (i == 0) Modifier.focusRequester(firstSwitch) else Modifier, onClick = { PhoneLink.forget(id) }) } }
|
||||
Heading("Other rooms")
|
||||
SettingLine("Multiroom is on its way", "Soon a TV in another room can join this one and copy its channels and settings. Your password never travels.") {}
|
||||
}
|
||||
}
|
||||
|
||||
/** A small heading over a group of settings. */
|
||||
@Composable
|
||||
private fun Heading(text: String) = Text(text.uppercase(), Modifier.padding(top = 6.dp), color = Muted, fontSize = 11.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.sp)
|
||||
|
||||
@Composable
|
||||
private fun AccountDetail(server: String, titles: Long?, onSignOut: () -> Unit, onRemoveServer: () -> Unit, firstSwitch: FocusRequester, modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
@@ -296,17 +363,15 @@ private fun AccountDetail(server: String, titles: Long?, onSignOut: () -> Unit,
|
||||
}
|
||||
}
|
||||
} else SettingLine("Remove this server", "For leaving a box or starting again: deletes this server's data from this box only.") { OrbitButton("Remove", onClick = { removing = true }) }
|
||||
// Phones that have joined for Create your own: each can be removed here.
|
||||
if (PhoneLink.phones.isEmpty()) SettingLine("Phones", "No phones have joined. Create a channel › On your phone shows a code.") {}
|
||||
else PhoneLink.phones.forEach { (id, name) -> SettingLine("Phone: $name", "Can make and edit your channels, and tune this TV.") { OrbitButton("Remove", onClick = { PhoneLink.forget(id) }) } }
|
||||
SettingLine("Welcome tour", "See the first-launch steps a new box goes through, from Ollie's welcome to You're on air. Nothing is changed by it.") {
|
||||
OrbitButton("Show it", onClick = { Onboarding.start(tour = true) })
|
||||
}
|
||||
Text("Orbit TV $version", color = Muted, fontSize = 12.sp)
|
||||
Text("Snake graphics by Bas de Reuver, bdrgames.nl", color = Muted, fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class ChannelPage { Director, Arrivals }
|
||||
|
||||
/** Everything behind the gear: a rail of sections on the left, and the chosen one beside it. */
|
||||
@Composable
|
||||
internal fun SettingsScreen(section: SettingsSection, onSection: (SettingsSection) -> Unit, modifier: Modifier, busy: Boolean, message: String?, server: String, titles: Long?,
|
||||
@@ -319,6 +384,9 @@ internal fun SettingsScreen(section: SettingsSection, onSection: (SettingsSectio
|
||||
// Making or editing a channel of your own takes over the Channels detail until it's done.
|
||||
var making by remember { mutableStateOf<Pair<Boolean, CustomChannel?>?>(null) }
|
||||
var made by remember { mutableStateOf<String?>(null) }
|
||||
var opened by remember { mutableStateOf<ChannelPage?>(null) }
|
||||
LaunchedEffect(section) { opened = null }
|
||||
val listFirst = remember { FocusRequester() }
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
val firstSwitch = remember { FocusRequester() }
|
||||
val channelList = rememberLazyListState()
|
||||
@@ -327,7 +395,7 @@ internal fun SettingsScreen(section: SettingsSection, onSection: (SettingsSectio
|
||||
Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text("Settings", color = Ink, fontSize = 22.sp, fontWeight = FontWeight.SemiBold)
|
||||
Row(Modifier.fillMaxWidth().weight(1f), horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
Column(Modifier.width(176.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Column(Modifier.width(224.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
SettingsSection.entries.forEach { item ->
|
||||
RailItem(item, item == section, if (item == section) Modifier.focusRequester(firstFocus) else Modifier, onRight = {
|
||||
// The channel list may have been scrolled, taking its first switch off the screen.
|
||||
@@ -337,17 +405,28 @@ internal fun SettingsScreen(section: SettingsSection, onSection: (SettingsSectio
|
||||
}
|
||||
val detail = Modifier.weight(1f).fillMaxHeight()
|
||||
when (section) {
|
||||
SettingsSection.Channels -> making?.let { (_, editing) -> if (maker != null) CreateChannelFlow(maker, editing, { result -> made = result; making = null }, detail) }
|
||||
?: ChannelsDetail(busy, made ?: message, isOn, onSwitch, onSuggestions, firstSwitch, channelList, detail,
|
||||
onCreate = if (maker != null) ({ made = null; making = true to null }) else null, onEdit = { c -> made = null; making = true to c })
|
||||
SettingsSection.Viewing -> ViewingDetail(isOn(TOTS_NUMBER), windingDown, onWindDown, firstSwitch, detail)
|
||||
SettingsSection.Arrivals -> arrivals(detail, firstSwitch)
|
||||
SettingsSection.Director -> director(detail, firstSwitch)
|
||||
SettingsSection.Profiles -> ProfilesDetail(firstSwitch, detail)
|
||||
SettingsSection.Look -> Column(detail, verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text("Every screen takes the look you pick, straight away. The cast, the channels' idents and the games keep their own colours.", color = Muted, fontSize = 13.sp)
|
||||
LookPicker(firstSwitch, cardWidth = 190.dp)
|
||||
SettingsSection.Channels -> when {
|
||||
making != null && maker != null -> making?.let { (_, editing) -> CreateChannelFlow(maker, editing, { result -> made = result; making = null }, detail) }
|
||||
// Director's Chair and New arrivals open in place of the list; Back returns to it.
|
||||
opened == ChannelPage.Director -> { BackHandler { opened = null }; director(detail, firstSwitch) }
|
||||
opened == ChannelPage.Arrivals -> { BackHandler { opened = null }; arrivals(detail, firstSwitch) }
|
||||
else -> Column(detail, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val card = Modifier.weight(1f).height(72.dp)
|
||||
if (maker != null) ChannelCard("Create your own", "Your own channel, on this TV or your phone", "ollie", card.focusRequester(firstSwitch)) { made = null; making = true to null }
|
||||
ChannelCard("Director's Chair", "Suggestions for your channels, and events", "luna", if (maker == null) card.focusRequester(firstSwitch) else card) { opened = ChannelPage.Director }
|
||||
ChannelCard("New in your library", "What's arrived, and where it went", "drift", card) { opened = ChannelPage.Arrivals }
|
||||
}
|
||||
ChannelsDetail(busy, made ?: message, isOn, onSwitch, onSuggestions, listFirst, channelList, Modifier.weight(1f), onEdit = { c -> made = null; making = true to c })
|
||||
}
|
||||
}
|
||||
SettingsSection.OnScreen -> ViewingDetail(firstSwitch, detail) {
|
||||
Heading("Look")
|
||||
Text("Every screen takes the look you pick, straight away. The cast, the channels' idents and the games keep their own colours.", color = Muted, fontSize = 12.sp)
|
||||
LookPicker(firstSwitch, cardWidth = 150.dp)
|
||||
}
|
||||
SettingsSection.Family -> FamilyDetail(isOn(TOTS_NUMBER), windingDown, onWindDown, firstSwitch, detail)
|
||||
SettingsSection.Rooms -> RoomsDetail(firstSwitch, detail)
|
||||
SettingsSection.Account -> AccountDetail(server, titles, onSignOut, onRemoveServer, firstSwitch, detail)
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 780 B |
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.9 KiB |
Reference in New Issue
Block a user