From 0a5acc012ed1ad49251419113fe5ed1967e88477 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Sat, 24 Oct 2026 16:12:51 -0700 Subject: [PATCH] setup: end on the app list, the button picker, then usage notes; intro keeps what's new The intro drops directions, app picks and the button picker. The setup guide's long pages fade out with a scroll hint, and the app list's names read on dark. --- .../seb3thehacker/gearslip/ui/IntroScreen.kt | 73 +------------------ .../gearslip/ui/RecommendedApps.kt | 6 +- .../gearslip/ui/SetupGuideScreen.kt | 56 +++++++++++--- 3 files changed, 54 insertions(+), 81 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt index e83ebd0..0af2f95 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/IntroScreen.kt @@ -171,7 +171,7 @@ internal fun introColors(): IntroColors { } /** In the order shown: what's new, what to pick, housekeeping, then the people behind it. */ -private enum class IntroStep { WELCOME, LOOK, DIRECTIONS, CONNECTS, CALLS, SETTINGS, APPS, THEME } +private enum class IntroStep { WELCOME, LOOK, CONNECTS, CALLS, SETTINGS } private val Pop = CubicBezierEasing(0.3f, 1.6f, 0.5f, 1f) private val Rise = CubicBezierEasing(0.2f, 0.8f, 0.2f, 1f) @@ -210,10 +210,6 @@ fun IntroScreen(onDone: () -> Unit) { "The car screen takes your phone's colors, and every card and button matches.", step) { GlyphIcon(Icons.Filled.Star, c.onPrimaryC) } - IntroStep.DIRECTIONS -> BadgeStep(c, ::cq, ::cqSp, c.secondaryC, "Directions at a glance.", - "A big turn card on the left shows the arrow, the distance, and your lane. The map keeps the right side.", step) { - GlyphIcon(Icons.Filled.KeyboardArrowUp, c.onSecondaryC) - } IntroStep.CONNECTS -> BadgeStep(c, ::cq, ::cqSp, c.tertiaryC, "Connects by itself.", "Say yes once after a wireless drive, and Gearslip connects whenever the car's Bluetooth does.", step) { WifiIcon(c.onTertiaryC) @@ -226,12 +222,6 @@ fun IntroScreen(onDone: () -> Unit) { "Change the car's settings from the phone's home screen, before you drive.", step) { GlyphIcon(Icons.Filled.Settings, c.onSecondaryC) } - IntroStep.APPS -> BadgeStep(c, ::cq, ::cqSp, c.secondaryC, "Good company.", - "Free, open-source apps that work well in the car. The first in each row works best. Tap one to see it on GitHub.", step, - extra = { AppPicks(c, ::cq, ::cqSp, step) }) { StarIcon(c.onSecondaryC) } - IntroStep.THEME -> BadgeStep(c, ::cq, ::cqSp, c.tertiaryC, "Your buttons.", - "Pick how the car's buttons look. Change it any time in the car's settings.", step, - extra = { ThemePicker(c, ::cq, ::cqSp, step) }) { ButtonsIcon(c.onTertiaryC) } } } Column(Modifier.fillMaxWidth().padding(horizontal = cq(8f))) { @@ -518,69 +508,12 @@ private fun CommandDrift(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> Tex } /** Each kind of app with its picks, the first recommended most. */ -private val APP_PICKS = listOf( - "Maps" to listOf( - "Organic Maps" to "https://github.com/organicmaps/organicmaps", - "Vela" to "https://github.com/PimpinPumpkin/Vela", - ), - "Music" to listOf( - "ViVi Music" to "https://github.com/vivizzz007/vivi-music", - "Metrolist" to "https://github.com/MetrolistGroup/Metrolist", - ), - "Podcasts" to listOf( - "AntennaPod" to "https://github.com/AntennaPod/AntennaPod", - ), - "Other cool apps" to listOf( - "RF Sentinel" to "https://github.com/CIS-C0/RFSentinel", - ), -) - -/** One row per kind, with a chip per app opening its GitHub page; the rows arrive one after another. */ -@Composable -private fun AppPicks(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, key: IntroStep) { - val context = LocalContext.current - Column(Modifier.padding(top = cq(6f)), verticalArrangement = Arrangement.spacedBy(cq(2.6f))) { - APP_PICKS.forEachIndexed { i, (kind, apps) -> - Column( - Modifier - .fillMaxWidth() - .up(entrance(key, 500 + i * 120, 700), cq(7f)) - .clip(RoundedCornerShape(cq(5f))) - .background(c.secondaryC) - .padding(horizontal = cq(4f), vertical = cq(3f)), - ) { - Text(kind.uppercase(), color = c.onSecondaryC.copy(alpha = 0.7f), - style = TextStyle(fontSize = cqSp(3.2f), fontWeight = FontWeight.Bold, letterSpacing = cqSp(0.3f)), - modifier = Modifier.padding(start = cq(1f), bottom = cq(1.6f))) - Row(horizontalArrangement = Arrangement.spacedBy(cq(2f))) { - apps.forEach { (name, url) -> - Text( - "$name ↗", color = c.onSecondaryC, - style = TextStyle(fontSize = cqSp(4.4f), fontWeight = FontWeight.ExtraBold), - maxLines = 1, - modifier = Modifier - .clip(RoundedCornerShape(50)) - .border(BorderStroke(1.5.dp, c.onSecondaryC.copy(alpha = 0.35f)), RoundedCornerShape(50)) - .clickable { - runCatching { - context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) - } - } - .padding(horizontal = cq(3.6f), vertical = cq(2f)), - ) - } - } - } - } - } -} - /** * The car's button styles side by side, each drawn in its own style, and a row of real car * controls below that redraws in whichever is picked. Picking saves it, so the car shows it too. */ @Composable -private fun ThemePicker(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, key: IntroStep) { +internal fun ThemePicker(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, key: Any) { val chosenId by CarSettings.uiTheme.collectAsState() val chosen = GsThemes.byId(chosenId) // Flat first: it's the default. @@ -628,7 +561,7 @@ private fun ThemePicker(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> Text /** Three keys of a keypad, pressing one after another. */ @Composable -private fun ButtonsIcon(color: Color) { +internal fun ButtonsIcon(color: Color) { val t by rememberInfiniteTransition(label = "keys").animateFloat( 0f, 3f, infiniteRepeatable(tween(2400, easing = LinearEasing)), label = "keys", ) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/RecommendedApps.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/RecommendedApps.kt index 2066db8..f641f4a 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/RecommendedApps.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/RecommendedApps.kt @@ -138,11 +138,13 @@ private fun AppRow(app: RecommendedApp, onOpen: () -> Unit) { if (icon != null) { Image(icon.toBitmap(96, 96).asImageBitmap(), contentDescription = null, Modifier.fillMaxSize()) } else { - Text(app.name.take(1), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) + Text(app.name.take(1), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface) } } Column(Modifier.weight(1f).padding(horizontal = 14.dp)) { - Text(app.name, style = MaterialTheme.typography.titleMedium) + // Colours set outright: the setup guide draws this on its own background, with no + // Surface to hand down a content colour, and unset text came out dark on dark. + Text(app.name, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface) Text(app.about, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) if (icon != null) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 4.dp)) { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt index 84881a4..8a26be4 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt @@ -93,6 +93,9 @@ import app.seb3thehacker.gearslip.host.AndroidAuto import app.seb3thehacker.gearslip.notify.GearslipNotificationListener import app.seb3thehacker.gearslip.stats.UsageStats import androidx.compose.runtime.DisposableEffect +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.foundation.layout.offset /** * One page of the guide. [permissions] drives a system permission dialog; [command] is an adb @@ -100,6 +103,9 @@ import androidx.compose.runtime.DisposableEffect * settings screen) without by itself finishing the page. Everything is skippable: none of this * can be verified as done, only offered. */ +/** The guide's colours and its one-percent-of-the-width scale, for content that sizes itself the same way. */ +internal class GuideScale(val c: IntroColors, val cq: (Float) -> Dp, val cqSp: (Float) -> TextUnit) + private class GuidePage( val title: String, /** Plain text, or an [AnnotatedString] when part of it needs to stand out. */ @@ -112,8 +118,8 @@ private class GuidePage( val command: String? = null, val actionLabel: String? = null, val onAction: (() -> Unit)? = null, - /** Drawn under [explanation]. */ - val content: (@Composable () -> Unit)? = null, + /** Drawn under [explanation], sized off the same screen-width scale as the page. */ + val content: (@Composable (GuideScale) -> Unit)? = null, /** Only shown when the driver picked USB audio on the car audio page. */ val usbOnly: Boolean = false, ) @@ -365,11 +371,8 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List Unit): List Unit, onDone: () -> Unit, onl // pushes them off the screen. Short pages sit in the middle. BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) { key(index) { + val scroll = rememberScrollState() Column( Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .verticalScroll(scroll) .heightIn(min = maxHeight) .padding(vertical = cq(6f)), verticalArrangement = Arrangement.Center, @@ -548,7 +561,7 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onl style = TextStyle(fontSize = cqSp(4.5f), lineHeight = cqSp(6.5f)), ) page.command?.let { CopyableCommand(it) } - page.content?.invoke() + page.content?.invoke(GuideScale(c, ::cq, ::cqSp)) if (page.permissions != null && alreadyGranted) { Text( "Already allowed.", color = c.primary, @@ -557,6 +570,9 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onl } } } + // A long page (the app list, the usage notes) says so: a fade over the + // last lines and a nudge to keep going, until the bottom is in view. + if (scroll.canScrollForward) ScrollHint(c, ::cq, ::cqSp, Modifier.align(Alignment.BottomCenter)) } } @@ -583,6 +599,28 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onl } } +/** A fade into the page's background with "Scroll for more" over it, at the bottom of a page that goes on. */ +@Composable +private fun ScrollHint(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, modifier: Modifier = Modifier) { + val bob by rememberInfiniteTransition(label = "hint").animateFloat( + 0f, 1f, infiniteRepeatable(tween(900, easing = FastOutSlowInEasing), RepeatMode.Reverse), label = "bob", + ) + Box( + modifier + .fillMaxWidth() + .height(cq(22f)) + .background(androidx.compose.ui.graphics.Brush.verticalGradient(listOf(c.surface.copy(alpha = 0f), c.surface))), + contentAlignment = Alignment.BottomCenter, + ) { + Text( + "Scroll for more ↓", + color = c.primary, + style = TextStyle(fontSize = cqSp(4f), fontWeight = FontWeight.Bold), + modifier = Modifier.padding(bottom = cq(2f)).offset(y = cq(1.2f) * bob), + ) + } +} + internal enum class PillStyle { FILLED, OUTLINED, TEXT } /** The intro's rounded buttons: filled for the step's main action, outlined beside it, bare text to skip. */