mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
app organization and movement options
This commit is contained in:
1 parent
a2d9ed7008
commit
ff8a6a4cff
3 files changed
+100
-8
No files matched your search
@@ -163,6 +163,50 @@ internal fun rememberAllEntries(): List<Entry> {
|
||||
return remember(installed) { BuiltInApps.entries + installed.orEmpty() }
|
||||
}
|
||||
|
||||
/** The launcher's groups, in the order they're laid out. */
|
||||
private enum class AppGroup { PHONE, MAPS, MUSIC, MESSAGING, WEB, SCREEN_SHARING, OTHER, SETTINGS }
|
||||
|
||||
private fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
||||
entry.builtIn?.let {
|
||||
return when (it) {
|
||||
BuiltInApps.Phone -> AppGroup.PHONE
|
||||
BuiltInApps.Web -> AppGroup.WEB
|
||||
BuiltInApps.ScreenSharing -> AppGroup.SCREEN_SHARING
|
||||
else -> AppGroup.SETTINGS
|
||||
}
|
||||
}
|
||||
if (entry.media != null) return AppGroup.MUSIC
|
||||
if (entry.messaging != null) return AppGroup.MESSAGING
|
||||
val app = entry.template ?: return AppGroup.OTHER
|
||||
// A media app's "· Browse" tile sits next to its player.
|
||||
if (app.component.packageName in mediaPackages) return AppGroup.MUSIC
|
||||
return when (app.kind) {
|
||||
"calling" -> AppGroup.PHONE
|
||||
"navigation", "poi", "parking", "charging" -> AppGroup.MAPS
|
||||
"messaging" -> AppGroup.MESSAGING
|
||||
"settings" -> AppGroup.SETTINGS
|
||||
else -> AppGroup.OTHER
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 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] }
|
||||
val moved = grouped.filter { it.rank() != null }.sortedBy { it.rank() }.iterator()
|
||||
return grouped.map { if (it.rank() != null) moved.next() else it }
|
||||
}
|
||||
|
||||
/** 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 }
|
||||
@@ -190,7 +234,7 @@ internal fun launchEntry(entry: Entry, navigator: CarNavigator, frame: CarEnviro
|
||||
entry.messaging?.let { navigator.messages(it.packageName, it.label) }
|
||||
}
|
||||
|
||||
/** The app launcher: Web and Screen sharing, every car app the phone has, then Settings, as an icon grid. */
|
||||
/** The app launcher: every app as an icon grid, grouped Phone, Maps, Music, Messaging, Web, Screen sharing, Settings. */
|
||||
@Composable
|
||||
fun CarLauncher() {
|
||||
val navigator = LocalCarNavigator.current
|
||||
@@ -216,11 +260,8 @@ fun CarLauncher() {
|
||||
) { openApp(entry, id?.let { running[it] }, navigator, frame) }
|
||||
}
|
||||
|
||||
// Gearslip's own apps first, then everything installed, then Settings last where it's expected.
|
||||
val builtIns = all.filter { it.builtIn != null && (it.builtIn !== BuiltInApps.VehicleData || showVehicleData) }
|
||||
val tiles = builtIns.filter { it.builtIn !== BuiltInApps.Settings }.map(::tileOf) +
|
||||
all.filter { it.builtIn == null }.map(::tileOf) +
|
||||
builtIns.filter { it.builtIn === BuiltInApps.Settings }.map(::tileOf)
|
||||
val appOrder by CarSettings.appOrder.collectAsState()
|
||||
val tiles = launcherOrder(all, showVehicleData, appOrder).map(::tileOf)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 108.dp),
|
||||
|
||||
@@ -34,6 +34,7 @@ object CarSettings {
|
||||
private const val KEY_THEME = "car_app_theme"
|
||||
private const val KEY_UI_THEME = "car_ui_theme"
|
||||
private const val KEY_PINNED = "car_pinned_apps"
|
||||
private const val KEY_APP_ORDER = "car_app_order"
|
||||
private const val KEY_EXPERIMENTAL_FEATURES = "car_experimental_features"
|
||||
private const val KEY_VOICE_ASSISTANT = "car_voice_assistant"
|
||||
private const val KEY_CAR_SENSORS = "car_sensors_enabled"
|
||||
@@ -66,6 +67,7 @@ object CarSettings {
|
||||
/** Id of the [GsTheme] controls are drawn in; an unknown id falls back to the default. */
|
||||
val uiTheme: StateFlow<String> = uiThemeFlow
|
||||
private val pinnedFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||
private val appOrderFlow = MutableStateFlow<List<String>>(emptyList())
|
||||
private val experimentalFeaturesFlow = MutableStateFlow(false)
|
||||
private val voiceAssistantFlow = MutableStateFlow(false)
|
||||
private val carSensorsFlow = MutableStateFlow(false)
|
||||
@@ -120,6 +122,9 @@ object CarSettings {
|
||||
/** Flattened component names of the apps long-pressed onto the nav bar as shortcuts. */
|
||||
val pinnedApps: StateFlow<Set<String>> = pinnedFlow
|
||||
|
||||
/** The launcher's order once the driver has moved a tile; empty keeps the grouped default. */
|
||||
val appOrder: StateFlow<List<String>> = appOrderFlow
|
||||
|
||||
/** Flattened component names of the map and media apps used last, brought back on connect. */
|
||||
val lastNav: StateFlow<String?> = lastNavFlow
|
||||
val lastMedia: StateFlow<String?> = lastMediaFlow
|
||||
@@ -197,6 +202,8 @@ object CarSettings {
|
||||
}.getOrDefault(NightMode.AUTO)
|
||||
pinnedFlow.value = AppSettings.getString(app, KEY_PINNED, "")
|
||||
.split(",").filter { it.isNotEmpty() }.toSet()
|
||||
appOrderFlow.value = AppSettings.getString(app, KEY_APP_ORDER, "")
|
||||
.split(",").filter { it.isNotEmpty() }
|
||||
experimentalFeaturesFlow.value = AppSettings.getString(app, KEY_EXPERIMENTAL_FEATURES, "false") == "true"
|
||||
voiceAssistantFlow.value = AppSettings.getString(app, KEY_VOICE_ASSISTANT, "false") == "true"
|
||||
carSensorsFlow.value = AppSettings.getString(app, KEY_CAR_SENSORS, "false") == "true"
|
||||
@@ -286,4 +293,9 @@ object CarSettings {
|
||||
pinnedFlow.value = next
|
||||
AppSettings.putString(app, KEY_PINNED, next.joinToString(","))
|
||||
}
|
||||
|
||||
fun setAppOrder(ids: List<String>) {
|
||||
appOrderFlow.value = ids
|
||||
AppSettings.putString(app, KEY_APP_ORDER, ids.joinToString(","))
|
||||
}
|
||||
}
|
||||
@@ -148,8 +148,8 @@ internal fun closeApp(slot: RunningSlot, navigator: CarNavigator) {
|
||||
}
|
||||
|
||||
/**
|
||||
* What a long press on an app offers - pin it to or unpin it from the nav bar, and close it if
|
||||
* it's open - as a card in the middle of the screen with car-sized buttons, over a scrim that
|
||||
* What a long press on an app offers - pin it to or unpin it from the nav bar, move it along the
|
||||
* launcher, and close it if it's open - as a card in the middle of the screen with car-sized buttons, over a scrim that
|
||||
* dismisses it. Drawn in the car UI's own layout rather than a system popup window, which the
|
||||
* car's virtual display has no reason to host well.
|
||||
*/
|
||||
@@ -218,6 +218,7 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
if (navigator.current == CarScreen.Apps) MoveButtons(id)
|
||||
if (slot != null) {
|
||||
GsButton(
|
||||
onClick = {
|
||||
@@ -237,3 +238,41 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the app one place left or right on the launcher. The menu stays open so a tile can be
|
||||
* walked several places, and the grid behind the scrim shows where it went.
|
||||
*/
|
||||
@Composable
|
||||
private fun MoveButtons(id: String) {
|
||||
val custom by CarSettings.appOrder.collectAsState()
|
||||
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(next)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
GsButton(
|
||||
onClick = { move(-1) },
|
||||
tone = GsTone.Tonal,
|
||||
enabled = index > 0,
|
||||
modifier = Modifier.weight(1f).height(56.dp),
|
||||
) { Text("Move left", style = MaterialTheme.typography.titleMedium) }
|
||||
GsButton(
|
||||
onClick = { move(1) },
|
||||
tone = GsTone.Tonal,
|
||||
enabled = index < order.lastIndex,
|
||||
modifier = Modifier.weight(1f).height(56.dp),
|
||||
) { Text("Move right", style = MaterialTheme.typography.titleMedium) }
|
||||
}
|
||||
if (custom.isNotEmpty()) {
|
||||
GsButton(
|
||||
onClick = { CarSettings.setAppOrder(emptyList()) },
|
||||
tone = GsTone.Neutral,
|
||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||
) { Text("Reset app order", style = MaterialTheme.typography.titleMedium) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user