Settings redesign: cards with focusable rows, segmented controls, gear button
- Settings page follows the Google TV pattern: Konto / App / Handy- Fernbedienung cards, each row a full-width focus target with label, subtitle and trailing value. - Quelle and Stream-Qualität are segmented controls switched with left/right inside the focused row (click cycles). - Account row with initial avatar, Abmelden as red row, update row with version badge; Update installieren only when one is available. - Top bar: drawn gear icon button replaces the text pill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "dev.castarr.tv"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 12
|
||||
versionName = "0.6.1"
|
||||
versionCode = 13
|
||||
versionName = "0.7.0"
|
||||
}
|
||||
|
||||
// Release signing from environment (see ~/.keys/castarr-release.env on the
|
||||
|
||||
@@ -11,11 +11,16 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -23,8 +28,12 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.tv.material3.Surface
|
||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||
import androidx.tv.material3.LocalContentColor
|
||||
import androidx.tv.material3.Text
|
||||
import dev.castarr.tv.AppState
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
@Composable
|
||||
fun CastarrApp(state: AppState) {
|
||||
@@ -123,36 +132,48 @@ private fun TopBar(state: AppState) {
|
||||
}
|
||||
Spacer(Modifier.width(18.dp))
|
||||
}
|
||||
TabItem(
|
||||
if (state.screen == AppState.Screen.LIVE) "Einstellungen" else "Zurück zu Live",
|
||||
selected = false,
|
||||
) {
|
||||
GearButton {
|
||||
state.screen = if (state.screen == AppState.Screen.LIVE) AppState.Screen.SETTINGS
|
||||
else AppState.Screen.LIVE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Round settings button with a drawn gear (glyphs render as emoji). */
|
||||
@Composable
|
||||
private fun TabItem(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
private fun GearButton(onClick: () -> Unit) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(999.dp)),
|
||||
modifier = Modifier.semantics { contentDescription = "Einstellungen" },
|
||||
shape = ClickableSurfaceDefaults.shape(CircleShape),
|
||||
colors = ClickableSurfaceDefaults.colors(
|
||||
// Visible resting surface, so the pill reads as a target even
|
||||
// without focus (design review P3).
|
||||
containerColor = if (selected) CastarrColors.accentDim else CastarrColors.surface,
|
||||
contentColor = if (selected) CastarrColors.accent else CastarrColors.muted,
|
||||
containerColor = CastarrColors.surface,
|
||||
contentColor = CastarrColors.muted,
|
||||
focusedContainerColor = CastarrColors.accent,
|
||||
focusedContentColor = CastarrColors.onAccent,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
modifier = Modifier.padding(horizontal = 18.dp, vertical = 8.dp),
|
||||
val color = LocalContentColor.current
|
||||
Box(Modifier.size(40.dp), contentAlignment = Alignment.Center) {
|
||||
Canvas(Modifier.size(17.dp)) {
|
||||
val stroke = 1.7.dp.toPx()
|
||||
val center = Offset(size.width / 2f, size.height / 2f)
|
||||
drawCircle(color, radius = size.minDimension * 0.17f, center = center, style = Stroke(stroke))
|
||||
val inner = size.minDimension * 0.30f
|
||||
val outer = size.minDimension * 0.48f
|
||||
for (i in 0 until 8) {
|
||||
val angle = i * (PI / 4)
|
||||
val dx = cos(angle).toFloat()
|
||||
val dy = sin(angle).toFloat()
|
||||
drawLine(
|
||||
color,
|
||||
start = center + Offset(dx * inner, dy * inner),
|
||||
end = center + Offset(dx * outer, dy * outer),
|
||||
strokeWidth = stroke,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
package dev.castarr.tv.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@@ -24,9 +29,17 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.tv.material3.Border
|
||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||
import androidx.tv.material3.Surface
|
||||
import androidx.tv.material3.Text
|
||||
@@ -38,8 +51,10 @@ import dev.castarr.tv.update.UpdateChecker
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Ten-Foot-Regel (#14): one screen, focusable buttons/chips only — no text
|
||||
* fields, no scrolling. Everything text-heavy lives in the Erweitert screen.
|
||||
* Google-TV-style settings: cards with full-width focusable rows, segmented
|
||||
* controls switched with left/right inside the row. Ten-Foot-Regel (#14):
|
||||
* one screen, no text fields, no scrolling — text-heavy input lives in the
|
||||
* Erweitert screen.
|
||||
*/
|
||||
@Composable
|
||||
fun SettingsScreen(state: AppState) {
|
||||
@@ -51,130 +66,312 @@ fun SettingsScreen(state: AppState) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 40.dp, vertical = 20.dp)
|
||||
.padding(horizontal = 40.dp, vertical = 12.dp)
|
||||
) {
|
||||
Column(Modifier.weight(1.2f)) {
|
||||
SectionTitle("Konto")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Column(Modifier.weight(1.25f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
SettingsCard("Konto") {
|
||||
if (state.auth.isLoggedIn) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(CastarrColors.accentDim),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
"Angemeldet als ${state.auth.username}",
|
||||
color = CastarrColors.muted, fontFamily = AppFont, fontSize = 14.sp,
|
||||
state.auth.username.take(1).uppercase().ifEmpty { "?" },
|
||||
color = CastarrColors.accent,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column {
|
||||
Text(
|
||||
state.auth.username,
|
||||
color = CastarrColors.fg, fontFamily = AppFont, fontSize = 15.sp,
|
||||
)
|
||||
Text(
|
||||
"Angemeldet über SSO",
|
||||
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 11.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (state.source.m3uUrl.isNotEmpty()) {
|
||||
Row {
|
||||
Chip("Dispatcharr", state.sourceMode == AppState.SourceMode.DISPATCHARR) {
|
||||
state.setMode(AppState.SourceMode.DISPATCHARR)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Chip("Eigene M3U", state.sourceMode == AppState.SourceMode.GENERIC) {
|
||||
state.setMode(AppState.SourceMode.GENERIC)
|
||||
val modes = listOf("Dispatcharr", "Eigene M3U")
|
||||
val selected = if (state.sourceMode == AppState.SourceMode.DISPATCHARR) 0 else 1
|
||||
SegmentedRow("Quelle", "Woher die Senderliste kommt", modes, selected) {
|
||||
state.setMode(
|
||||
if (it == 0) AppState.SourceMode.DISPATCHARR
|
||||
else AppState.SourceMode.GENERIC
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
val qualities = listOf("Standard", "Original") + profiles
|
||||
val qualityIndex = when (state.outputProfile) {
|
||||
"" -> 0
|
||||
"raw" -> 1
|
||||
else -> (profiles.indexOf(state.outputProfile) + 2).coerceAtLeast(0)
|
||||
}
|
||||
Text("Stream-Qualität", color = CastarrColors.faint, fontFamily = AppFont, fontSize = 12.sp)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Row {
|
||||
Chip("Standard", state.outputProfile.isEmpty()) { state.outputProfile = "" }
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Chip("Original", state.outputProfile == "raw") { state.outputProfile = "raw" }
|
||||
profiles.forEach { name ->
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Chip(name, state.outputProfile == name) { state.outputProfile = name }
|
||||
SegmentedRow(
|
||||
"Stream-Qualität", "Standard remuxt nur den Ton",
|
||||
qualities, qualityIndex,
|
||||
) {
|
||||
state.outputProfile = when (it) {
|
||||
0 -> ""
|
||||
1 -> "raw"
|
||||
else -> profiles.getOrElse(it - 2) { "" }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(18.dp))
|
||||
ActionButton("Abmelden", danger = true) { state.startOnboarding() }
|
||||
SettingRow("Abmelden", danger = true) { state.startOnboarding() }
|
||||
} else {
|
||||
Text(
|
||||
"Nicht angemeldet.",
|
||||
color = CastarrColors.muted, fontFamily = AppFont, fontSize = 14.sp,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
ActionButton("Jetzt anmelden") { state.startOnboarding() }
|
||||
SettingRow("Jetzt anmelden") { state.startOnboarding() }
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(28.dp))
|
||||
SectionTitle("App")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
SettingsCard("App") {
|
||||
SettingRow(
|
||||
"Version ${BuildConfig.VERSION_NAME}",
|
||||
subtitle = updateStatus.ifEmpty { "Klicken prüft auf Updates" },
|
||||
trailing = state.updateAvailable?.let { version ->
|
||||
{
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CastarrColors.accentDim)
|
||||
.padding(horizontal = 10.dp, vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
"Version ${BuildConfig.VERSION_NAME}" + (state.updateAvailable?.let { " · $it verfügbar" } ?: ""),
|
||||
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 12.sp,
|
||||
"$version verfügbar",
|
||||
color = CastarrColors.accent,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row {
|
||||
ActionButton(
|
||||
if (state.updateAvailable != null) "Update installieren" else "Nach Update suchen"
|
||||
}
|
||||
}
|
||||
},
|
||||
) {
|
||||
scope.launch {
|
||||
updateStatus = if (state.updateAvailable != null) {
|
||||
UpdateChecker.downloadAndInstall(context, state) ?: "Update wird geöffnet…"
|
||||
} else {
|
||||
when (val v = UpdateChecker.check(state)) {
|
||||
updateStatus = when (val v = UpdateChecker.check(state)) {
|
||||
null -> "App ist aktuell"
|
||||
else -> "$v verfügbar — nochmal drücken zum Installieren"
|
||||
else -> "$v verfügbar"
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.updateAvailable != null) {
|
||||
SettingRow("Update installieren") {
|
||||
scope.launch {
|
||||
updateStatus =
|
||||
UpdateChecker.downloadAndInstall(context, state)
|
||||
?: "Update wird geöffnet…"
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
ActionButton("Erweitert") { state.screen = AppState.Screen.ADVANCED }
|
||||
}
|
||||
if (updateStatus.isNotEmpty()) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(updateStatus, color = CastarrColors.muted, fontFamily = AppFont, fontSize = 12.sp)
|
||||
}
|
||||
SettingRow(
|
||||
"Erweitert",
|
||||
subtitle = "M3U/EPG-Adressen von Hand eintragen",
|
||||
) { state.screen = AppState.Screen.ADVANCED }
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.width(40.dp))
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
SectionTitle("Handy-Fernbedienung")
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Column(Modifier.weight(0.75f)) {
|
||||
SettingsCard("Handy-Fernbedienung") {
|
||||
val address = remember { Pairing.lanAddress() }
|
||||
if (address != null) {
|
||||
val qr = remember(address) {
|
||||
Qr.encode(Pairing.pairingUrl(context, address), 400, android.graphics.Color.parseColor("#101216"))
|
||||
Qr.encode(
|
||||
Pairing.pairingUrl(context, address), 400,
|
||||
android.graphics.Color.parseColor("#101216"),
|
||||
)
|
||||
}
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
QrCard(qr)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
"${Pairing.remoteUrl(address)} · Code ${Pairing.code(context)}",
|
||||
"Mit der Handy-Kamera scannen",
|
||||
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 12.sp,
|
||||
)
|
||||
} else {
|
||||
Text("Keine Netzwerkverbindung", color = CastarrColors.faint, fontFamily = AppFont, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionTitle(text: String) {
|
||||
Text(text, color = CastarrColors.fg, fontFamily = AppFont, fontSize = 20.sp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Chip(label: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(999.dp)),
|
||||
colors = ClickableSurfaceDefaults.colors(
|
||||
containerColor = if (selected) CastarrColors.accentDim else CastarrColors.surface,
|
||||
contentColor = if (selected) CastarrColors.accent else CastarrColors.muted,
|
||||
focusedContainerColor = CastarrColors.accent,
|
||||
focusedContentColor = CastarrColors.onAccent,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
label,
|
||||
fontFamily = AppFont, fontSize = 12.sp,
|
||||
"${Pairing.remoteUrl(address)} · Code ${Pairing.code(context)}",
|
||||
color = CastarrColors.muted, fontFamily = AppFont, fontSize = 12.sp,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
"Keine Netzwerkverbindung",
|
||||
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 13.sp,
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsCard(title: String, content: @Composable () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(CastarrColors.surface)
|
||||
.padding(horizontal = 10.dp, vertical = 14.dp)
|
||||
) {
|
||||
Text(
|
||||
title.uppercase(),
|
||||
color = CastarrColors.faint,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
letterSpacing = 2.sp,
|
||||
modifier = Modifier.padding(start = 14.dp, bottom = 8.dp),
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
private val rowShape = RoundedCornerShape(10.dp)
|
||||
|
||||
/** One full-width focusable settings row: label + subtitle, value right. */
|
||||
@Composable
|
||||
private fun SettingRow(
|
||||
label: String,
|
||||
subtitle: String? = null,
|
||||
danger: Boolean = false,
|
||||
trailing: (@Composable () -> Unit)? = null,
|
||||
keyHandler: ((Key) -> Boolean)? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.let { m ->
|
||||
if (keyHandler == null) m
|
||||
else m.onPreviewKeyEvent { event ->
|
||||
event.type == KeyEventType.KeyDown && keyHandler(event.key)
|
||||
}
|
||||
},
|
||||
shape = ClickableSurfaceDefaults.shape(rowShape),
|
||||
colors = ClickableSurfaceDefaults.colors(
|
||||
containerColor = androidx.compose.ui.graphics.Color.Transparent,
|
||||
contentColor = CastarrColors.fg,
|
||||
focusedContainerColor = CastarrColors.surfaceFocused,
|
||||
focusedContentColor = CastarrColors.fg,
|
||||
),
|
||||
border = ClickableSurfaceDefaults.border(
|
||||
focusedBorder = Border(
|
||||
border = BorderStroke(2.dp, if (danger) CastarrColors.live else CastarrColors.accent),
|
||||
shape = rowShape,
|
||||
),
|
||||
),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(
|
||||
label,
|
||||
color = if (danger) CastarrColors.live else CastarrColors.fg,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 15.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
subtitle,
|
||||
color = CastarrColors.faint,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 11.sp,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (trailing != null) {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
trailing()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Row whose value is a segmented control; left/right switches in place. */
|
||||
@Composable
|
||||
private fun SegmentedRow(
|
||||
label: String,
|
||||
subtitle: String,
|
||||
options: List<String>,
|
||||
selected: Int,
|
||||
onChange: (Int) -> Unit,
|
||||
) {
|
||||
SettingRow(
|
||||
label = label,
|
||||
subtitle = subtitle,
|
||||
trailing = {
|
||||
Row(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(CastarrColors.bg)
|
||||
.border(1.dp, CastarrColors.line, RoundedCornerShape(999.dp))
|
||||
.padding(3.dp)
|
||||
) {
|
||||
options.forEachIndexed { index, option ->
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(999.dp))
|
||||
.background(
|
||||
if (index == selected) CastarrColors.accent
|
||||
else androidx.compose.ui.graphics.Color.Transparent
|
||||
)
|
||||
.padding(horizontal = 12.dp, vertical = 5.dp)
|
||||
) {
|
||||
Text(
|
||||
option,
|
||||
color = if (index == selected) CastarrColors.onAccent
|
||||
else CastarrColors.muted,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = if (index == selected) FontWeight.SemiBold
|
||||
else FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
keyHandler = { key ->
|
||||
when {
|
||||
key == Key.DirectionLeft && selected > 0 -> {
|
||||
onChange(selected - 1); true
|
||||
}
|
||||
key == Key.DirectionRight && selected < options.lastIndex -> {
|
||||
onChange(selected + 1); true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
},
|
||||
) { onChange((selected + 1) % options.size) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(label: String, danger: Boolean = false, onClick: () -> Unit) {
|
||||
|
||||
Reference in New Issue
Block a user