Added light theme option suitable for e-ink devices (#256)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ondrej KolonicnyandClaude Opus 4.8 authored and GitHub committed 2026-09-19 11:35:42 +01:00
1 parent de3d5a6d84
commit b7ac9c913a
7 files changed
+72 -10

No files matched your search

@@ -27,6 +27,8 @@ import android.view.View
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
@@ -48,8 +50,10 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
observeNightFilterState() observeNightFilterState()
val settingsRepo = (applicationContext as IContainerProvider).getMainContainer().settingsRepo
setContent { setContent {
MainTheme(isDarkTheme = true) { NavRoot() } val otherSettings by settingsRepo.otherSettings.collectAsState()
MainTheme(isDarkTheme = !otherSettings.stateOfLightTheme) { NavRoot() }
} }
} }
@@ -61,8 +61,8 @@ fun MainTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Composable
SideEffect { SideEffect {
val window = (view.context as ComponentActivity).window val window = (view.context as ComponentActivity).window
val insetsController = WindowCompat.getInsetsController(window, view) val insetsController = WindowCompat.getInsetsController(window, view)
insetsController.isAppearanceLightStatusBars = false insetsController.isAppearanceLightStatusBars = !isDarkTheme
insetsController.isAppearanceLightNavigationBars = false insetsController.isAppearanceLightNavigationBars = !isDarkTheme
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false window.isNavigationBarContrastEnforced = false
} }
@@ -266,6 +266,7 @@
<string name="prefs_other_switch_sweep">Radar sweep</string> <string name="prefs_other_switch_sweep">Radar sweep</string>
<string name="prefs_other_switch_sensors">Use sensors</string> <string name="prefs_other_switch_sensors">Use sensors</string>
<string name="prefs_other_switch_night_mode">Night filter</string> <string name="prefs_other_switch_night_mode">Night filter</string>
<string name="prefs_other_switch_light_theme">Light theme</string>
<string name="prefs_other_compass_offset">Radar compass offset</string> <string name="prefs_other_compass_offset">Radar compass offset</string>
<string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string> <string name="prefs_other_compass_offset_elev">Radar compass offset (elev)</string>
@@ -122,6 +122,39 @@ private val moonIconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN) android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
} }
/** Accent used for the station marker; recolored per theme so it stays visible on a light map. */
private var mapAccentColor = "#FFE082".toColorInt()
/** The dark-map filter (grayscale + invert) is only applied when NOT in light theme; cached to avoid rebuilds. */
private val darkTileFilter by lazy { createColorFilter() }
/**
* Recolor the shared overlay paints for the current theme. The light OSM tiles are white, so the
* dark-theme amber/grey overlays would vanish; light theme swaps them for dark, high-contrast colors.
* Must run before the set*() overlay builders on each frame, as several icons cache the paint color.
*/
private fun applyMapColors(isLightUi: Boolean) {
if (isLightUi) {
mapAccentColor = "#715C0C".toColorInt() // dark amber (matches app lightScheme primary)
footprintPaint.color = mapAccentColor
textPaint.color = "#1E1B13".toColorInt()
textPaint.setShadowLayer(3f, 3f, 3f, Color.WHITE)
sunIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
moonIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter("#4C4639".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
} else {
mapAccentColor = "#FFE082".toColorInt()
footprintPaint.color = mapAccentColor
textPaint.color = mapAccentColor
textPaint.setShadowLayer(3f, 3f, 3f, Color.BLACK)
sunIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
moonIconPaint.colorFilter =
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
}
}
@Composable @Composable
fun MapDestination() { fun MapDestination() {
val context = LocalContext.current val context = LocalContext.current
@@ -177,6 +210,8 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
ElevatedCard(modifier = Modifier.weight(1f)) { ElevatedCard(modifier = Modifier.weight(1f)) {
Box(contentAlignment = Alignment.BottomCenter) { Box(contentAlignment = Alignment.BottomCenter) {
AndroidView({ mapView }) { view -> AndroidView({ mapView }) { view ->
applyMapColors(uiState.isLightUi)
view.overlayManager.tilesOverlay.setColorFilter(if (uiState.isLightUi) null else darkTileFilter)
uiState.stationPosition?.let { setStationPosition(it, view) } uiState.stationPosition?.let { setStationPosition(it, view) }
uiState.track?.let { setSatelliteTrack(it, view) } uiState.track?.let { setSatelliteTrack(it, view) }
uiState.footprint?.let { setFootprint(it, view) } uiState.footprint?.let { setFootprint(it, view) }
@@ -288,7 +323,9 @@ private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
mapView.overlays[OVERLAY_STATION] = Marker(mapView).apply { mapView.overlays[OVERLAY_STATION] = Marker(mapView).apply {
setInfoWindow(null) setInfoWindow(null)
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position) icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)?.apply {
setTint(mapAccentColor)
}
position = GeoPoint(stationPos.latitude, stationPos.longitude) position = GeoPoint(stationPos.latitude, stationPos.longitude)
} }
} }
@@ -585,7 +622,7 @@ private fun rememberMapViewWithLifecycle(): MapView {
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER) zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
overlayManager.tilesOverlay.setColorFilter(createColorFilter()) // Tile color filter (dark inversion vs light) is applied per-theme in the AndroidView update block.
setScrollableAreaLimitLatitude(maxLat, minLat, 0) setScrollableAreaLimitLatitude(maxLat, minLat, 0)
overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() }) overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() })
} }
@@ -80,7 +80,7 @@ class MapViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings -> settingsRepo.otherSettings.collectLatest { settings ->
_uiState.update { it.copy(isUtc = settings.stateOfUtc) } _uiState.update { it.copy(isUtc = settings.stateOfUtc, isLightUi = settings.stateOfLightTheme) }
} }
} }
val (selectedCatNum, _) = satelliteRepo.selectedPass.value val (selectedCatNum, _) = satelliteRepo.selectedPass.value
@@ -495,12 +495,24 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
// Appearance: light theme (paired with a spacer to align with the two-column rows)
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SwitchTile(R.string.prefs_other_switch_light_theme, settings.stateOfLightTheme) {
onAction(SettingsAction.ToggleLightTheme(it))
}
Spacer(modifier = Modifier.weight(1f))
}
Spacer(modifier = Modifier.height(4.dp))
// Display preferences: UTC clock + night filter // Display preferences: UTC clock + night filter
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
SwitchTile(R.string.prefs_other_switch_utc, settings.stateOfUtc) { SwitchTile(R.string.prefs_other_switch_utc, settings.stateOfUtc) {
onAction(SettingsAction.ToggleUtc(it)) onAction(SettingsAction.ToggleUtc(it))
} }
SwitchTile(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) { SwitchTile(
R.string.prefs_other_switch_night_mode,
settings.stateOfNightMode,
enabled = !settings.stateOfLightTheme
) {
onAction(SettingsAction.ToggleNightMode(it)) onAction(SettingsAction.ToggleNightMode(it))
} }
} }
@@ -573,7 +585,12 @@ private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boole
} }
@Composable @Composable
private fun RowScope.SwitchTile(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { private fun RowScope.SwitchTile(
labelResId: Int,
checked: Boolean,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -583,7 +600,7 @@ private fun RowScope.SwitchTile(labelResId: Int, checked: Boolean, onCheckedChan
text = stringResource(id = labelResId), text = stringResource(id = labelResId),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
Switch(checked = checked, onCheckedChange = onCheckedChange) Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange)
} }
} }
@@ -127,7 +127,10 @@ class SettingsViewModel(
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) } is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) } is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) } is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) } is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings {
// Night filter (red-only overlay) is incompatible with the light theme, so turn it off.
it.copy(stateOfLightTheme = action.value, stateOfNightMode = if (action.value) false else it.stateOfNightMode)
}
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) } is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
is SettingsAction.SetRadarCompassOffset -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffset = action.value) } is SettingsAction.SetRadarCompassOffset -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffset = action.value) }
is SettingsAction.SetRadarCompassOffsetElev -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffsetElev = action.value) } is SettingsAction.SetRadarCompassOffsetElev -> settingsRepo.updateOtherSettings { it.copy(radarCompassOffsetElev = action.value) }