From 08eef32e59b4ce01659cfd2b90aa9501f6a856e1 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Mon, 26 Oct 2026 00:47:56 -0700 Subject: [PATCH] car: a Dark mode setting with black, black and colour, deep colour and the old grey, black by default --- .../seb3thehacker/gearslip/car/CarSettings.kt | 14 +++++ .../gearslip/car/CarSettingsScreen.kt | 15 +++++ .../app/seb3thehacker/gearslip/car/CarUi.kt | 3 +- .../gearslip/car/theme/CarColors.kt | 55 ++++++++++++++++++- .../gearslip/ui/HomeCarSettings.kt | 12 ++++ 5 files changed, 97 insertions(+), 2 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt index c4ca6e2..778749f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettings.kt @@ -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 = themeFlow + private val darkStyleFlow = MutableStateFlow(DarkStyle.BLACK) + + /** How dark the car's dark mode goes; black until the driver picks another. */ + val darkStyle: StateFlow = 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) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt index 433b550..b4ce854 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarSettingsScreen.kt @@ -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", diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt index 3b8f670..aaae20d 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarUi.kt @@ -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) } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt index ec99cc3..8a6642b 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/theme/CarColors.kt @@ -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)) +} diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt index 5218a42..fc59eb0 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt @@ -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)