mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
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.
This commit is contained in:
1 parent
8d673d855b
commit
0a5acc012e
3 files changed
+54
-81
No files matched your search
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<Gu
|
||||
)
|
||||
}
|
||||
|
||||
// --- Near the end, every time: usage notes. On a replay, the switch shows what was
|
||||
// chosen before.
|
||||
add(usageNotesPage(context))
|
||||
|
||||
// --- Last of all, every time: apps worth installing, now that everything is set up.
|
||||
// --- At the end, every time: apps worth installing, how the car's buttons look, and last
|
||||
// the usage notes. On a replay, the switch shows what was chosen before.
|
||||
add(
|
||||
GuidePage(
|
||||
title = "Good company.",
|
||||
@@ -378,6 +381,15 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
|
||||
content = { RecommendedAppsList() },
|
||||
),
|
||||
)
|
||||
add(
|
||||
GuidePage(
|
||||
title = "Your buttons.",
|
||||
explanation = "Pick how the car's buttons look. Change it any time in the car's settings.",
|
||||
icon = { ButtonsIcon(it) },
|
||||
content = { ThemePicker(it.c, it.cq, it.cqSp, key = "guide-buttons") },
|
||||
),
|
||||
)
|
||||
add(usageNotesPage(context))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -507,10 +519,11 @@ fun SetupGuideScreen(onRequestCallScreening: () -> 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. */
|
||||
|
||||
Reference in new issue
Block a user