mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +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,
|
start = if (LocalCardsAtEnd.current) LocalSidePanelInset.current else 0.dp,
|
||||||
end = if (LocalCardsAtEnd.current) 0.dp else LocalSidePanelInset.current,
|
end = if (LocalCardsAtEnd.current) 0.dp else LocalSidePanelInset.current,
|
||||||
).padding(24.dp),
|
).padding(24.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
val hasLast = CarSettings.lastNav.value != null
|
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 -> {
|
CarAppConnection.Phase.REJECTED, CarAppConnection.Phase.FAILED -> {
|
||||||
Text("${status.app ?: "The map app"} would not start.", style = MaterialTheme.typography.titleMedium)
|
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) }
|
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) {
|
else -> if (!hasNavApps) {
|
||||||
// Nothing to auto-select and nothing to pick from Car apps either - this is the
|
// 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
|
// 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
|
// 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.
|
// than sit on a "starting" message that never resolves.
|
||||||
Text("Map stopped.", style = MaterialTheme.typography.titleMedium)
|
Text("Map stopped", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
GsButton(onClick = { scope.launch { CarServices.reconnectNav(frame) } }, tone = GsTone.Tonal) { Text("Reopen") }
|
Text(
|
||||||
GsButton(onClick = { navigator.apps() }, tone = GsTone.Tonal) { Text("Car apps") }
|
"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 {
|
} else {
|
||||||
// Autostart is still picking one - a navigation app is installed, so this resolves
|
// Autostart is still picking one - a navigation app is installed, so this resolves
|
||||||
// to a "Starting..." state itself, in the next frame or two.
|
// 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,
|
* 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
|
* 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. */
|
/** 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 }
|
private enum class AppGroup { PHONE, MAPS, MUSIC, MESSAGING, WEB, SCREEN_SHARING, OTHER, 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 fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
private fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
||||||
entry.builtIn?.let {
|
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
|
* The launcher's tiles in the order they're drawn: grouped by [AppGroup], alphabetical inside a
|
||||||
* with Settings last of all, then with the driver's own moves from [custom] laid over it. Moved
|
* group with Settings last of all, then with the driver's own moves from [custom] laid over it.
|
||||||
* apps only trade places among themselves and inside their own heading, so an app installed later
|
* Moved apps only trade places among themselves, so an app installed later still lands in its group.
|
||||||
* still lands in its group and nothing ends up under the wrong heading.
|
|
||||||
*/
|
*/
|
||||||
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 shown = all.filter { it.builtIn !== BuiltInApps.VehicleData || showVehicleData }
|
||||||
val mediaPackages = shown.mapNotNull { it.media?.component?.packageName }.toSet()
|
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() }),
|
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 }
|
val rank = custom.withIndex().associate { (i, id) -> id to i }
|
||||||
fun Entry.rank() = componentId?.let { rank[it] }
|
fun Entry.rank() = componentId?.let { rank[it] }
|
||||||
return sorted.groupBy { groupOf(it, mediaPackages).heading }.map { (heading, entries) ->
|
val moved = grouped.filter { it.rank() != null }.sortedBy { it.rank() }.iterator()
|
||||||
val moved = entries.filter { it.rank() != null }.sortedBy { it.rank() }.iterator()
|
return grouped.map { if (it.rank() != null) moved.next() else it }
|
||||||
LauncherSection(heading, entries.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. */
|
/** An app by the id a pin or the running list knows it by, built-in or installed. */
|
||||||
internal fun findEntry(id: String): Entry? =
|
internal fun findEntry(id: String): Entry? =
|
||||||
BuiltInApps.entries.find { it.componentId == id } ?: LauncherCache.entries?.find { it.componentId == id }
|
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 appOrder by CarSettings.appOrder.collectAsState()
|
||||||
val sections = launcherSections(all, showVehicleData, appOrder)
|
val tiles = launcherOrder(all, showVehicleData, appOrder).map(::tileOf)
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(minSize = 116.dp),
|
columns = GridCells.Adaptive(minSize = 108.dp),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 8.dp, bottom = 20.dp),
|
contentPadding = PaddingValues(20.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
sections.forEach { section ->
|
items(tiles) { AppTile(it) }
|
||||||
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) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,8 +320,8 @@ private fun AppTile(tile: Tile) {
|
|||||||
GsIconBox(
|
GsIconBox(
|
||||||
onClick = tile.onClick,
|
onClick = tile.onClick,
|
||||||
onLongClick = tile.onLongClick,
|
onLongClick = tile.onLongClick,
|
||||||
modifier = Modifier.height(120.dp).gsPanelShadow(shape),
|
modifier = Modifier.height(120.dp),
|
||||||
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface),
|
colors = GsColors(MaterialTheme.colorScheme.surfaceVariant, MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
shape = shape,
|
shape = shape,
|
||||||
) {
|
) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
@@ -368,7 +338,7 @@ private fun AppTile(tile: Tile) {
|
|||||||
BrokenBadge(
|
BrokenBadge(
|
||||||
22.dp,
|
22.dp,
|
||||||
Modifier.align(Alignment.BottomEnd).offset(x = 6.dp, y = 4.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
|
@Composable
|
||||||
private fun TileIcon(tile: Tile) {
|
private fun TileIcon(tile: Tile) {
|
||||||
when {
|
when {
|
||||||
tile.icon != null -> Image(tile.icon, 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(56.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
|
* The player beside the browse list, the queue and the lyrics: the cover as big as the controls
|
||||||
* Home, the cover taking whatever the title and controls leave, so every tab keeps the song in
|
* leave room for, with the title and artist beside it rather than under it, so the cover gets the
|
||||||
* sight and the controls in one place.
|
* height; then the seek bar and the controls along the bottom.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompactPlayer(now: NowPlaying, media: CarMedia, art: android.graphics.Bitmap?, modifier: Modifier) {
|
private fun CompactPlayer(now: NowPlaying, media: CarMedia, art: android.graphics.Bitmap?, modifier: Modifier) {
|
||||||
val shape = RoundedCornerShape(24.dp)
|
Column(modifier) {
|
||||||
Surface(modifier.gsPanelShadow(shape), shape = shape, color = MaterialTheme.colorScheme.surfaceContainer) {
|
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
|
||||||
AlbumGlow(art, Modifier.fillMaxSize())
|
val cover = minOf(maxHeight, maxWidth * 0.62f).coerceAtLeast(0.dp)
|
||||||
Column(Modifier.fillMaxSize().padding(14.dp), horizontalAlignment = Alignment.CenterHorizontally) {
|
Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.Bottom) {
|
||||||
CoverArt(art, Modifier.weight(1f).fillMaxWidth(), Alignment.BottomCenter)
|
CoverArt(art, Modifier.size(cover))
|
||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
Text(
|
Column(Modifier.weight(1f).padding(bottom = 4.dp)) {
|
||||||
now.title.ifEmpty { "Nothing playing" },
|
Text(
|
||||||
style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold,
|
now.title.ifEmpty { "Nothing playing" },
|
||||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold,
|
||||||
)
|
maxLines = 3, overflow = TextOverflow.Ellipsis,
|
||||||
Text(
|
)
|
||||||
now.artist,
|
Text(
|
||||||
style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
now.artist,
|
||||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||||
PlayerProgress(now, media, Modifier.fillMaxWidth().padding(vertical = 2.dp), showTimes = false)
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
TransportRow(now, media, skip = 52.dp, modifier = Modifier.fillMaxWidth())
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
}
|
||||||
PlayModeRow(now, media, size = 46.dp, modifier = Modifier.fillMaxWidth())
|
}
|
||||||
}
|
}
|
||||||
|
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
|
@Composable
|
||||||
private fun MoveButtons(id: String) {
|
private fun MoveButtons(id: String) {
|
||||||
val custom by CarSettings.appOrder.collectAsState()
|
val custom by CarSettings.appOrder.collectAsState()
|
||||||
// Only within the app's own heading: past its edge it would land under another one.
|
val order = launcherOrder(rememberAllEntries(), rememberVehicleDataShown(), custom).mapNotNull { it.componentId }
|
||||||
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 index = order.indexOf(id)
|
val index = order.indexOf(id)
|
||||||
if (index < 0) return
|
if (index < 0) return
|
||||||
fun move(by: Int) {
|
fun move(by: Int) {
|
||||||
val next = order.toMutableList()
|
val next = order.toMutableList()
|
||||||
java.util.Collections.swap(next, index, index + by)
|
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)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
GsButton(
|
GsButton(
|
||||||
|
|||||||
Reference in new issue
Block a user