car launcher: only apps that don't work get a mark, a red X, and the badge key is gone

This commit is contained in:
Seb3thehacker committed 2026-10-13 17:39:15 -07:00
1 parent 73be554eb2
commit cae05ecdc0
1 file changed
+9 -95
@@ -70,12 +70,8 @@ private class Tile(
val label: String, val label: String,
val icon: ImageBitmap? = null, val icon: ImageBitmap? = null,
val glyph: ImageVector? = null, val glyph: ImageVector? = null,
/** Tested from start to finish with Gearslip; drawn with a check mark. */
val verified: Boolean = false,
/** Known not to work; drawn with a red X instead of being left off the launcher. */ /** Known not to work; drawn with a red X instead of being left off the launcher. */
val broken: Boolean = false, val broken: Boolean = false,
/** Starts and can be used, but rough enough not to call working; drawn with a yellow mark. */
val partial: Boolean = false,
val onLongClick: (() -> Unit)? = null, val onLongClick: (() -> Unit)? = null,
val onClick: () -> Unit, val onClick: () -> Unit,
) )
@@ -250,18 +246,15 @@ fun CarLauncher() {
val all = rememberAllEntries() val all = rememberAllEntries()
val running = rememberRunningApps() val running = rememberRunningApps()
val showVehicleData = rememberVehicleDataShown() val showVehicleData = rememberVehicleDataShown()
var showBadgeKey by remember { mutableStateOf(false) }
fun tileOf(entry: Entry): Tile { fun tileOf(entry: Entry): Tile {
// The badges rate an app's own car screens; a messenger shown on Gearslip's own Messages // Only apps that don't work get a mark. The X rates an app's own car screens; a messenger
// screen has none, so it gets no badge. // shown on Gearslip's own Messages screen has none, so it never gets one.
val pkg = entry.packageName?.takeIf { entry.messaging == null } val pkg = entry.packageName?.takeIf { entry.messaging == null }
val id = entry.componentId val id = entry.componentId
return Tile( return Tile(
entry.label, entry.icon, entry.builtIn?.glyph, entry.label, entry.icon, entry.builtIn?.glyph,
verified = pkg != null && !KnownApps.isPartial(pkg) && (KnownApps.works(pkg) || (entry.media != null && KnownApps.playerWorks(pkg))),
broken = pkg != null && (KnownApps.isBroken(pkg) || (entry.template != null && KnownApps.screenBroken(pkg))), broken = pkg != null && (KnownApps.isBroken(pkg) || (entry.template != null && KnownApps.screenBroken(pkg))),
partial = pkg != null && KnownApps.isPartial(pkg),
onLongClick = id?.let { { navigator.showAppMenu(it) } }, onLongClick = id?.let { { navigator.showAppMenu(it) } },
) { openApp(entry, id?.let { running[it] }, navigator, frame) } ) { openApp(entry, id?.let { running[it] }, navigator, frame) }
} }
@@ -277,60 +270,6 @@ fun CarLauncher() {
verticalArrangement = Arrangement.spacedBy(12.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
items(tiles) { AppTile(it) } items(tiles) { AppTile(it) }
if (tiles.any { it.verified || it.broken || it.partial }) {
// Folded away behind one button: it's reference, read once, not something every
// visit to the launcher needs spelled out under the grid.
item(span = { GridItemSpan(maxLineSpan) }) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
GsButton(onClick = { showBadgeKey = !showBadgeKey }, tone = GsTone.Neutral) {
Icon(Icons.Filled.Info, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(8.dp))
Text(if (showBadgeKey) "Hide badge key" else "What the badges mean")
}
if (showBadgeKey) {
BadgeLegend(
verified = tiles.any { it.verified },
partial = tiles.any { it.partial },
broken = tiles.any { it.broken },
)
}
}
}
}
}
}
/** Says what each badge on a tile means, once, under the grid - only the ones actually in use. */
@Composable
private fun BadgeLegend(verified: Boolean, partial: Boolean, broken: Boolean) {
Column(
Modifier.fillMaxWidth().padding(top = 4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
if (verified) LegendRow(size = 16.dp, badge = { VerifiedBadge(it) }, text = "Tested and working with Gearslip")
if (partial) LegendRow(size = 16.dp, badge = { PartialBadge(it) }, text = "Runs, but not cleanly, with Gearslip")
if (broken) LegendRow(size = 16.dp, badge = { BrokenBadge(it) }, text = "Doesn't work with Gearslip yet")
}
}
@Composable
private fun LegendRow(size: androidx.compose.ui.unit.Dp, badge: @Composable (androidx.compose.ui.unit.Dp) -> Unit, text: String) {
Row(horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
badge(size)
Spacer(Modifier.width(8.dp))
Text(text, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
/** A green disc with a white check: the same mark on every tile and in the legend. */
@Composable
private fun VerifiedBadge(size: androidx.compose.ui.unit.Dp, modifier: Modifier = Modifier) {
Box(
modifier.size(size).background(Color(0xFF43A047), CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Filled.Check, contentDescription = "Tested and working", tint = Color.White, modifier = Modifier.size(size * 0.66f))
} }
} }
@@ -345,39 +284,16 @@ private fun BrokenBadge(size: androidx.compose.ui.unit.Dp, modifier: Modifier =
} }
} }
/** A yellow disc with a warning mark: neither the check nor the X, since this app runs but isn't clean about it. */
@Composable
private fun PartialBadge(size: androidx.compose.ui.unit.Dp, modifier: Modifier = Modifier) {
Box(
modifier.size(size).background(Color(0xFFF9A825), CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(Icons.Filled.Warning, contentDescription = "Works, but not cleanly, with Gearslip", tint = Color.White, modifier = Modifier.size(size * 0.6f))
}
}
/** Trial: where a tile says whether its app works. See [AppTileBody]. */
private enum class BadgeStyle { ON_ICON, OUTLINE }
private val BADGE_STYLE = BadgeStyle.ON_ICON
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun AppTile(tile: Tile) { private fun AppTile(tile: Tile) {
val status = when {
tile.verified -> Color(0xFF43A047)
tile.partial -> Color(0xFFF9A825)
tile.broken -> Color(0xFFD32F2F)
else -> null
}
val shape = MaterialTheme.shapes.extraLarge val shape = MaterialTheme.shapes.extraLarge
// A long press opens the app's menu (pin, close) - the same gesture everywhere apps are // A long press opens the app's menu (pin, close) - the same gesture everywhere apps are
// shown, here and on the nav bar. GsIconBox marks it in the theme's style. // shown, here and on the nav bar. GsIconBox marks it in the theme's style.
GsIconBox( GsIconBox(
onClick = tile.onClick, onClick = tile.onClick,
onLongClick = tile.onLongClick, onLongClick = tile.onLongClick,
modifier = Modifier.height(120.dp).then( modifier = Modifier.height(120.dp),
if (BADGE_STYLE == BadgeStyle.OUTLINE && status != null) Modifier.border(2.5.dp, status, shape) else Modifier,
),
colors = GsColors(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant), colors = GsColors(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant),
shape = shape, shape = shape,
) { ) {
@@ -389,16 +305,14 @@ private fun AppTile(tile: Tile) {
) { ) {
Box { Box {
TileIcon(tile) TileIcon(tile)
if (BADGE_STYLE == BadgeStyle.ON_ICON) { if (tile.broken) {
// Pinned to the icon's lower corner, ringed in the tile's colour so it // Pinned to the icon's lower corner, ringed in the tile's colour so it
// reads as sitting on top of the icon. // reads as sitting on top of the icon.
val ring = Modifier.align(Alignment.BottomEnd).offset(x = 6.dp, y = 4.dp) BrokenBadge(
.border(2.dp, MaterialTheme.colorScheme.surfaceVariant, CircleShape) 22.dp,
when { Modifier.align(Alignment.BottomEnd).offset(x = 6.dp, y = 4.dp)
tile.verified -> VerifiedBadge(22.dp, ring) .border(2.dp, MaterialTheme.colorScheme.surfaceVariant, CircleShape),
tile.broken -> BrokenBadge(22.dp, ring) )
tile.partial -> PartialBadge(22.dp, ring)
}
} }
} }
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))