phone: car settings all live under Home's car groups, with voice models, car audio and screen size moved in and a new More group

This commit is contained in:
Seb3thehacker committed 2026-10-26 01:18:35 -07:00
1 parent 1f9ef552c1
commit 15d6721705
2 files changed
+89 -29

No files matched your search

@@ -2,6 +2,7 @@ package app.seb3thehacker.gearslip.ui
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -43,6 +44,7 @@ enum class CarGroup(val title: String) {
NAV_BAR("Nav bar and player"),
SOUND("Sound and voice"),
WARNINGS("Warnings"),
MORE("More"),
}
private val sizeChoices = listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large")
@@ -78,6 +80,7 @@ internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Mo
val locationWarning by CarSettings.locationWarning.collectAsState()
val playerLeft by CarSettings.playerLeft.collectAsState()
val theme by CarSettings.appTheme.collectAsState()
val experimental by CarSettings.experimentalFeaturesEnabled.collectAsState()
fun summary(group: CarGroup) = when (group) {
CarGroup.SCREEN -> listOf(openChoices().label(open), sizeChoices.label(scale), themeChoices.label(theme))
@@ -87,6 +90,7 @@ internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Mo
"Bluetooth ${if (bluetoothWarning && !pipeAudio) "on" else "off"}",
"Location ${if (locationWarning) "on" else "off"}",
)
CarGroup.MORE -> listOf("Web page", "Experimental features ${if (experimental) "shown" else "hidden"}")
}.filter { it.isNotEmpty() }.joinToString(" · ")
Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
@@ -134,6 +138,16 @@ internal fun CarGroupScreen(group: CarGroup, onBack: () -> Unit) {
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
SettingsCard { GroupRows(group) }
// The bigger pieces each keep their own card: the car screen's size and the preview's,
// and the two voice models with their downloads.
when (group) {
CarGroup.SCREEN -> CarScreenSection()
CarGroup.SOUND -> {
CarAudioSection()
VoiceModelSection()
}
else -> Unit
}
Text(
"These are the same settings as Settings in the car. A change here shows there too.",
style = MaterialTheme.typography.bodySmall,
@@ -163,6 +177,9 @@ private fun GroupRows(group: CarGroup) {
val theme by CarSettings.appTheme.collectAsState()
val uiTheme by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
val experimental by CarSettings.experimentalFeaturesEnabled.collectAsState()
val carSensors by CarSettings.carSensorsEnabled.collectAsState()
val adaptiveVideo by CarSettings.adaptiveVideoEnabled.collectAsState()
when (group) {
CarGroup.SCREEN -> {
@@ -199,8 +216,6 @@ private fun GroupRows(group: CarGroup) {
)
}
CarGroup.SOUND -> {
Choice("Car audio", listOf(false to "Bluetooth", true to "USB"), pipeAudio, CarSettings::setPipeAudio)
SettingsDivider()
Toggle(
"Spoken directions",
speakDirections,
@@ -222,6 +237,27 @@ private fun GroupRows(group: CarGroup) {
}
Toggle("Warn when location is off", locationWarning, CarSettings::setLocationWarning)
}
CarGroup.MORE -> {
WebPageRow()
SettingsDivider()
Toggle("Experimental features", experimental, CarSettings::setExperimentalFeaturesEnabled)
if (experimental) {
SettingsDivider()
Toggle(
"Vehicle data",
carSensors,
CarSettings::setCarSensorsEnabled,
note = "Adds a Vehicle data tile: a live dump of the car's sensors. The field names are guesses until you've watched one change.",
)
SettingsDivider()
Toggle(
"Adaptive video quality",
adaptiveVideo,
CarSettings::setAdaptiveVideoEnabled,
note = "Lowers the picture's quality when the car or its Wi-Fi can't keep up, and raises it again once it can. Applies on the next connection.",
)
}
}
}
}
@@ -263,3 +299,54 @@ private fun Toggle(title: String, on: Boolean, onChange: (Boolean) -> Unit, note
trailing = { Switch(checked = on, onCheckedChange = null) },
)
}
/** The page the car's Browser app opens, typed here instead of on the car's keyboard. */
@Composable
private fun WebPageRow() {
val context = androidx.compose.ui.platform.LocalContext.current
var url by remember { mutableStateOf(app.seb3thehacker.gearslip.AppSettings.startupUrl(context)) }
Column(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp)) {
androidx.compose.material3.OutlinedTextField(
url,
{
url = it
app.seb3thehacker.gearslip.AppSettings.setStartupUrl(context, it)
},
label = { Text("Web page") },
placeholder = { Text("DuckDuckGo") },
singleLine = true,
keyboardOptions = androidx.compose.foundation.text.KeyboardOptions(keyboardType = androidx.compose.ui.text.input.KeyboardType.Uri),
modifier = Modifier.fillMaxWidth(),
)
Text(
"Shown by the Browser app in the car. Empty opens DuckDuckGo. Applies on the next connection.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 6.dp),
)
}
}
/** How music reaches the car: by Bluetooth as usual, or through Gearslip over USB. Same choice as setup. */
@Composable
internal fun CarAudioSection() {
val usb by CarSettings.pipeAudio.collectAsState()
SettingsSection(
"Car audio",
footer = if (usb) "Run the command in Help once, and USB stops asking." else "Calls go over Bluetooth either way.",
) {
SettingsRow(
"Bluetooth (Recommended)",
Modifier.selectable(!usb, role = androidx.compose.ui.semantics.Role.RadioButton) { CarSettings.setPipeAudio(false) },
subtitle = "No prompt each drive, and your map app's voice reaches the car",
trailing = { androidx.compose.material3.RadioButton(selected = !usb, onClick = null) },
)
SettingsDivider()
SettingsRow(
"USB",
Modifier.selectable(usb, role = androidx.compose.ui.semantics.Role.RadioButton) { CarSettings.setPipeAudio(true) },
subtitle = "Clearer sound with no delay. Each drive, Android asks you to share your screen, which lets Gearslip send the sound",
trailing = { androidx.compose.material3.RadioButton(selected = usb, onClick = null) },
)
}
}
@@ -140,9 +140,6 @@ fun SettingsScreen(
footer = "Gearslip is free, with no ads and no tracking. Your support keeps it going.",
) { DonateRows() }
if (ready(2)) ConnectionSection()
if (ready(3)) CarAudioSection()
if (ready(4)) CarScreenSection()
if (ready(5)) VoiceModelSection()
if (SHOW_RECOMMENDED_APPS && ready(6)) AppsSection(onOpenRecommendedApps)
if (ready(7)) SettingsSection(
"Usage notes",
@@ -225,30 +222,6 @@ private fun DebugSection() {
}
}
/** How music reaches the car: by Bluetooth as usual, or through Gearslip over USB. Same choice as setup. */
@Composable
private fun CarAudioSection() {
val usb by CarSettings.pipeAudio.collectAsState()
SettingsSection(
"Car audio",
footer = if (usb) "Run the command in Help once, and USB stops asking." else "Calls go over Bluetooth either way.",
) {
SettingsRow(
"Bluetooth (Recommended)",
Modifier.selectable(!usb, role = Role.RadioButton) { CarSettings.setPipeAudio(false) },
subtitle = "No prompt each drive, and your map app's voice reaches the car",
trailing = { RadioButton(selected = !usb, onClick = null) },
)
SettingsDivider()
SettingsRow(
"USB",
Modifier.selectable(usb, role = Role.RadioButton) { CarSettings.setPipeAudio(true) },
subtitle = "Clearer sound with no delay. Each drive, Android asks you to share your screen, which lets Gearslip send the sound",
trailing = { RadioButton(selected = usb, onClick = null) },
)
}
}
@Composable
private fun AppsSection(onOpenRecommendedApps: () -> Unit) {
SettingsSection("Apps") {