From c212c2e2d1925b2905398003300e564b9d758f29 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Wed, 7 Oct 2026 23:50:47 -0700 Subject: [PATCH] usage notes changes, audio changes, video quality increase. --- .../seb3thehacker/gearslip/ui/GearslipApp.kt | 10 ++- .../seb3thehacker/gearslip/ui/HomeScreen.kt | 5 +- .../gearslip/ui/SettingsScreen.kt | 14 ++- .../gearslip/ui/UsageStatsChoices.kt | 89 ++++++++++++------- 4 files changed, 83 insertions(+), 35 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 1a23888..4d06629 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt @@ -60,6 +60,8 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { }, ) } + // Home's usage notes row opens Settings at that section rather than at the top. + var settingsAtUsageNotes by rememberSaveable { mutableStateOf(false) } BackHandler(enabled = screen != Screen.HOME) { screen = if (screen == Screen.RECOMMENDED_APPS) Screen.SETTINGS else Screen.HOME } @@ -95,18 +97,20 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) { when (screen) { Screen.HOME -> HomeScreen( onOpenLogs = { screen = Screen.LOGS }, - onOpenSettings = { screen = Screen.SETTINGS }, + onOpenSettings = { settingsAtUsageNotes = false; screen = Screen.SETTINGS }, onOpenCarPreview = { screen = Screen.CAR_PREVIEW }, onOpenHelp = { screen = Screen.HELP }, + onOpenUsageNotes = { settingsAtUsageNotes = true; screen = Screen.SETTINGS }, onDisconnect = onDisconnect, ) Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = Screen.SETTINGS }) Screen.LOGS -> LogsScreen(onBack = { screen = Screen.HOME }) Screen.SETTINGS -> SettingsScreen( onBack = { screen = Screen.HOME }, - onOpenRecommendedApps = { screen = Screen.RECOMMENDED_APPS }, - onOpenWhatsNew = { screen = Screen.WHATS_NEW }, + onOpenRecommendedApps = { settingsAtUsageNotes = false; screen = Screen.RECOMMENDED_APPS }, + onOpenWhatsNew = { settingsAtUsageNotes = false; screen = Screen.WHATS_NEW }, onReplayTutorial = { screen = Screen.SETUP_GUIDE }, + atUsageNotes = settingsAtUsageNotes, ) Screen.RECOMMENDED_APPS -> RecommendedAppsScreen(onBack = { screen = Screen.SETTINGS }) Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = Screen.HOME }) 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 0680697..4040fc3 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeScreen.kt @@ -48,6 +48,7 @@ fun HomeScreen( onOpenSettings: () -> Unit, onOpenCarPreview: () -> Unit, onOpenHelp: () -> Unit, + onOpenUsageNotes: () -> Unit, onDisconnect: () -> Unit, ) { val status by SessionStatus.state.collectAsStateWithLifecycle() @@ -92,7 +93,6 @@ fun HomeScreen( StatusCard(status) Spacer(Modifier.height(16.dp)) UpdateCard() - UsageNotesHomeCard() } // Sits at the bottom, where a thumb reaches it. @@ -106,6 +106,9 @@ fun HomeScreen( Spacer(Modifier.height(16.dp)) } + UsageNotesHomeCard(onOpen = onOpenUsageNotes) + Spacer(Modifier.height(16.dp)) + Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.extraLarge) { Column { if (!live) { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt index 2b4045e..f397fbf 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SettingsScreen.kt @@ -57,6 +57,9 @@ import app.seb3thehacker.gearslip.BuildConfig import app.seb3thehacker.gearslip.CertProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class) @@ -66,7 +69,16 @@ fun SettingsScreen( onOpenRecommendedApps: () -> Unit, onOpenWhatsNew: () -> Unit, onReplayTutorial: () -> Unit, + atUsageNotes: Boolean = false, ) { + val scroll = rememberScrollState() + // Opened from Home's usage notes row: usage notes is the last section, so start at the end. + if (atUsageNotes) { + LaunchedEffect(Unit) { + snapshotFlow { scroll.maxValue }.first { it > 0 && it < Int.MAX_VALUE } + scroll.scrollTo(scroll.maxValue) + } + } Scaffold( containerColor = MaterialTheme.colorScheme.background, topBar = { @@ -85,7 +97,7 @@ fun SettingsScreen( .fillMaxSize() .padding(padding) .padding(horizontal = 24.dp) - .verticalScroll(rememberScrollState()), + .verticalScroll(scroll), verticalArrangement = Arrangement.spacedBy(24.dp), ) { Spacer(Modifier.height(0.dp)) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt index 36a04eb..612b5da 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/UsageStatsChoices.kt @@ -1,6 +1,7 @@ package app.seb3thehacker.gearslip.ui import androidx.compose.animation.AnimatedVisibility +import android.content.Context import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -9,6 +10,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.selection.toggleable import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp @@ -83,11 +86,7 @@ internal fun ColumnScope.UsageStatsRows() { SettingsRow( "Send usage notes", Modifier.toggleable(on, role = Role.Switch) { on = it; save() }, - subtitle = when { - BuildConfig.DEBUG -> "Dev build: nothing is ever sent" - BuildConfig.STATS_URL.isBlank() -> "This build has no stats server, so it sends nothing" - else -> null - }, + subtitle = buildNote(), icon = Icons.Filled.Send, trailing = { Switch(checked = on, onCheckedChange = null) }, ) @@ -126,14 +125,7 @@ internal fun ColumnScope.UsageStatsRows() { ) AnimatedVisibility(noteOpen) { // Rebuilt from the saved choices each time one changes, so it always matches. - val note = remember(choices) { - val preview = UsageStats.preview(context, everything = true) - // Until the first drive the list is empty, so show what one entry looks like. - val example = if (preview.optJSONArray("sessions")?.length() == 0) { - "\n\nExample, not sent: one drive adds this to \"sessions\"\n" + UsageStats.exampleSession.toString(2) - } else "" - preview.toString(2) + example - } + val note = remember(choices) { notePreview(context) } Surface( color = MaterialTheme.colorScheme.surfaceContainerLowest, shape = MaterialTheme.shapes.medium, @@ -164,23 +156,60 @@ private fun Choice(label: String, checked: Boolean, onChange: (Boolean) -> Unit) } } -/** Usage notes on Home, always: the switch, and once it's on, what to share. */ +/** The note as it would go out now, with an example drive until a real one is in it. */ +private fun notePreview(context: Context): String { + val preview = UsageStats.preview(context, everything = true) + // Until the first drive the list is empty, so show what one entry looks like. + val example = if (preview.optJSONArray("sessions")?.length() == 0) { + "\n\nExample, not sent: one drive adds this to \"sessions\"\n" + UsageStats.exampleSession.toString(2) + } else "" + return preview.toString(2) + example +} + +/** Why this build sends nothing, when it doesn't; null for a normal release. */ +private fun buildNote(): String? = when { + BuildConfig.DEBUG -> "Dev build: nothing is ever sent" + BuildConfig.STATS_URL.isBlank() -> "This build has no stats server, so it sends nothing" + else -> null +} + +/** + * Usage notes on Home, one row with one action either way. While they're off, the row is the + * switch, so opting in is a single tap. Once they're on, it shrinks to a quiet line that opens + * Settings at its usage notes section, so a driver already sharing isn't pitched again. + */ @Composable -internal fun UsageNotesHomeCard(modifier: Modifier = Modifier) { - Column(modifier) { - Text( - USAGE_STATS_TITLE, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 16.dp, bottom = 4.dp), - ) - Text( - "Share a short note after each drive, so the next driver knows whether Gearslip works " + - "in their car. No name, account, or location.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), - ) - SettingsCard { UsageStatsRows() } +internal fun UsageNotesHomeCard(onOpen: () -> Unit, modifier: Modifier = Modifier) { + val context = LocalContext.current + var on by remember { mutableStateOf(UsageStats.enabled(context)) } + + SettingsCard(modifier) { + if (on) { + SettingsRow( + "Sharing usage notes", + Modifier.clickable(onClick = onOpen), + subtitle = buildNote(), + icon = Icons.Filled.Check, + trailing = { + Icon( + Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + ) + } else { + SettingsRow( + USAGE_STATS_TITLE, + Modifier.toggleable(false, role = Role.Switch) { + UsageStats.set(context, true, UsageStats.choices(context)) + on = true + }, + subtitle = buildNote() ?: ("A short note after each drive tells the next driver " + + "whether their car works. No name, account, or location"), + icon = Icons.Filled.Send, + trailing = { Switch(checked = false, onCheckedChange = null) }, + ) + } } }