mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car: undo the launcher's headings, the media player beside the tabs drops its card for a bigger cover with the title beside it, and the map-stopped buttons grow
This commit is contained in:
1 parent
324112ca8b
commit
74f557524f
4 files changed
+74
-83
No files matched your search
@@ -161,7 +161,7 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
||||
start = if (LocalCardsAtEnd.current) LocalSidePanelInset.current else 0.dp,
|
||||
end = if (LocalCardsAtEnd.current) 0.dp else LocalSidePanelInset.current,
|
||||
).padding(24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val hasLast = CarSettings.lastNav.value != null
|
||||
@@ -173,7 +173,8 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
||||
CarAppConnection.Phase.REJECTED, CarAppConnection.Phase.FAILED -> {
|
||||
Text("${status.app ?: "The map app"} would not start.", style = MaterialTheme.typography.titleMedium)
|
||||
status.detail?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||
GsButton(onClick = navigator::apps, tone = GsTone.Tonal) { Text("Choose another app") }
|
||||
Spacer(Modifier.height(8.dp))
|
||||
StatusButton("Choose another app", GsTone.Primary, navigator::apps)
|
||||
}
|
||||
else -> if (!hasNavApps) {
|
||||
// Nothing to auto-select and nothing to pick from Car apps either - this is the
|
||||
@@ -188,9 +189,18 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
||||
// IDLE with a last app on record means the driver stopped it themselves - it will
|
||||
// not come back on its own, so the screen must offer a way to bring it back rather
|
||||
// than sit on a "starting" message that never resolves.
|
||||
Text("Map stopped.", style = MaterialTheme.typography.titleMedium)
|
||||
GsButton(onClick = { scope.launch { CarServices.reconnectNav(frame) } }, tone = GsTone.Tonal) { Text("Reopen") }
|
||||
GsButton(onClick = { navigator.apps() }, tone = GsTone.Tonal) { Text("Car apps") }
|
||||
Text("Map stopped", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
"It stays closed until you open it again.",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
// Side by side, the same size, the likely one lit: one clear choice, not a stack.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||
StatusButton("Reopen the map", GsTone.Primary) { scope.launch { CarServices.reconnectNav(frame) } }
|
||||
StatusButton("Car apps", GsTone.Tonal) { navigator.apps() }
|
||||
}
|
||||
} else {
|
||||
// Autostart is still picking one - a navigation app is installed, so this resolves
|
||||
// to a "Starting..." state itself, in the next frame or two.
|
||||
@@ -199,6 +209,14 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
||||
}
|
||||
}
|
||||
|
||||
/** A big, even button for the map pane's few choices. */
|
||||
@Composable
|
||||
private fun StatusButton(label: String, tone: GsTone, onClick: () -> Unit) {
|
||||
GsButton(onClick = onClick, tone = tone, modifier = Modifier.width(220.dp).height(64.dp)) {
|
||||
Text(label, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The player, docked beside the map: the art as large as it fits, then title, progress, transport,
|
||||
* and like, shuffle, repeat and lyrics along the bottom. While it's here the nav bar's pill steps aside on Home, so the controls never show
|
||||
|
||||
@@ -189,20 +189,7 @@ internal fun rememberAllEntries(): List<Entry> {
|
||||
}
|
||||
|
||||
/** The launcher's groups, in the order they're laid out. */
|
||||
private enum class AppGroup { MAPS, MUSIC, PHONE, MESSAGING, OTHER, WEB, SCREEN_SHARING, SETTINGS, EXIT }
|
||||
|
||||
/** The heading a group's tiles sit under; calls and messages share one, as do Gearslip's own. */
|
||||
private val AppGroup.heading: String
|
||||
get() = when (this) {
|
||||
AppGroup.MAPS -> "Navigation"
|
||||
AppGroup.MUSIC -> "Media"
|
||||
AppGroup.PHONE, AppGroup.MESSAGING -> "Calls and messages"
|
||||
AppGroup.OTHER -> "Other apps"
|
||||
else -> "Gearslip"
|
||||
}
|
||||
|
||||
/** One heading on the launcher and the tiles under it, in order. */
|
||||
internal class LauncherSection(val heading: String, val entries: List<Entry>)
|
||||
private enum class AppGroup { PHONE, MAPS, MUSIC, MESSAGING, WEB, SCREEN_SHARING, OTHER, SETTINGS, EXIT }
|
||||
|
||||
private fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
||||
entry.builtIn?.let {
|
||||
@@ -231,29 +218,23 @@ private fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
||||
}
|
||||
|
||||
/**
|
||||
* The launcher's tiles under their headings: grouped by [AppGroup], alphabetical inside a group
|
||||
* with Settings last of all, then with the driver's own moves from [custom] laid over it. Moved
|
||||
* apps only trade places among themselves and inside their own heading, so an app installed later
|
||||
* still lands in its group and nothing ends up under the wrong heading.
|
||||
* The launcher's tiles in the order they're drawn: grouped by [AppGroup], alphabetical inside a
|
||||
* group with Settings last of all, then with the driver's own moves from [custom] laid over it.
|
||||
* Moved apps only trade places among themselves, so an app installed later still lands in its group.
|
||||
*/
|
||||
internal fun launcherSections(all: List<Entry>, showVehicleData: Boolean, custom: List<String>): List<LauncherSection> {
|
||||
internal fun launcherOrder(all: List<Entry>, showVehicleData: Boolean, custom: List<String>): List<Entry> {
|
||||
val shown = all.filter { it.builtIn !== BuiltInApps.VehicleData || showVehicleData }
|
||||
val mediaPackages = shown.mapNotNull { it.media?.component?.packageName }.toSet()
|
||||
val sorted = shown.sortedWith(
|
||||
val grouped = shown.sortedWith(
|
||||
compareBy<Entry>({ groupOf(it, mediaPackages) }, { it.builtIn === BuiltInApps.Settings }, { it.label.lowercase() }),
|
||||
)
|
||||
if (custom.isEmpty()) return grouped
|
||||
val rank = custom.withIndex().associate { (i, id) -> id to i }
|
||||
fun Entry.rank() = componentId?.let { rank[it] }
|
||||
return sorted.groupBy { groupOf(it, mediaPackages).heading }.map { (heading, entries) ->
|
||||
val moved = entries.filter { it.rank() != null }.sortedBy { it.rank() }.iterator()
|
||||
LauncherSection(heading, entries.map { if (it.rank() != null) moved.next() else it })
|
||||
}
|
||||
val moved = grouped.filter { it.rank() != null }.sortedBy { it.rank() }.iterator()
|
||||
return grouped.map { if (it.rank() != null) moved.next() else it }
|
||||
}
|
||||
|
||||
/** The launcher's tiles in the order they're drawn, headings aside. */
|
||||
internal fun launcherOrder(all: List<Entry>, showVehicleData: Boolean, custom: List<String>): List<Entry> =
|
||||
launcherSections(all, showVehicleData, custom).flatMap { it.entries }
|
||||
|
||||
/** An app by the id a pin or the running list knows it by, built-in or installed. */
|
||||
internal fun findEntry(id: String): Entry? =
|
||||
BuiltInApps.entries.find { it.componentId == id } ?: LauncherCache.entries?.find { it.componentId == id }
|
||||
@@ -306,27 +287,16 @@ fun CarLauncher() {
|
||||
}
|
||||
|
||||
val appOrder by CarSettings.appOrder.collectAsState()
|
||||
val sections = launcherSections(all, showVehicleData, appOrder)
|
||||
val tiles = launcherOrder(all, showVehicleData, appOrder).map(::tileOf)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 116.dp),
|
||||
columns = GridCells.Adaptive(minSize = 108.dp),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 20.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
||||
contentPadding = PaddingValues(20.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
sections.forEach { section ->
|
||||
item(span = { GridItemSpan(maxLineSpan) }, contentType = "heading") {
|
||||
Text(
|
||||
section.heading,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 4.dp, top = 12.dp),
|
||||
)
|
||||
}
|
||||
items(section.entries.map(::tileOf)) { AppTile(it) }
|
||||
}
|
||||
items(tiles) { AppTile(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,8 +320,8 @@ private fun AppTile(tile: Tile) {
|
||||
GsIconBox(
|
||||
onClick = tile.onClick,
|
||||
onLongClick = tile.onLongClick,
|
||||
modifier = Modifier.height(120.dp).gsPanelShadow(shape),
|
||||
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface),
|
||||
modifier = Modifier.height(120.dp),
|
||||
colors = GsColors(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant),
|
||||
shape = shape,
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
@@ -368,7 +338,7 @@ private fun AppTile(tile: Tile) {
|
||||
BrokenBadge(
|
||||
22.dp,
|
||||
Modifier.align(Alignment.BottomEnd).offset(x = 6.dp, y = 4.dp)
|
||||
.border(2.dp, MaterialTheme.colorScheme.surfaceContainerHigh, CircleShape),
|
||||
.border(2.dp, MaterialTheme.colorScheme.surfaceVariant, CircleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -382,8 +352,8 @@ private fun AppTile(tile: Tile) {
|
||||
@Composable
|
||||
private fun TileIcon(tile: Tile) {
|
||||
when {
|
||||
tile.icon != null -> Image(tile.icon, contentDescription = null, modifier = Modifier.size(56.dp))
|
||||
tile.glyph != null -> Icon(tile.glyph, contentDescription = null, modifier = Modifier.size(56.dp))
|
||||
tile.icon != null -> Image(tile.icon, contentDescription = null, modifier = Modifier.size(52.dp))
|
||||
tile.glyph != null -> Icon(tile.glyph, contentDescription = null, modifier = Modifier.size(52.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -707,33 +707,39 @@ private fun NowPlayingControls(now: NowPlaying, media: CarMedia, modifier: Modif
|
||||
}
|
||||
|
||||
/**
|
||||
* The player beside the browse list, the queue and the lyrics: the same card as beside the map on
|
||||
* Home, the cover taking whatever the title and controls leave, so every tab keeps the song in
|
||||
* sight and the controls in one place.
|
||||
* The player beside the browse list, the queue and the lyrics: the cover as big as the controls
|
||||
* leave room for, with the title and artist beside it rather than under it, so the cover gets the
|
||||
* height; then the seek bar and the controls along the bottom.
|
||||
*/
|
||||
@Composable
|
||||
private fun CompactPlayer(now: NowPlaying, media: CarMedia, art: android.graphics.Bitmap?, modifier: Modifier) {
|
||||
val shape = RoundedCornerShape(24.dp)
|
||||
Surface(modifier.gsPanelShadow(shape), shape = shape, color = MaterialTheme.colorScheme.surfaceContainer) {
|
||||
AlbumGlow(art, Modifier.fillMaxSize())
|
||||
Column(Modifier.fillMaxSize().padding(14.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CoverArt(art, Modifier.weight(1f).fillMaxWidth(), Alignment.BottomCenter)
|
||||
Spacer(Modifier.height(10.dp))
|
||||
Text(
|
||||
now.title.ifEmpty { "Nothing playing" },
|
||||
style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
now.artist,
|
||||
style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
PlayerProgress(now, media, Modifier.fillMaxWidth().padding(vertical = 2.dp), showTimes = false)
|
||||
TransportRow(now, media, skip = 52.dp, modifier = Modifier.fillMaxWidth())
|
||||
Spacer(Modifier.height(8.dp))
|
||||
PlayModeRow(now, media, size = 46.dp, modifier = Modifier.fillMaxWidth())
|
||||
Column(modifier) {
|
||||
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
|
||||
val cover = minOf(maxHeight, maxWidth * 0.62f).coerceAtLeast(0.dp)
|
||||
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.Bottom) {
|
||||
CoverArt(art, Modifier.size(cover))
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Column(Modifier.weight(1f).padding(bottom = 4.dp)) {
|
||||
Text(
|
||||
now.title.ifEmpty { "Nothing playing" },
|
||||
style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold,
|
||||
maxLines = 3, overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text(
|
||||
now.artist,
|
||||
style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
PlayerProgress(now, media, Modifier.fillMaxWidth())
|
||||
Spacer(Modifier.height(6.dp))
|
||||
TransportRow(now, media, skip = 54.dp, modifier = Modifier.fillMaxWidth())
|
||||
Spacer(Modifier.height(10.dp))
|
||||
PlayModeRow(now, media, size = 48.dp, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -267,16 +267,13 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
||||
@Composable
|
||||
private fun MoveButtons(id: String) {
|
||||
val custom by CarSettings.appOrder.collectAsState()
|
||||
// Only within the app's own heading: past its edge it would land under another one.
|
||||
val sections = launcherSections(rememberAllEntries(), rememberVehicleDataShown(), custom)
|
||||
val section = sections.find { s -> s.entries.any { it.componentId == id } } ?: return
|
||||
val order = section.entries.mapNotNull { it.componentId }
|
||||
val order = launcherOrder(rememberAllEntries(), rememberVehicleDataShown(), custom).mapNotNull { it.componentId }
|
||||
val index = order.indexOf(id)
|
||||
if (index < 0) return
|
||||
fun move(by: Int) {
|
||||
val next = order.toMutableList()
|
||||
java.util.Collections.swap(next, index, index + by)
|
||||
CarSettings.setAppOrder(sections.flatMap { s -> if (s === section) next else s.entries.mapNotNull { it.componentId } })
|
||||
CarSettings.setAppOrder(next)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
GsButton(
|
||||
|
||||
Reference in new issue
Block a user