From 0f2582f6380637d978633cd1f8db8a0196156917 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Sat, 24 Oct 2026 14:49:54 -0700 Subject: [PATCH] phone: each car settings group opens its own screen, under a bigger Car heading --- .../seb3thehacker/gearslip/ui/GearslipApp.kt | 6 +- .../gearslip/ui/HomeCarSettings.kt | 191 ++++++++++-------- .../seb3thehacker/gearslip/ui/HomeScreen.kt | 3 +- 3 files changed, 118 insertions(+), 82 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt index 070d7ea..8cee197 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt @@ -37,7 +37,7 @@ import app.seb3thehacker.gearslip.BuildConfig import app.seb3thehacker.gearslip.CertProvider import app.seb3thehacker.gearslip.stats.UsageStats -private enum class Screen { INTRO, HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, USAGE_NOTES, HELP, WHATS_NEW, RECOMMENDED_APPS } +private enum class Screen { INTRO, HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, USAGE_NOTES, HELP, WHATS_NEW, RECOMMENDED_APPS, CAR_GROUP } /** Three screens and a back stack of depth one: no navigation library needed. */ @Composable @@ -64,6 +64,8 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { var settingsAtUsageNotes by rememberSaveable { mutableStateOf(false) } // What's new and Connection help open from Home and from Settings; back returns to whichever. var backTo by rememberSaveable { mutableStateOf(Screen.SETTINGS) } + // Which of the car's setting groups Home opened. + var carGroup by rememberSaveable { mutableStateOf(CarGroup.SCREEN) } BackHandler(enabled = screen != Screen.HOME && screen != Screen.INTRO) { screen = when (screen) { Screen.RECOMMENDED_APPS, Screen.LOGS, Screen.CAR_PREVIEW -> Screen.SETTINGS @@ -113,7 +115,9 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { onOpenCarPreview = { backTo = Screen.HOME; screen = Screen.CAR_PREVIEW }, onOpenLogs = { backTo = Screen.HOME; screen = Screen.LOGS }, onDisconnect = onDisconnect, + onOpenCarGroup = { carGroup = it; screen = Screen.CAR_GROUP }, ) + Screen.CAR_GROUP -> CarGroupScreen(carGroup, onBack = { screen = Screen.HOME }) Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = backTo }) Screen.LOGS -> LogsScreen(onBack = { screen = backTo }) Screen.SETTINGS -> SettingsScreen( diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt index c0eab65..6b2b825 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt @@ -1,7 +1,5 @@ package app.seb3thehacker.gearslip.ui -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -10,7 +8,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -21,12 +22,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.rotate import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.seb3thehacker.gearslip.car.AppTheme @@ -37,13 +36,109 @@ import app.seb3thehacker.gearslip.car.NightMode import app.seb3thehacker.gearslip.car.Units import app.seb3thehacker.gearslip.car.theme.GsThemes +/** The car's setting groups, each opening its own screen from the phone's home screen. */ +enum class CarGroup(val title: String) { + SCREEN("Screen"), + NAV_BAR("Nav bar and player"), + SOUND("Sound and voice"), + WARNINGS("Warnings"), +} + +private val sizeChoices = listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large") +private val themeChoices = listOf(AppTheme.PHONE to "Follow phone", AppTheme.LIGHT to "Light", AppTheme.DARK to "Dark") +private val nightChoices = listOf(NightMode.AUTO to "Auto", NightMode.DAY to "Day", NightMode.NIGHT to "Night") +private val unitChoices = listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial", Units.METRIC to "Metric") +private val navBarChoices = listOf( + NavBarMode.SHOW to "Always show", + NavBarMode.HIDE_NAVIGATING to "Hide while navigating", + NavBarMode.HIDE to "Hide until tapped", +) +private fun openChoices() = listOf(CarSettings.HOME to "Home") + CarApps.all.filter { it.id != "settings" }.map { it.id to it.label } + /** - * The car's settings on the phone's home screen, one collapsible card per group. They are the - * same settings as the car's own Settings app, so a change in either shows in both; the phone is - * just the easier place to make them when parked. + * The car's settings on the phone's home screen: one card per group with a summary of where it + * stands, each opening a screen of its own. They are the same settings as the car's own Settings + * app, so a change in either shows in both; the phone is just the easier place when parked. */ @Composable -internal fun HomeCarSettings(modifier: Modifier = Modifier) { +internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Modifier) { + val scale by CarSettings.scale.collectAsState() + val open by CarSettings.openOnConnect.collectAsState() + val navBar by CarSettings.navBarMode.collectAsState() + val pipeAudio by CarSettings.pipeAudio.collectAsState() + val speakDirections by CarSettings.speakDirections.collectAsState() + val bluetoothWarning by CarSettings.bluetoothWarning.collectAsState() + val locationWarning by CarSettings.locationWarning.collectAsState() + val playerLeft by CarSettings.playerLeft.collectAsState() + val theme by CarSettings.appTheme.collectAsState() + + fun summary(group: CarGroup) = when (group) { + CarGroup.SCREEN -> listOf(openChoices().label(open), sizeChoices.label(scale), themeChoices.label(theme)) + CarGroup.NAV_BAR -> listOf(navBarChoices.label(navBar), "Player on the ${if (playerLeft) "left" else "right"}") + CarGroup.SOUND -> listOf(if (pipeAudio) "USB audio" else "Bluetooth audio", "Directions ${if (speakDirections) "on" else "off"}") + CarGroup.WARNINGS -> listOf( + "Bluetooth ${if (bluetoothWarning && !pipeAudio) "on" else "off"}", + "Location ${if (locationWarning) "on" else "off"}", + ) + }.filter { it.isNotEmpty() }.joinToString(" · ") + + Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text( + "Car", + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(start = 4.dp), + ) + SettingsCard { + CarGroup.entries.forEachIndexed { i, group -> + if (i > 0) SettingsDivider() + SettingsRow( + group.title, + Modifier.clickable { onOpen(group) }, + subtitle = summary(group), + trailing = { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null) }, + ) + } + } + } +} + +/** One group of the car's settings, on a screen of its own. */ +@OptIn(androidx.compose.material3.ExperimentalMaterial3Api::class) +@Composable +internal fun CarGroupScreen(group: CarGroup, onBack: () -> Unit) { + androidx.compose.material3.Scaffold( + containerColor = MaterialTheme.colorScheme.background, + topBar = { + androidx.compose.material3.TopAppBar( + title = { Text(group.title) }, + navigationIcon = { + androidx.compose.material3.IconButton(onClick = onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + }, + ) + }, + ) { padding -> + Column( + Modifier.fillMaxSize().padding(padding).padding(horizontal = 24.dp) + .verticalScroll(androidx.compose.foundation.rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + SettingsCard { GroupRows(group) } + Text( + "These are the same settings as Settings in the car. A change here shows there too.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } +} + +@Composable +private fun GroupRows(group: CarGroup) { val scale by CarSettings.scale.collectAsState() val open by CarSettings.openOnConnect.collectAsState() val night by CarSettings.nightMode.collectAsState() @@ -60,45 +155,21 @@ internal fun HomeCarSettings(modifier: Modifier = Modifier) { val theme by CarSettings.appTheme.collectAsState() val uiTheme by CarSettings.uiTheme.collectAsState() - val openChoices = listOf(CarSettings.HOME to "Home") + CarApps.all.filter { it.id != "settings" }.map { it.id to it.label } - val sizeChoices = listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large") - val themeChoices = listOf(AppTheme.PHONE to "Follow phone", AppTheme.LIGHT to "Light", AppTheme.DARK to "Dark") - val lookChoices = GsThemes.builtIn.map { it.id to it.name } - val nightChoices = listOf(NightMode.AUTO to "Auto", NightMode.DAY to "Day", NightMode.NIGHT to "Night") - val unitChoices = listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial", Units.METRIC to "Metric") - val navBarChoices = listOf( - NavBarMode.SHOW to "Always show", - NavBarMode.HIDE_NAVIGATING to "Hide while navigating", - NavBarMode.HIDE to "Hide until tapped", - ) - - Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { - Text( - "Car", - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 16.dp), - ) - Group( - "Screen", - summary = listOf(openChoices.label(open), sizeChoices.label(scale), themeChoices.label(theme)), - ) { - Choice("Open on connect", openChoices, open, CarSettings::setOpenOnConnect) + when (group) { + CarGroup.SCREEN -> { + Choice("Open on connect", openChoices(), open, CarSettings::setOpenOnConnect) SettingsDivider() Choice("Size", sizeChoices, scale, CarSettings::setScale) SettingsDivider() Choice("App theme", themeChoices, theme, CarSettings::setAppTheme) SettingsDivider() - Choice("Buttons", lookChoices, uiTheme, CarSettings::setUiTheme) + Choice("Buttons", GsThemes.builtIn.map { it.id to it.name }, uiTheme, CarSettings::setUiTheme) SettingsDivider() Choice("Map day/night", nightChoices, night, CarSettings::setNightMode) SettingsDivider() Choice("Units", unitChoices, units, CarSettings::setUnits) } - Group( - "Nav bar and player", - summary = listOf(navBarChoices.label(navBar), "Player on the ${if (playerLeft) "left" else "right"}"), - ) { + CarGroup.NAV_BAR -> { Choice("Nav bar", navBarChoices, navBar, CarSettings::setNavBarMode) SettingsDivider() Choice("Player in the nav bar", listOf(false to "Skip buttons", true to "Song title"), playerTitle, CarSettings::setPlayerTitle) @@ -107,10 +178,7 @@ internal fun HomeCarSettings(modifier: Modifier = Modifier) { SettingsDivider() Toggle("Autoplay on connect", autoplay, CarSettings::setAutoplay) } - Group( - "Sound and voice", - summary = listOf(if (pipeAudio) "USB audio" else "Bluetooth audio", "Directions ${if (speakDirections) "on" else "off"}"), - ) { + CarGroup.SOUND -> { Choice("Car audio", listOf(false to "Bluetooth", true to "USB"), pipeAudio, CarSettings::setPipeAudio) SettingsDivider() Toggle( @@ -127,13 +195,7 @@ internal fun HomeCarSettings(modifier: Modifier = Modifier) { note = "On most phones that's Google's, which may send your voice to Google.", ) } - Group( - "Warnings", - summary = listOf( - "Bluetooth ${if (bluetoothWarning && !pipeAudio) "on" else "off"}", - "Location ${if (locationWarning) "on" else "off"}", - ), - ) { + CarGroup.WARNINGS -> { if (!pipeAudio) { Toggle("Warn when Bluetooth isn't connected", bluetoothWarning, CarSettings::setBluetoothWarning) SettingsDivider() @@ -145,37 +207,6 @@ internal fun HomeCarSettings(modifier: Modifier = Modifier) { private fun List>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty() -/** One group: its name and a one-line summary, opening in place to the settings themselves. */ -@Composable -private fun Group(title: String, summary: List, content: @Composable () -> Unit) { - var open by rememberSaveable(title) { mutableStateOf(false) } - val turn by animateFloatAsState(if (open) 180f else 0f, label = "chevron") - SettingsCard { - Row( - Modifier.fillMaxWidth().clickable { open = !open }.padding(horizontal = 16.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Column(Modifier.weight(1f)) { - Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) - if (!open) { - Text( - summary.filter { it.isNotEmpty() }.joinToString(" · "), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - Icon(Icons.Filled.KeyboardArrowDown, contentDescription = if (open) "Close" else "Open", modifier = Modifier.rotate(turn)) - } - AnimatedVisibility(open) { - Column { - SettingsDivider() - content() - } - } - } -} - /** A setting with a few named choices: its current one shown, the rest a tap away. */ @Composable private fun Choice(title: String, choices: List>, selected: T, onSelect: (T) -> Unit) { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt index 6573a5d..dd17d9f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt @@ -68,6 +68,7 @@ fun HomeScreen( onOpenCarPreview: () -> Unit, onOpenLogs: () -> Unit, onDisconnect: () -> Unit, + onOpenCarGroup: (CarGroup) -> Unit, ) { val status by SessionStatus.state.collectAsStateWithLifecycle() val context = LocalContext.current @@ -126,7 +127,7 @@ fun HomeScreen( Spacer(Modifier.height(16.dp)) UpdateCard() Spacer(Modifier.height(24.dp)) - HomeCarSettings() + HomeCarSettings(onOpenCarGroup) Spacer(Modifier.height(16.dp)) }