car: a Dark mode setting with black, black and colour, deep colour and the old grey, black by default

This commit is contained in:
Seb3thehacker committed 2026-10-26 00:47:56 -07:00
1 parent 894712c38d
commit 08eef32e59
5 files changed
+97 -2

No files matched your search

@@ -1,5 +1,6 @@
package app.seb3thehacker.gearslip.car
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import android.content.Context
import app.seb3thehacker.gearslip.AppSettings
import app.seb3thehacker.gearslip.audio.CarAudio
@@ -67,6 +68,7 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
private const val KEY_MORE_LYRICS = "car_more_lyrics"
private const val KEY_THEME = "car_app_theme"
private const val KEY_UI_THEME = "car_ui_theme"
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_EXPERIMENTAL_FEATURES = "car_experimental_features"
@@ -108,6 +110,10 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
private val moreLyricsFlow = MutableStateFlow(true)
private val themeFlow = MutableStateFlow(AppTheme.PHONE)
val appTheme: StateFlow<AppTheme> = themeFlow
private val darkStyleFlow = MutableStateFlow(DarkStyle.BLACK)
/** How dark the car's dark mode goes; black until the driver picks another. */
val darkStyle: StateFlow<DarkStyle> = darkStyleFlow
private val uiThemeFlow = MutableStateFlow(GsThemes.default.id)
/** Id of the [GsTheme] controls are drawn in; an unknown id falls back to the default. */
@@ -317,6 +323,9 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
themeFlow.value = runCatching {
AppTheme.valueOf(AppSettings.getString(app, KEY_THEME, AppTheme.PHONE.name))
}.getOrDefault(AppTheme.PHONE)
darkStyleFlow.value = runCatching {
DarkStyle.valueOf(AppSettings.getString(app, KEY_DARK_STYLE, DarkStyle.BLACK.name))
}.getOrDefault(DarkStyle.BLACK)
uiThemeFlow.value = AppSettings.getString(app, KEY_UI_THEME, GsThemes.default.id)
autoplayFlow.value = AppSettings.getString(app, KEY_AUTOPLAY, "false") == "true"
playerTitleFlow.value = AppSettings.getString(app, KEY_PLAYER_TITLE, "false") == "true"
@@ -384,6 +393,11 @@ private const val KEY_LOCATION_WARNING = "car_location_warning"
AppSettings.putString(app, KEY_THEME, theme.name)
}
fun setDarkStyle(style: DarkStyle) {
darkStyleFlow.value = style
AppSettings.putString(app, KEY_DARK_STYLE, style.name)
}
fun setUiTheme(id: String) {
uiThemeFlow.value = id
AppSettings.putString(app, KEY_UI_THEME, id)
@@ -1,6 +1,7 @@
package app.seb3thehacker.gearslip.car
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import app.seb3thehacker.gearslip.car.theme.*
import android.os.Build
import androidx.compose.foundation.layout.Arrangement
@@ -68,6 +69,7 @@ fun CarSettingsScreen() {
val moreLyrics by CarSettings.moreLyrics.collectAsState()
val theme by CarSettings.appTheme.collectAsState()
val uiTheme by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
val experimentalFeatures by CarSettings.experimentalFeaturesEnabled.collectAsState()
val carSensors by CarSettings.carSensorsEnabled.collectAsState()
val adaptiveVideo by CarSettings.adaptiveVideoEnabled.collectAsState()
@@ -116,6 +118,19 @@ fun CarSettingsScreen() {
) { CarSettings.setAppTheme(it) }
}
if (theme != AppTheme.LIGHT) item {
ChoiceRow(
"Dark mode",
listOf(
DarkStyle.BLACK to "Black",
DarkStyle.BLACK_COLOUR to "Black and colour",
DarkStyle.DEEP_COLOUR to "Deep colour",
DarkStyle.GREY to "Grey",
),
darkStyle,
) { CarSettings.setDarkStyle(it) }
}
item {
ChoiceRow(
"Theme",
@@ -110,6 +110,7 @@ fun CarUi() {
val phoneDark by CarEnvironment.phoneDark.collectAsState()
val appTheme by CarSettings.appTheme.collectAsState()
val uiThemeId by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
val mediaApp by CarServices.mediaApp.collectAsState()
val darkOutside by CarEnvironment.darkOutside.collectAsState()
val appContext = LocalContext.current.applicationContext
@@ -118,7 +119,7 @@ fun CarUi() {
AppTheme.LIGHT -> false
AppTheme.DARK -> true
}
val colors = (if (dark) dynamicDarkColorScheme(appContext) else dynamicLightColorScheme(appContext)).withTintedSurfaces(dark)
val colors = (if (dark) dynamicDarkColorScheme(appContext) else dynamicLightColorScheme(appContext)).withTintedSurfaces(dark, darkStyle)
val base = LocalDensity.current
val insets by CarEnvironment.insets.collectAsState()
val navigator = remember { CarNavigator(CarSettings.openOnConnect.value) }
@@ -3,6 +3,7 @@ package app.seb3thehacker.gearslip.car.theme
import androidx.compose.material3.ColorScheme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.lerp
/**
* The phone's Material You colours, with its accent washed over the surfaces. Material You's own
@@ -11,7 +12,8 @@ import androidx.compose.ui.graphics.compositeOver
* Material 3's surface tint does, and it changes with the wallpaper like everything else.
* Higher containers take more of the tint, so cards still stand off the background.
*/
fun ColorScheme.withTintedSurfaces(dark: Boolean): ColorScheme {
fun ColorScheme.withTintedSurfaces(dark: Boolean, style: DarkStyle = DarkStyle.GREY): ColorScheme {
if (dark && style != DarkStyle.GREY) return withDarkStyle(style)
val base = if (dark) 0.03f else 0.02f
fun tint(own: Color, step: Int) = primary.copy(alpha = base + step * (if (dark) 0.015f else 0.01f)).compositeOver(own)
return copy(
@@ -27,3 +29,54 @@ fun ColorScheme.withTintedSurfaces(dark: Boolean): ColorScheme {
surfaceVariant = tint(surfaceVariant, 3),
)
}
/**
* How dark the car's dark mode goes. [GREY] is Material You's own dark, with the tint above;
* the others are darker, and the two colour ones take their hue from the phone's accent.
*/
enum class DarkStyle { GREY, BLACK, DEEP_COLOUR, BLACK_COLOUR }
/**
* The surfaces rebuilt at [style]'s brightness, from the background up to the highest container.
* The levels are 0-255 greys; each surface keeps Material You's own slight hue at that level.
*/
private fun ColorScheme.withDarkStyle(style: DarkStyle): ColorScheme {
val levels = when (style) {
DarkStyle.BLACK -> listOf(0f, 12f, 17f, 25f, 34f)
DarkStyle.DEEP_COLOUR -> listOf(11f, 19f, 24f, 32f, 41f)
else -> listOf(0f, 17f, 22f, 30f, 39f)
}
val colour = style != DarkStyle.BLACK
// The accent's saturated container colour, not the pastel accent itself: tinting with the
// pastel only makes a surface lighter, where this one gives it colour at the same brightness.
fun at(own: Color, step: Int): Color {
val level = levels[step] / 255f
if (level == 0f) return Color.Black
val neutral = own.scaledTo(level)
return if (colour) {
lerp(neutral, primaryContainer.scaledTo(level), (0.6f + 0.08f * step).coerceAtMost(0.95f))
} else {
primary.copy(alpha = 0.02f + 0.015f * step).compositeOver(neutral)
}
}
return copy(
background = at(background, 0),
surface = at(surface, 0),
surfaceDim = at(surfaceDim, 0),
surfaceContainerLowest = at(surfaceContainerLowest, 0),
surfaceContainerLow = at(surfaceContainerLow, 1),
surfaceContainer = at(surfaceContainer, 2),
surfaceContainerHigh = at(surfaceContainerHigh, 3),
surfaceContainerHighest = at(surfaceContainerHighest, 4),
surfaceBright = at(surfaceBright, 4),
surfaceVariant = at(surfaceVariant, 3),
)
}
/** This colour's hue at an average brightness of [level], 0 to 1. */
private fun Color.scaledTo(level: Float): Color {
val mean = (red + green + blue) / 3f
if (mean <= 0f) return Color(level, level, level)
val k = level / mean
return Color((red * k).coerceAtMost(1f), (green * k).coerceAtMost(1f), (blue * k).coerceAtMost(1f))
}
@@ -1,5 +1,6 @@
package app.seb3thehacker.gearslip.ui
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -46,6 +47,12 @@ enum class CarGroup(val title: String) {
private val sizeChoices = listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large")
private val themeChoices = listOf(AppTheme.PHONE to "Follow phone", AppTheme.LIGHT to "Light", AppTheme.DARK to "Dark")
private val darkChoices = listOf(
DarkStyle.BLACK to "Black",
DarkStyle.BLACK_COLOUR to "Black and colour",
DarkStyle.DEEP_COLOUR to "Deep colour",
DarkStyle.GREY to "Grey",
)
private val nightChoices = listOf(NightMode.AUTO to "Auto", NightMode.DAY to "Day", NightMode.NIGHT to "Night")
private val unitChoices = listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial", Units.METRIC to "Metric")
private val navBarChoices = listOf(
@@ -155,6 +162,7 @@ private fun GroupRows(group: CarGroup) {
val moreLyrics by CarSettings.moreLyrics.collectAsState()
val theme by CarSettings.appTheme.collectAsState()
val uiTheme by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
when (group) {
CarGroup.SCREEN -> {
@@ -164,6 +172,10 @@ private fun GroupRows(group: CarGroup) {
SettingsDivider()
Choice("App theme", themeChoices, theme, CarSettings::setAppTheme)
SettingsDivider()
if (theme != AppTheme.LIGHT) {
Choice("Dark mode", darkChoices, darkStyle, CarSettings::setDarkStyle)
SettingsDivider()
}
Choice("Buttons", GsThemes.builtIn.map { it.id to it.name }, uiTheme, CarSettings::setUiTheme)
SettingsDivider()
Choice("Map day/night", nightChoices, night, CarSettings::setNightMode)