intro: only after an update, never on a new install; recommend Organic Maps before Vela and ViVi Music before Metrolist; the thanks page links the list of cars that work

This commit is contained in:
Seb3thehacker committed 2026-10-13 19:00:51 -07:00
1 parent cd7b13c88e
commit 76955669f5
2 files changed
+57 -25

No files matched your search

@@ -97,8 +97,8 @@ import kotlin.random.Random
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
/** /**
* The release intro: a few tap-through screens shown once after each new release (and on a new * The release intro: a few screens shown once after updating to a new release, before setup. New
* install), before setup. Its look follows the 0.3 mockup: Material shapes in the phone's own colours, * installs skip it. Its look follows the 0.3 mockup: Material shapes in the phone's own colours,
* with every size taken from the screen's width so it scales from small phones to tablets. * with every size taken from the screen's width so it scales from small phones to tablets.
*/ */
object Intro { object Intro {
@@ -108,8 +108,19 @@ object Intro {
/** "0.3" from "0.3.00-alpha": the intro belongs to a release, not to each patch. */ /** "0.3" from "0.3.00-alpha": the intro belongs to a release, not to each patch. */
val release: String = BuildConfig.VERSION_NAME.split('.').take(2).joinToString(".") val release: String = BuildConfig.VERSION_NAME.split('.').take(2).joinToString(".")
fun due(context: Context): Boolean = /**
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_SEEN, null) != release * True once per release for people who updated. A new install gets the setup guide instead:
* there's nothing "new" yet, so this release counts as seen and the next one shows.
*/
fun due(context: Context): Boolean {
if (context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(KEY_SEEN, null) == release) return false
val info = runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }.getOrNull()
if (info != null && info.firstInstallTime == info.lastUpdateTime) {
markSeen(context)
return false
}
return true
}
fun markSeen(context: Context) { fun markSeen(context: Context) {
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY_SEEN, release).apply() context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putString(KEY_SEEN, release).apply()
@@ -187,7 +198,10 @@ fun IntroScreen(onDone: () -> Unit) {
) { ShieldIcon(c.onSecondaryC, c.secondaryC) } ) { ShieldIcon(c.onSecondaryC, c.secondaryC) }
IntroStep.THANKS -> BadgeStep(c, ::cq, ::cqSp, c.tertiaryC, "Thank you.", IntroStep.THANKS -> BadgeStep(c, ::cq, ::cqSp, c.tertiaryC, "Thank you.",
"Thanks to your usage notes, Gearslip is confirmed to work in cars newer than 2020. " + "Thanks to your usage notes, Gearslip is confirmed to work in cars newer than 2020. " +
"Each note shows which cars work and which still fail, so I know what to fix next.", step) { HeartIcon(c.onTertiaryC) } "Each note shows which cars work and which still fail, so I know what to fix next.", step,
action = "See which cars work" to { GearslipLinks.open(context, GearslipLinks.CARS) },
outlined = true,
) { HeartIcon(c.onTertiaryC) }
IntroStep.CHAT -> BadgeStep(c, ::cq, ::cqSp, c.primaryC, "Come say hi.", IntroStep.CHAT -> BadgeStep(c, ::cq, ::cqSp, c.primaryC, "Come say hi.",
"Gearslip has a chat room on Matrix. Talk with other drivers about cars, apps, and " + "Gearslip has a chat room on Matrix. Talk with other drivers about cars, apps, and " +
"where Gearslip is headed.", step, "where Gearslip is headed.", step,
@@ -195,7 +209,7 @@ fun IntroScreen(onDone: () -> Unit) {
outlined = true, outlined = true,
) { ChatIcon(c.onPrimaryC, c.primaryC) } ) { ChatIcon(c.onPrimaryC, c.primaryC) }
IntroStep.APPS -> BadgeStep(c, ::cq, ::cqSp, c.secondaryC, "Good company.", IntroStep.APPS -> BadgeStep(c, ::cq, ::cqSp, c.secondaryC, "Good company.",
"Free, open-source apps that work well in the car. Tap one to see it on GitHub.", step, "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) } extra = { AppPicks(c, ::cq, ::cqSp, step) }) { StarIcon(c.onSecondaryC) }
IntroStep.THEME -> BadgeStep(c, ::cq, ::cqSp, c.tertiaryC, "Your buttons.", 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, "Pick how the car's buttons look. Change it any time in the car's settings.", step,
@@ -489,38 +503,56 @@ 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( private val APP_PICKS = listOf(
Triple("Music", "Metrolist", "https://github.com/MetrolistGroup/Metrolist"), "Maps" to listOf(
Triple("Maps", "Vela", "https://github.com/PimpinPumpkin/Vela"), "Organic Maps" to "https://github.com/organicmaps/organicmaps",
Triple("Podcasts", "AntennaPod", "https://github.com/AntennaPod/AntennaPod"), "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",
),
) )
/** One row per pick, each opening the app's GitHub page; they arrive one after another. */ /** One row per kind, with a chip per app opening its GitHub page; the rows arrive one after another. */
@Composable @Composable
private fun AppPicks(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, key: IntroStep) { private fun AppPicks(c: IntroColors, cq: (Float) -> Dp, cqSp: (Float) -> TextUnit, key: IntroStep) {
val context = LocalContext.current val context = LocalContext.current
Column(Modifier.padding(top = cq(6f)), verticalArrangement = Arrangement.spacedBy(cq(2.6f))) { Column(Modifier.padding(top = cq(6f)), verticalArrangement = Arrangement.spacedBy(cq(2.6f))) {
APP_PICKS.forEachIndexed { i, (kind, name, url) -> APP_PICKS.forEachIndexed { i, (kind, apps) ->
Row( Column(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.up(entrance(key, 500 + i * 120, 700), cq(7f)) .up(entrance(key, 500 + i * 120, 700), cq(7f))
.clip(RoundedCornerShape(cq(5f))) .clip(RoundedCornerShape(cq(5f)))
.background(c.secondaryC) .background(c.secondaryC)
.clickable { .padding(horizontal = cq(4f), vertical = cq(3f)),
runCatching {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
.padding(horizontal = cq(5f), vertical = cq(3.4f)),
verticalAlignment = Alignment.CenterVertically,
) { ) {
Column(Modifier.weight(1f)) { Text(kind.uppercase(), color = c.onSecondaryC.copy(alpha = 0.7f),
Text(kind.uppercase(), color = c.onSecondaryC.copy(alpha = 0.7f), style = TextStyle(fontSize = cqSp(3.2f), fontWeight = FontWeight.Bold, letterSpacing = cqSp(0.3f)),
style = TextStyle(fontSize = cqSp(3.2f), fontWeight = FontWeight.Bold, letterSpacing = cqSp(0.3f))) modifier = Modifier.padding(start = cq(1f), bottom = cq(1.6f)))
Text(name, color = c.onSecondaryC, style = TextStyle(fontSize = cqSp(5.2f), fontWeight = FontWeight.ExtraBold)) 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)),
)
}
} }
Text("GitHub ↗", color = c.onSecondaryC, style = TextStyle(fontSize = cqSp(3.8f), fontWeight = FontWeight.SemiBold))
} }
} }
} }
@@ -47,8 +47,8 @@ private class RecommendedApp(val name: String, val packageName: String, val url:
private val recommended = listOf( private val recommended = listOf(
"Maps" to listOf( "Maps" to listOf(
RecommendedApp("Vela Maps", "app.vela", "https://github.com/PimpinPumpkin/Vela", "Turn-by-turn navigation without Google."),
RecommendedApp("Organic Maps", "app.organicmaps", "https://github.com/organicmaps/organicmaps", "Offline maps and routing."), RecommendedApp("Organic Maps", "app.organicmaps", "https://github.com/organicmaps/organicmaps", "Offline maps and routing."),
RecommendedApp("Vela Maps", "app.vela", "https://github.com/PimpinPumpkin/Vela", "Turn-by-turn navigation without Google."),
), ),
"Music" to listOf( "Music" to listOf(
RecommendedApp("ViVi Music", "com.vivi.vivimusic", "https://github.com/vivizzz007/vivi-music", "A YouTube Music player."), RecommendedApp("ViVi Music", "com.vivi.vivimusic", "https://github.com/vivizzz007/vivi-music", "A YouTube Music player."),