mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
car: choose which apps the drawer shows, from a new Apps card on the phone
Short car screens fill up fast, and a phone can offer dozens of car apps that a driver never opens there. The drawer had no way to leave one out. The Apps card comes first under Car, with one row per tile, all ticked. Unticking a row takes the tile out of the drawer at once. It stays pinned to the nav bar if it was, and Gearslip's own tiles aren't listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37d56be59a
commit
db90c72584
8 files changed
+164
-5
No files matched your search
@@ -224,9 +224,10 @@ private fun groupOf(entry: Entry, mediaPackages: Set<String>): AppGroup {
|
||||
* 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.
|
||||
* [hidden] leaves tiles out of the drawer only: a pinned app stays on the nav bar.
|
||||
*/
|
||||
internal fun launcherOrder(all: List<Entry>, showVehicleData: Boolean, custom: List<String>): List<Entry> {
|
||||
val shown = all.filter { it.builtIn !== BuiltInApps.VehicleData || showVehicleData }
|
||||
internal fun launcherOrder(all: List<Entry>, showVehicleData: Boolean, custom: List<String>, hidden: Set<String>): List<Entry> {
|
||||
val shown = all.filter { (it.builtIn !== BuiltInApps.VehicleData || showVehicleData) && it.componentId !in hidden }
|
||||
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() }),
|
||||
@@ -290,7 +291,8 @@ fun CarLauncher() {
|
||||
}
|
||||
|
||||
val appOrder by CarSettings.appOrder.collectAsState()
|
||||
val tiles = launcherOrder(all, showVehicleData, appOrder).map(::tileOf)
|
||||
val hidden by CarSettings.hiddenApps.collectAsState()
|
||||
val tiles = launcherOrder(all, showVehicleData, appOrder, hidden).map(::tileOf)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 108.dp),
|
||||
|
||||
@@ -71,6 +71,7 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
|
||||
private const val KEY_DARK_STYLE = "car_dark_style"
|
||||
private const val KEY_PINNED = "car_pinned_apps"
|
||||
private const val KEY_APP_ORDER = "car_app_order"
|
||||
private const val KEY_HIDDEN_APPS = "car_hidden_apps"
|
||||
private const val KEY_EXPERIMENTAL_FEATURES = "car_experimental_features"
|
||||
private const val KEY_SYSTEM_SPEECH = "car_system_speech"
|
||||
private const val KEY_CAR_SENSORS = "car_sensors_enabled"
|
||||
@@ -121,6 +122,7 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
||||
val uiTheme: StateFlow<String> = uiThemeFlow
|
||||
private val pinnedFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||
private val appOrderFlow = MutableStateFlow<List<String>>(emptyList())
|
||||
private val hiddenAppsFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||
private val experimentalFeaturesFlow = MutableStateFlow(false)
|
||||
private val systemSpeechFlow = MutableStateFlow(false)
|
||||
private val carSensorsFlow = MutableStateFlow(false)
|
||||
@@ -218,6 +220,9 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
||||
/** The launcher's order once the driver has moved a tile; empty keeps the grouped default. */
|
||||
val appOrder: StateFlow<List<String>> = appOrderFlow
|
||||
|
||||
/** The tiles left out of the car's app drawer, by [Entry.componentId]. */
|
||||
val hiddenApps: StateFlow<Set<String>> = hiddenAppsFlow
|
||||
|
||||
/** 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
|
||||
@@ -353,6 +358,8 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
||||
.split(",").filter { it.isNotEmpty() }.toSet()
|
||||
appOrderFlow.value = AppSettings.getString(app, KEY_APP_ORDER, "")
|
||||
.split(",").filter { it.isNotEmpty() }
|
||||
hiddenAppsFlow.value = AppSettings.getString(app, KEY_HIDDEN_APPS, "")
|
||||
.split(",").filter { it.isNotEmpty() }.toSet()
|
||||
experimentalFeaturesFlow.value = AppSettings.getString(app, KEY_EXPERIMENTAL_FEATURES, "false") == "true"
|
||||
systemSpeechFlow.value = AppSettings.getString(app, KEY_SYSTEM_SPEECH, "false") == "true"
|
||||
carSensorsFlow.value = AppSettings.getString(app, KEY_CAR_SENSORS, "false") == "true"
|
||||
@@ -505,6 +512,12 @@ private const val KEY_CLUSTER_VIDEO = "cluster_video_enabled"
|
||||
AppSettings.putString(app, KEY_PINNED, next.joinToString(","))
|
||||
}
|
||||
|
||||
fun setAppHidden(id: String, hide: Boolean) {
|
||||
val next = if (hide) hiddenAppsFlow.value + id else hiddenAppsFlow.value - id
|
||||
hiddenAppsFlow.value = next
|
||||
AppSettings.putString(app, KEY_HIDDEN_APPS, next.joinToString(","))
|
||||
}
|
||||
|
||||
fun setAppOrder(ids: List<String>) {
|
||||
appOrderFlow.value = ids
|
||||
AppSettings.putString(app, KEY_APP_ORDER, ids.joinToString(","))
|
||||
|
||||
@@ -276,7 +276,8 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
||||
@Composable
|
||||
private fun MoveButtons(id: String) {
|
||||
val custom by CarSettings.appOrder.collectAsState()
|
||||
val order = launcherOrder(rememberAllEntries(), rememberVehicleDataShown(), custom).mapNotNull { it.componentId }
|
||||
val hidden by CarSettings.hiddenApps.collectAsState()
|
||||
val order = launcherOrder(rememberAllEntries(), rememberVehicleDataShown(), custom, hidden).mapNotNull { it.componentId }
|
||||
val index = order.indexOf(id)
|
||||
if (index < 0) return
|
||||
fun move(by: Int) {
|
||||
|
||||
@@ -4,19 +4,23 @@ import androidx.annotation.StringRes
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.seb3thehacker.gearslip.R
|
||||
import app.seb3thehacker.gearslip.car.theme.DarkStyle
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -24,6 +28,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -31,18 +36,23 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.seb3thehacker.gearslip.car.AppTheme
|
||||
import app.seb3thehacker.gearslip.car.CarApps
|
||||
import app.seb3thehacker.gearslip.car.CarSettings
|
||||
import app.seb3thehacker.gearslip.car.LauncherCache
|
||||
import app.seb3thehacker.gearslip.car.NavBarMode
|
||||
import app.seb3thehacker.gearslip.car.NightMode
|
||||
import app.seb3thehacker.gearslip.car.Units
|
||||
import app.seb3thehacker.gearslip.car.theme.GsThemes
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/** The car's setting groups, each opening its own screen from the phone's home screen. */
|
||||
enum class CarGroup(@param:StringRes val title: Int) {
|
||||
APPS(R.string.crumb_apps),
|
||||
SCREEN(R.string.car_group_screen),
|
||||
NAV_BAR(R.string.car_group_nav_bar),
|
||||
SOUND(R.string.car_group_sound),
|
||||
@@ -102,6 +112,7 @@ internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Mo
|
||||
// Read again each time Home shows, so a place saved a moment ago is listed.
|
||||
val places = androidx.compose.runtime.remember { app.seb3thehacker.gearslip.car.SavedPlaces.all(context) }
|
||||
fun summary(group: CarGroup) = when (group) {
|
||||
CarGroup.APPS -> listOf(str(R.string.car_apps_sub))
|
||||
CarGroup.SCREEN -> listOf(opens.label(open), sizes.label(scale), themes.label(theme))
|
||||
CarGroup.NAV_BAR -> listOf(navBars.label(navBar), str(if (playerLeft) R.string.summary_player_left else R.string.summary_player_right))
|
||||
CarGroup.SOUND -> listOf(
|
||||
@@ -181,7 +192,8 @@ internal fun CarGroupScreen(group: CarGroup, onBack: () -> Unit) {
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
Text(
|
||||
// The car's own Settings has no list of apps.
|
||||
if (group != CarGroup.APPS) Text(
|
||||
stringResource(R.string.car_group_same),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
@@ -216,6 +228,7 @@ private fun GroupRows(group: CarGroup) {
|
||||
val clusterVideo by CarSettings.clusterVideoEnabled.collectAsState()
|
||||
|
||||
when (group) {
|
||||
CarGroup.APPS -> AppRows()
|
||||
CarGroup.SCREEN -> {
|
||||
Choice(stringResource(R.string.car_open_on_connect), openChoices(), open, CarSettings::setOpenOnConnect)
|
||||
SettingsDivider()
|
||||
@@ -303,6 +316,28 @@ private fun GroupRows(group: CarGroup) {
|
||||
}
|
||||
}
|
||||
|
||||
/** One row per tile the installed apps put in the car's drawer, ticked while it shows there. */
|
||||
@Composable
|
||||
private fun AppRows() {
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
val apps by LauncherCache.state.collectAsState()
|
||||
val hidden by CarSettings.hiddenApps.collectAsState()
|
||||
// Its own scan, so an app installed since the last drive is here to choose.
|
||||
LaunchedEffect(Unit) { withContext(Dispatchers.IO) { LauncherCache.load(context, force = true) } }
|
||||
val rows = apps.orEmpty().mapNotNull { entry -> entry.componentId?.let { it to entry } }
|
||||
if (rows.isEmpty()) SettingsRow(stringResource(if (apps == null) R.string.car_apps_finding else R.string.car_apps_none))
|
||||
rows.forEachIndexed { i, (id, entry) ->
|
||||
val shown = id !in hidden
|
||||
if (i > 0) SettingsDivider()
|
||||
SettingsRow(
|
||||
entry.label,
|
||||
Modifier.toggleable(shown, role = Role.Checkbox) { CarSettings.setAppHidden(id, !it) },
|
||||
leading = { entry.icon?.let { Image(it, contentDescription = null, Modifier.size(40.dp)) } },
|
||||
trailing = { Checkbox(checked = shown, onCheckedChange = null) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> List<Pair<T, String>>.label(value: T): String = firstOrNull { it.first == value }?.second.orEmpty()
|
||||
|
||||
/** A setting with a few named choices: its current one shown, the rest a tap away. */
|
||||
|
||||
@@ -91,6 +91,7 @@ internal fun SettingsRow(
|
||||
icon: ImageVector? = null,
|
||||
tint: Color = MaterialTheme.colorScheme.primary,
|
||||
titleColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
leading: (@Composable () -> Unit)? = null,
|
||||
trailing: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
@@ -99,6 +100,7 @@ internal fun SettingsRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
if (icon != null) Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp))
|
||||
leading?.invoke()
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, style = MaterialTheme.typography.bodyLarge, color = titleColor)
|
||||
subtitle?.let {
|
||||
|
||||
@@ -148,6 +148,9 @@
|
||||
<string name="cert_kind_self">Self-signed: %1$s</string>
|
||||
|
||||
<!-- Car settings on the phone -->
|
||||
<string name="car_apps_sub">Choose which apps appear in the car</string>
|
||||
<string name="car_apps_finding">Finding apps…</string>
|
||||
<string name="car_apps_none">No car apps found</string>
|
||||
<string name="car_group_screen">Screen</string>
|
||||
<string name="car_group_nav_bar">Nav bar and player</string>
|
||||
<string name="car_group_sound">Sound and voice</string>
|
||||
|
||||
Reference in new issue
Block a user