redesign the phone app's settings

One look for every section, What's new and the setup guide at the top, and the setup guide's buttons stay on screen. Recommended apps is hidden for now.
This commit is contained in:
Seb3thehacker committed 2026-10-05 01:21:16 -07:00
1 parent 5a3bbe82a0
commit cf0c324d80
6 files changed
+325 -133

No files matched your search

@@ -86,7 +86,7 @@ private fun setupSteps(onRequestCallScreening: () -> Unit) = listOf(
* here too so it can be reread without repeating the guide itself. */
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit, onReplayTutorial: () -> Unit) {
fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit) {
val steps = remember(onRequestCallScreening) { setupSteps(onRequestCallScreening) }
Scaffold(
@@ -110,10 +110,7 @@ fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit, onReplayT
.verticalScroll(rememberScrollState()),
) {
Spacer(Modifier.height(8.dp))
FilledTonalButton(onClick = onReplayTutorial, modifier = Modifier.fillMaxWidth().height(52.dp)) {
Text("Replay the guided tutorial")
}
Spacer(Modifier.height(24.dp))
Spacer(Modifier.height(16.dp))
Column(verticalArrangement = Arrangement.spacedBy(18.dp)) {
steps.forEachIndexed { index, step ->
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.List
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings
@@ -49,7 +48,6 @@ fun HomeScreen(
onOpenSettings: () -> Unit,
onOpenCarPreview: () -> Unit,
onOpenHelp: () -> Unit,
onOpenWhatsNew: () -> Unit,
onDisconnect: () -> Unit,
) {
val status by SessionStatus.state.collectAsStateWithLifecycle()
@@ -115,8 +113,6 @@ fun HomeScreen(
ActionRow(Icons.Filled.List, "Live logs", onOpenLogs)
RowDivider()
ActionRow(Icons.Filled.Info, "Connection help", onOpenHelp)
RowDivider()
ActionRow(Icons.Filled.Star, "What's new", onOpenWhatsNew)
if (BuildConfig.DEBUG) {
val context = LocalContext.current
RowDivider()
@@ -92,6 +92,9 @@ fun RecommendedAppsScreen(onBack: () -> Unit) {
}
}
/** Hidden for now: off, neither Settings nor the setup guide shows the list. The code stays. */
internal const val SHOW_RECOMMENDED_APPS = false
internal const val RECOMMENDED_INTRO =
"These free apps run on Gearslip's car screen. Each button opens the app's page on GitHub."
@@ -0,0 +1,116 @@
package app.seb3thehacker.gearslip.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
/*
* The pieces every phone settings screen is built from, so they all share one shape, colour and
* spacing: a section label, a card holding rows, and the rows themselves, with a footer note
* under the card when a section needs one.
*/
/** A titled group: label above, rows in one card, an optional note below. */
@Composable
internal fun SettingsSection(
title: String? = null,
modifier: Modifier = Modifier,
header: AnnotatedString? = null,
footer: String? = null,
content: @Composable ColumnScope.() -> Unit,
) {
Column(modifier.fillMaxWidth()) {
title?.let {
Text(
it,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp),
)
}
header?.let {
Text(
it,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
)
}
SettingsCard(content = content)
footer?.let {
Text(
it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
)
}
}
}
/** The card behind a group of rows; also used on its own, as in the setup guide. */
@Composable
internal fun SettingsCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge,
modifier = modifier.fillMaxWidth(),
) {
Column(content = content)
}
}
/**
* One row: an optional icon, a title with an optional line under it, and whatever sits at the
* end (a switch, a checkbox, an arrow). [modifier] carries the click or toggle, so the whole
* row is the touch target.
*/
@Composable
internal fun SettingsRow(
title: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
icon: ImageVector? = null,
tint: Color = MaterialTheme.colorScheme.primary,
titleColor: Color = MaterialTheme.colorScheme.onSurface,
trailing: (@Composable () -> Unit)? = null,
) {
Row(
modifier.fillMaxWidth().heightIn(min = 56.dp).padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
if (icon != null) Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp))
Column(Modifier.weight(1f)) {
Text(title, style = MaterialTheme.typography.bodyLarge, color = titleColor)
subtitle?.let {
Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
trailing?.let { Box { it() } }
}
}
/** A thin line between rows in one card. */
@Composable
internal fun SettingsDivider() {
HorizontalDivider(Modifier.padding(horizontal = 16.dp), color = MaterialTheme.colorScheme.outlineVariant)
}
@@ -3,30 +3,37 @@ package app.seb3thehacker.gearslip.ui
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.rememberScrollState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -39,22 +46,28 @@ import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.unit.dp
import app.seb3thehacker.gearslip.AppSettings
import app.seb3thehacker.gearslip.BuildConfig
import app.seb3thehacker.gearslip.CertProvider
import app.seb3thehacker.gearslip.stats.UsageStats
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onBack: () -> Unit, onOpenRecommendedApps: () -> Unit) {
fun SettingsScreen(
onBack: () -> Unit,
onOpenRecommendedApps: () -> Unit,
onOpenWhatsNew: () -> Unit,
onReplayTutorial: () -> Unit,
) {
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
topBar = {
@@ -74,59 +87,78 @@ fun SettingsScreen(onBack: () -> Unit, onOpenRecommendedApps: () -> Unit) {
.padding(padding)
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(32.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Spacer(Modifier.height(0.dp))
GearslipEnabledSection()
AppsSection(onOpenRecommendedApps)
AboutSection(onOpenWhatsNew, onReplayTutorial)
ConnectionSection()
if (SHOW_RECOMMENDED_APPS) AppsSection(onOpenRecommendedApps)
CertificateSection()
if (UsageStats.choosable) {
SettingsSection("Usage notes", header = USAGE_STATS_INTRO) { UsageStatsRows(showNote = true) }
}
Spacer(Modifier.height(16.dp))
}
}
}
@Composable
private fun SectionTitle(text: String) {
Text(
text,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary,
)
private fun Arrow() = Icon(
Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
/** What's new and the setup guide, one tap from the top. */
@Composable
private fun AboutSection(onOpenWhatsNew: () -> Unit, onReplayTutorial: () -> Unit) {
SettingsSection {
SettingsRow(
"What's new",
Modifier.clickable(onClick = onOpenWhatsNew),
subtitle = "Version ${BuildConfig.VERSION_NAME}",
icon = Icons.Filled.Star,
trailing = { Arrow() },
)
SettingsDivider()
SettingsRow(
"Replay the setup guide",
Modifier.clickable(onClick = onReplayTutorial),
subtitle = "Go through the first-run steps again",
icon = Icons.Filled.Refresh,
trailing = { Arrow() },
)
}
}
/** Off, a stock Android Auto app or head unit answers the USB connection instead of Gearslip. */
@Composable
private fun GearslipEnabledSection() {
private fun ConnectionSection() {
val context = LocalContext.current
var enabled by remember { mutableStateOf(AppSettings.gearslipEnabled(context)) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle("Gearslip")
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Text("Enabled", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
Switch(checked = enabled, onCheckedChange = {
SettingsSection("Connection") {
SettingsRow(
"Use Gearslip in the car",
Modifier.toggleable(enabled, role = Role.Switch) {
enabled = it
AppSettings.setGearslipEnabled(context, it)
})
}
if (!enabled) {
Text(
"Off - stock Android Auto can take the USB connection instead.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
subtitle = if (enabled) "Gearslip answers when you plug into a car" else "Off: Android Auto can take the connection instead",
icon = Icons.Filled.Settings,
trailing = { Switch(checked = enabled, onCheckedChange = null) },
)
}
}
@Composable
private fun AppsSection(onOpenRecommendedApps: () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
SectionTitle("Apps")
OutlinedButton(onClick = onOpenRecommendedApps, modifier = Modifier.fillMaxWidth().height(52.dp)) {
Text("Recommended apps")
}
SettingsSection("Apps") {
SettingsRow(
"Recommended apps",
Modifier.clickable(onClick = onOpenRecommendedApps),
icon = Icons.Filled.Star,
trailing = { Arrow() },
)
}
}
@@ -142,37 +174,49 @@ private fun CertificateSection() {
var pendingUri by remember { mutableStateOf<Uri?>(null) }
var notice by remember { mutableStateOf<String?>(null) }
var downloading by remember { mutableStateOf(false) }
var detailsOpen by remember { mutableStateOf(false) }
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) pendingUri = uri
}
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
SectionTitle("Certificate")
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.large) {
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
val s = summary
if (s == null) {
Text("Checking…", style = MaterialTheme.typography.bodyMedium)
} else {
Text(s.headline, style = MaterialTheme.typography.bodyLarge)
s.subject?.let { Detail("Subject", it) }
s.issuer?.let { Detail("Issuer", it) }
s.validUntil?.let { Detail("Valid until", it) }
SettingsSection("Certificate", footer = notice ?: "Changes take effect on the next connection.") {
val s = summary
val loaded = s != null && s.kind != CertProvider.Kind.SELF_SIGNED
val hasDetails = s?.subject != null || s?.issuer != null
SettingsRow(
s?.headline ?: "Checking…",
if (hasDetails) Modifier.clickable { detailsOpen = !detailsOpen } else Modifier,
subtitle = s?.validUntil?.let { "Valid until $it" },
icon = if (s == null || loaded) Icons.Filled.Lock else Icons.Filled.Warning,
tint = if (s == null || loaded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
trailing = if (hasDetails) {
{
Icon(
if (detailsOpen) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
contentDescription = if (detailsOpen) "Hide details" else "Show details",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else null,
)
AnimatedVisibility(detailsOpen && hasDetails) {
Column(
Modifier.padding(start = 56.dp, end = 16.dp, bottom = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
s?.subject?.let { Detail("Subject", it) }
s?.issuer?.let { Detail("Issuer", it) }
}
}
Button(onClick = { picker.launch(arrayOf("*/*")) }, modifier = Modifier.fillMaxWidth().height(52.dp)) {
Text("Import certificate (.p12)")
}
FilledTonalButton(
onClick = {
SettingsDivider()
SettingsRow(
if (downloading) "Downloading…" else "Download certificate",
Modifier.clickable(enabled = !downloading) {
downloading = true
scope.launch {
val result = withContext(Dispatchers.IO) {
runCatching { CertProvider.downloadAndImport(context) }
}
val result = withContext(Dispatchers.IO) { runCatching { CertProvider.downloadAndImport(context) } }
downloading = false
result.onSuccess {
notice = "Certificate downloaded."
refresh++
@@ -181,39 +225,34 @@ private fun CertificateSection() {
}
}
},
modifier = Modifier.fillMaxWidth().height(52.dp),
) {
Text("Download certificate")
}
if (summary?.kind == CertProvider.Kind.IMPORTED) {
OutlinedButton(
onClick = {
CertProvider.removeImported(context)
notice = "Removed the imported certificate."
refresh++
},
modifier = Modifier.fillMaxWidth().height(52.dp),
) { Text("Remove imported certificate") }
}
if (summary?.kind == CertProvider.Kind.DOWNLOADED) {
OutlinedButton(
onClick = {
CertProvider.removeDownloaded(context)
notice = "Removed the downloaded certificate."
refresh++
},
modifier = Modifier.fillMaxWidth().height(52.dp),
) { Text("Remove downloaded certificate") }
}
notice?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) }
Text(
"Changes take effect on the next connection.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
icon = Icons.Filled.Refresh,
)
SettingsDivider()
SettingsRow(
"Import a certificate",
Modifier.clickable { picker.launch(arrayOf("*/*")) },
subtitle = "A .p12 file with its private key",
icon = Icons.Filled.Add,
)
val removable = when (s?.kind) {
CertProvider.Kind.IMPORTED -> "Remove imported certificate"
CertProvider.Kind.DOWNLOADED -> "Remove downloaded certificate"
else -> null
}
if (removable != null) {
SettingsDivider()
SettingsRow(
removable,
Modifier.clickable {
if (s?.kind == CertProvider.Kind.IMPORTED) CertProvider.removeImported(context) else CertProvider.removeDownloaded(context)
notice = if (s?.kind == CertProvider.Kind.IMPORTED) "Removed the imported certificate." else "Removed the downloaded certificate."
refresh++
},
icon = Icons.Filled.Delete,
tint = MaterialTheme.colorScheme.error,
titleColor = MaterialTheme.colorScheme.error,
)
}
}
pendingUri?.let { uri ->
@@ -1,5 +1,10 @@
package app.seb3thehacker.gearslip.ui
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.key
import app.seb3thehacker.gearslip.BuildConfig
import android.Manifest
import android.app.AppOpsManager
@@ -40,6 +45,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import app.seb3thehacker.gearslip.AppSettings
@@ -47,6 +53,8 @@ import app.seb3thehacker.gearslip.call.CarCalls
import app.seb3thehacker.gearslip.car.Prefetch
import app.seb3thehacker.gearslip.host.AndroidAuto
import app.seb3thehacker.gearslip.notify.GearslipNotificationListener
import app.seb3thehacker.gearslip.stats.UsageStats
import androidx.compose.runtime.DisposableEffect
/**
* One page of the guide. [permissions] drives a system permission dialog; [command] is an adb
@@ -56,7 +64,8 @@ import app.seb3thehacker.gearslip.notify.GearslipNotificationListener
*/
private class GuidePage(
val title: String,
val explanation: String,
/** Plain text, or an [AnnotatedString] when part of it needs to stand out. */
val explanation: CharSequence,
val permissions: Array<String>? = null,
val command: String? = null,
val actionLabel: String? = null,
@@ -265,14 +274,34 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
)
}
// --- Last: apps worth installing, once everything above is set up ---------------
add(
GuidePage(
title = "Recommended apps",
explanation = RECOMMENDED_INTRO,
content = { RecommendedAppsList() },
),
)
// --- Apps worth installing, once everything above is set up -----------------------
if (SHOW_RECOMMENDED_APPS) {
add(
GuidePage(
title = "Recommended apps",
explanation = RECOMMENDED_INTRO,
content = { RecommendedAppsList() },
),
)
}
// --- The very end, every time: usage notes. The first time through, leaving the page
// without turning them on is a no; on a replay, the switch shows what was chosen before.
// Dev builds always send, so they skip the page.
if (UsageStats.choosable) {
add(
GuidePage(
title = USAGE_STATS_TITLE,
explanation = USAGE_STATS_INTRO,
content = {
UsageStatsChoices()
DisposableEffect(Unit) {
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
}
},
),
)
}
}
/**
@@ -319,8 +348,8 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
.padding(padding)
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Spacer(Modifier.height(24.dp))
LinearProgressIndicator(
progress = { (index + 1f) / pages.size },
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
@@ -332,28 +361,39 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(48.dp))
Surface(color = MaterialTheme.colorScheme.surfaceContainerLow, shape = MaterialTheme.shapes.extraLarge) {
Column(Modifier.fillMaxWidth().padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
page.title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
page.explanation,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
page.command?.let { CopyableCommand(it) }
page.content?.invoke()
// The page scrolls between the progress bar and the buttons, so a tall page (the usage
// note's choices, say) never pushes Next off the screen. Short pages stay centred.
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
key(index) {
Column(
Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.heightIn(min = maxHeight)
.padding(vertical = 24.dp),
verticalArrangement = Arrangement.Center,
) {
Surface(color = MaterialTheme.colorScheme.surfaceContainerLow, shape = MaterialTheme.shapes.extraLarge) {
Column(Modifier.fillMaxWidth().padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
page.title,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
page.explanation as? AnnotatedString ?: AnnotatedString(page.explanation.toString()),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
page.command?.let { CopyableCommand(it) }
page.content?.invoke()
}
}
}
}
}
Spacer(Modifier.height(32.dp))
when {
page.permissions != null && alreadyGranted -> {
Text(
@@ -391,6 +431,7 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
}
}
}
Spacer(Modifier.height(24.dp))
}
}
}