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 package app.seb3thehacker.gearslip.car
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import android.content.Context import android.content.Context
import app.seb3thehacker.gearslip.AppSettings import app.seb3thehacker.gearslip.AppSettings
import app.seb3thehacker.gearslip.audio.CarAudio 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_MORE_LYRICS = "car_more_lyrics"
private const val KEY_THEME = "car_app_theme" private const val KEY_THEME = "car_app_theme"
private const val KEY_UI_THEME = "car_ui_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_PINNED = "car_pinned_apps"
private const val KEY_APP_ORDER = "car_app_order" private const val KEY_APP_ORDER = "car_app_order"
private const val KEY_EXPERIMENTAL_FEATURES = "car_experimental_features" 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 moreLyricsFlow = MutableStateFlow(true)
private val themeFlow = MutableStateFlow(AppTheme.PHONE) private val themeFlow = MutableStateFlow(AppTheme.PHONE)
val appTheme: StateFlow<AppTheme> = themeFlow 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) private val uiThemeFlow = MutableStateFlow(GsThemes.default.id)
/** Id of the [GsTheme] controls are drawn in; an unknown id falls back to the default. */ /** 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 { themeFlow.value = runCatching {
AppTheme.valueOf(AppSettings.getString(app, KEY_THEME, AppTheme.PHONE.name)) AppTheme.valueOf(AppSettings.getString(app, KEY_THEME, AppTheme.PHONE.name))
}.getOrDefault(AppTheme.PHONE) }.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) uiThemeFlow.value = AppSettings.getString(app, KEY_UI_THEME, GsThemes.default.id)
autoplayFlow.value = AppSettings.getString(app, KEY_AUTOPLAY, "false") == "true" autoplayFlow.value = AppSettings.getString(app, KEY_AUTOPLAY, "false") == "true"
playerTitleFlow.value = AppSettings.getString(app, KEY_PLAYER_TITLE, "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) 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) { fun setUiTheme(id: String) {
uiThemeFlow.value = id uiThemeFlow.value = id
AppSettings.putString(app, KEY_UI_THEME, id) AppSettings.putString(app, KEY_UI_THEME, id)
@@ -1,6 +1,7 @@
package app.seb3thehacker.gearslip.car package app.seb3thehacker.gearslip.car
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import app.seb3thehacker.gearslip.car.theme.* import app.seb3thehacker.gearslip.car.theme.*
import android.os.Build import android.os.Build
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -68,6 +69,7 @@ fun CarSettingsScreen() {
val moreLyrics by CarSettings.moreLyrics.collectAsState() val moreLyrics by CarSettings.moreLyrics.collectAsState()
val theme by CarSettings.appTheme.collectAsState() val theme by CarSettings.appTheme.collectAsState()
val uiTheme by CarSettings.uiTheme.collectAsState() val uiTheme by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
val experimentalFeatures by CarSettings.experimentalFeaturesEnabled.collectAsState() val experimentalFeatures by CarSettings.experimentalFeaturesEnabled.collectAsState()
val carSensors by CarSettings.carSensorsEnabled.collectAsState() val carSensors by CarSettings.carSensorsEnabled.collectAsState()
val adaptiveVideo by CarSettings.adaptiveVideoEnabled.collectAsState() val adaptiveVideo by CarSettings.adaptiveVideoEnabled.collectAsState()
@@ -116,6 +118,19 @@ fun CarSettingsScreen() {
) { CarSettings.setAppTheme(it) } ) { 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 { item {
ChoiceRow( ChoiceRow(
"Theme", "Theme",
@@ -110,6 +110,7 @@ fun CarUi() {
val phoneDark by CarEnvironment.phoneDark.collectAsState() val phoneDark by CarEnvironment.phoneDark.collectAsState()
val appTheme by CarSettings.appTheme.collectAsState() val appTheme by CarSettings.appTheme.collectAsState()
val uiThemeId by CarSettings.uiTheme.collectAsState() val uiThemeId by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
val mediaApp by CarServices.mediaApp.collectAsState() val mediaApp by CarServices.mediaApp.collectAsState()
val darkOutside by CarEnvironment.darkOutside.collectAsState() val darkOutside by CarEnvironment.darkOutside.collectAsState()
val appContext = LocalContext.current.applicationContext val appContext = LocalContext.current.applicationContext
@@ -118,7 +119,7 @@ fun CarUi() {
AppTheme.LIGHT -> false AppTheme.LIGHT -> false
AppTheme.DARK -> true 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 base = LocalDensity.current
val insets by CarEnvironment.insets.collectAsState() val insets by CarEnvironment.insets.collectAsState()
val navigator = remember { CarNavigator(CarSettings.openOnConnect.value) } 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.material3.ColorScheme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver 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 * 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. * 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. * 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 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) fun tint(own: Color, step: Int) = primary.copy(alpha = base + step * (if (dark) 0.015f else 0.01f)).compositeOver(own)
return copy( return copy(
@@ -27,3 +29,54 @@ fun ColorScheme.withTintedSurfaces(dark: Boolean): ColorScheme {
surfaceVariant = tint(surfaceVariant, 3), 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 package app.seb3thehacker.gearslip.ui
import app.seb3thehacker.gearslip.car.theme.DarkStyle
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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 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 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 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 unitChoices = listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial", Units.METRIC to "Metric")
private val navBarChoices = listOf( private val navBarChoices = listOf(
@@ -155,6 +162,7 @@ private fun GroupRows(group: CarGroup) {
val moreLyrics by CarSettings.moreLyrics.collectAsState() val moreLyrics by CarSettings.moreLyrics.collectAsState()
val theme by CarSettings.appTheme.collectAsState() val theme by CarSettings.appTheme.collectAsState()
val uiTheme by CarSettings.uiTheme.collectAsState() val uiTheme by CarSettings.uiTheme.collectAsState()
val darkStyle by CarSettings.darkStyle.collectAsState()
when (group) { when (group) {
CarGroup.SCREEN -> { CarGroup.SCREEN -> {
@@ -164,6 +172,10 @@ private fun GroupRows(group: CarGroup) {
SettingsDivider() SettingsDivider()
Choice("App theme", themeChoices, theme, CarSettings::setAppTheme) Choice("App theme", themeChoices, theme, CarSettings::setAppTheme)
SettingsDivider() 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) Choice("Buttons", GsThemes.builtIn.map { it.id to it.name }, uiTheme, CarSettings::setUiTheme)
SettingsDivider() SettingsDivider()
Choice("Map day/night", nightChoices, night, CarSettings::setNightMode) Choice("Map day/night", nightChoices, night, CarSettings::setNightMode)