home: Help, Preview and Logs in one row at the bottom, and What's new tops Settings

Small labelled buttons pinned at the bottom of Home, so cards above can't push them out of sight.
Help and tools leaves Settings, Connection help (the same page as Help) goes, What's new moves to
the top of Settings, the intro replay goes, and the debug build's test message gets its own
section there.
This commit is contained in:
Seb3thehacker committed 2026-10-24 00:17:12 -07:00
1 parent 55c25fe967
commit ef006dc695
3 files changed
+55 -85

No files matched your search

@@ -108,26 +108,23 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
Screen.INTRO -> IntroScreen(onDone = { screen = introReturn }) Screen.INTRO -> IntroScreen(onDone = { screen = introReturn })
Screen.HOME -> HomeScreen( Screen.HOME -> HomeScreen(
onOpenSettings = { settingsAtUsageNotes = false; screen = Screen.SETTINGS }, onOpenSettings = { settingsAtUsageNotes = false; screen = Screen.SETTINGS },
onOpenWhatsNew = { backTo = Screen.HOME; screen = Screen.WHATS_NEW },
onOpenHelp = { backTo = Screen.HOME; screen = Screen.HELP }, onOpenHelp = { backTo = Screen.HOME; screen = Screen.HELP },
onOpenUsageNotes = { settingsAtUsageNotes = true; screen = Screen.SETTINGS }, onOpenUsageNotes = { settingsAtUsageNotes = true; screen = Screen.SETTINGS },
onOpenCarPreview = { backTo = Screen.HOME; screen = Screen.CAR_PREVIEW },
onOpenLogs = { backTo = Screen.HOME; screen = Screen.LOGS },
onDisconnect = onDisconnect, onDisconnect = onDisconnect,
) )
Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = backTo }) Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = backTo })
Screen.LOGS -> LogsScreen(onBack = { screen = Screen.SETTINGS }) Screen.LOGS -> LogsScreen(onBack = { screen = backTo })
Screen.SETTINGS -> SettingsScreen( Screen.SETTINGS -> SettingsScreen(
onBack = { screen = Screen.HOME }, onBack = { screen = Screen.HOME },
onOpenRecommendedApps = { settingsAtUsageNotes = false; screen = Screen.RECOMMENDED_APPS }, onOpenRecommendedApps = { settingsAtUsageNotes = false; screen = Screen.RECOMMENDED_APPS },
onOpenWhatsNew = { settingsAtUsageNotes = false; backTo = Screen.SETTINGS; screen = Screen.WHATS_NEW }, onOpenWhatsNew = { settingsAtUsageNotes = false; backTo = Screen.SETTINGS; screen = Screen.WHATS_NEW },
onOpenHelp = { settingsAtUsageNotes = false; backTo = Screen.SETTINGS; screen = Screen.HELP },
onOpenCarPreview = { settingsAtUsageNotes = false; screen = Screen.CAR_PREVIEW },
onOpenLogs = { settingsAtUsageNotes = false; screen = Screen.LOGS },
onReplayTutorial = { screen = Screen.SETUP_GUIDE }, onReplayTutorial = { screen = Screen.SETUP_GUIDE },
onReplayIntro = { Intro.replay(context); introReturn = Screen.SETTINGS; screen = Screen.INTRO },
atUsageNotes = settingsAtUsageNotes, atUsageNotes = settingsAtUsageNotes,
) )
Screen.RECOMMENDED_APPS -> RecommendedAppsScreen(onBack = { screen = Screen.SETTINGS }) Screen.RECOMMENDED_APPS -> RecommendedAppsScreen(onBack = { screen = Screen.SETTINGS })
Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = Screen.SETTINGS }) Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = backTo })
Screen.HELP -> HelpScreen( Screen.HELP -> HelpScreen(
onBack = { screen = backTo }, onBack = { screen = backTo },
onRequestCallScreening = onRequestCallScreening, onRequestCallScreening = onRequestCallScreening,
@@ -5,6 +5,7 @@ import app.seb3thehacker.gearslip.speech.SpeechModels
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -27,6 +28,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.List import androidx.compose.material.icons.filled.List
@@ -60,9 +63,10 @@ import app.seb3thehacker.gearslip.stats.WorkedPrompt
@Composable @Composable
fun HomeScreen( fun HomeScreen(
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onOpenWhatsNew: () -> Unit,
onOpenHelp: () -> Unit, onOpenHelp: () -> Unit,
onOpenUsageNotes: () -> Unit, onOpenUsageNotes: () -> Unit,
onOpenCarPreview: () -> Unit,
onOpenLogs: () -> Unit,
onDisconnect: () -> Unit, onDisconnect: () -> Unit,
) { ) {
val status by SessionStatus.state.collectAsStateWithLifecycle() val status by SessionStatus.state.collectAsStateWithLifecycle()
@@ -125,21 +129,11 @@ fun HomeScreen(
// Sits at the bottom, where a thumb reaches it. // Sits at the bottom, where a thumb reaches it.
Column(Modifier.padding(vertical = 16.dp)) { Column(Modifier.padding(vertical = 16.dp)) {
SettingsCard { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SettingsRow( HomeShortcut(Icons.Filled.Info, "Help", onOpenHelp, Modifier.weight(1f))
"What's new in ${Intro.release}", // The preview draws the car screen on the phone, so it waits until no car is using it.
Modifier.clickable(onClick = onOpenWhatsNew), if (!live) HomeShortcut(Icons.Filled.PlayArrow, "Preview", onOpenCarPreview, Modifier.weight(1f))
icon = Icons.Filled.Star, HomeShortcut(Icons.AutoMirrored.Filled.List, "Logs", onOpenLogs, Modifier.weight(1f))
trailing = { Arrow() },
)
SettingsDivider()
SettingsRow(
"Which cars work",
Modifier.clickable(onClick = onOpenHelp),
subtitle = "Most do. See which ones, and what to set up first",
icon = Icons.Filled.Info,
trailing = { Arrow() },
)
} }
if (live) { if (live) {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
@@ -157,6 +151,27 @@ fun HomeScreen(
} }
} }
/** One of Home's shortcuts at the bottom: an icon over a one-word label, small enough that all fit in a row. */
@Composable
private fun HomeShortcut(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Surface(
onClick = onClick,
modifier = modifier.heightIn(min = 64.dp),
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
) {
Column(
Modifier.padding(vertical = 10.dp, horizontal = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(icon, contentDescription = null, modifier = Modifier.size(22.dp))
Text(label, style = MaterialTheme.typography.labelMedium, maxLines = 1)
}
}
}
/** Bluetooth audio is chosen, but the phone isn't connected to the car. See [BluetoothAudioCheck]. */ /** Bluetooth audio is chosen, but the phone isn't connected to the car. See [BluetoothAudioCheck]. */
@Composable @Composable
private fun BluetoothMissingCard() { private fun BluetoothMissingCard() {
@@ -80,11 +80,7 @@ fun SettingsScreen(
onBack: () -> Unit, onBack: () -> Unit,
onOpenRecommendedApps: () -> Unit, onOpenRecommendedApps: () -> Unit,
onOpenWhatsNew: () -> Unit, onOpenWhatsNew: () -> Unit,
onOpenHelp: () -> Unit,
onOpenCarPreview: () -> Unit,
onOpenLogs: () -> Unit,
onReplayTutorial: () -> Unit, onReplayTutorial: () -> Unit,
onReplayIntro: () -> Unit,
atUsageNotes: Boolean = false, atUsageNotes: Boolean = false,
) { ) {
val scroll = rememberScrollState() val scroll = rememberScrollState()
@@ -119,12 +115,20 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
Spacer(Modifier.height(0.dp)) Spacer(Modifier.height(0.dp))
SettingsSection {
SettingsRow(
"What's new",
Modifier.clickable(onClick = onOpenWhatsNew),
subtitle = "Version ${BuildConfig.VERSION_NAME}",
icon = Icons.Filled.Star,
trailing = { Arrow() },
)
}
SettingsSection( SettingsSection(
"Support Gearslip", "Support Gearslip",
footer = "Gearslip is free, with no ads and no tracking. Your support keeps it going.", footer = "Gearslip is free, with no ads and no tracking. Your support keeps it going.",
) { DonateRows() } ) { DonateRows() }
ConnectionSection() ConnectionSection()
HelpSection(onOpenHelp, onOpenCarPreview, onOpenLogs)
CarAudioSection() CarAudioSection()
CarScreenSection() CarScreenSection()
VoiceModelSection() VoiceModelSection()
@@ -136,7 +140,8 @@ fun SettingsScreen(
) { UsageStatsRows() } ) { UsageStatsRows() }
SettingsSection("Updates") { UpdateRow() } SettingsSection("Updates") { UpdateRow() }
CertificateSection() CertificateSection()
AboutSection(onOpenWhatsNew, onReplayTutorial, onReplayIntro) AboutSection(onReplayTutorial)
if (BuildConfig.DEBUG) DebugSection()
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
} }
} }
@@ -149,26 +154,10 @@ private fun Arrow() = Icon(
tint = MaterialTheme.colorScheme.onSurfaceVariant, tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
/** What's new, the replays, and where Gearslip lives outside the app. */ /** The setup guide replay, and where Gearslip lives outside the app. */
@Composable @Composable
private fun AboutSection(onOpenWhatsNew: () -> Unit, onReplayTutorial: () -> Unit, onReplayIntro: () -> Unit) { private fun AboutSection(onReplayTutorial: () -> Unit) {
SettingsSection("About") { SettingsSection("About") {
SettingsRow(
"What's new",
Modifier.clickable(onClick = onOpenWhatsNew),
subtitle = "Version ${BuildConfig.VERSION_NAME}",
icon = Icons.Filled.Star,
trailing = { Arrow() },
)
SettingsDivider()
SettingsRow(
"Replay the ${Intro.release} intro",
Modifier.clickable(onClick = onReplayIntro),
subtitle = "What's in this release, in a few taps",
icon = Icons.Filled.Star,
trailing = { Arrow() },
)
SettingsDivider()
SettingsRow( SettingsRow(
"Replay the setup guide", "Replay the setup guide",
Modifier.clickable(onClick = onReplayTutorial), Modifier.clickable(onClick = onReplayTutorial),
@@ -200,48 +189,17 @@ private fun ConnectionSection() {
} }
} }
/** Connection help, and the tools for checking what Gearslip does. */ /** Tools for testing a debug build; release builds never show this section. */
@Composable @Composable
private fun HelpSection(onOpenHelp: () -> Unit, onOpenCarPreview: () -> Unit, onOpenLogs: () -> Unit) { private fun DebugSection() {
val context = LocalContext.current val context = LocalContext.current
val status by SessionStatus.state.collectAsState() SettingsSection("Debug build") {
val live = status.phase == SessionStatus.Phase.CONNECTING || status.phase == SessionStatus.Phase.PROJECTING
SettingsSection("Help and tools") {
SettingsRow( SettingsRow(
"Connection help", "Send a test message",
Modifier.clickable(onClick = onOpenHelp), Modifier.clickable { TestMessage.post(context) },
subtitle = "Which cars work, and what to set up first", subtitle = "Posts a message notification to read on the car",
icon = Icons.Filled.Info, icon = Icons.AutoMirrored.Filled.Send,
trailing = { Arrow() },
) )
// The preview draws the car screen on the phone, so it waits until no car is using it.
if (!live) {
SettingsDivider()
SettingsRow(
"Car preview",
Modifier.clickable(onClick = onOpenCarPreview),
subtitle = "See the car screen on your phone",
icon = Icons.Filled.PlayArrow,
trailing = { Arrow() },
)
}
SettingsDivider()
SettingsRow(
"Live logs",
Modifier.clickable(onClick = onOpenLogs),
subtitle = "What Gearslip and the car are saying to each other",
icon = Icons.AutoMirrored.Filled.List,
trailing = { Arrow() },
)
if (BuildConfig.DEBUG) {
SettingsDivider()
SettingsRow(
"Send a test message",
Modifier.clickable { TestMessage.post(context) },
subtitle = "Debug builds only. Posts a message notification to read on the car",
icon = Icons.AutoMirrored.Filled.Send,
)
}
} }
} }