feat(theme): follow the stored theme mode and dynamic colour
The theme now reads the persisted preference instead of the device night flag alone. The system bar styles are resolved from the same value and re-applied when it changes, so a light theme forced under a dark system no longer draws white status bar icons onto a light background. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,38 +1,51 @@
|
||||
package de.jeanlucmakiola.clockula
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import de.jeanlucmakiola.clockula.data.prefs.SettingsPrefs
|
||||
import de.jeanlucmakiola.clockula.ui.crash.CrashReportActivity
|
||||
import de.jeanlucmakiola.clockula.ui.theme.ClockulaTheme
|
||||
import de.jeanlucmakiola.floret.crash.CrashReportDialog
|
||||
import de.jeanlucmakiola.floret.crash.CrashReporter
|
||||
import de.jeanlucmakiola.floret.crash.submitCrashReport
|
||||
import de.jeanlucmakiola.floret.prefs.Appearance
|
||||
import de.jeanlucmakiola.floret.prefs.isDark
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Single activity. M0 scaffolding: it brings up the theme and the crash surface
|
||||
* and nothing else. The four-tab shell and the live pill (docs/PLAN.md §9) land
|
||||
* in M4, and the theme starts following stored preferences once the data layer
|
||||
* exists in M2 — until then [ClockulaTheme]'s own defaults (follow-system,
|
||||
* dynamic colour) stand in.
|
||||
* Single activity. M1 scaffolding: it brings up the theme — now following the
|
||||
* stored appearance preference — and the crash surface, and nothing else. The
|
||||
* four-tab shell and the live pill (docs/PLAN.md §9) land in M4.
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
// Field-injected rather than read through a ViewModel: the appearance feeds
|
||||
// the theme before any screen exists. The settings ViewModel arrives with
|
||||
// the settings screen in M10.
|
||||
@Inject
|
||||
lateinit var settingsPrefs: SettingsPrefs
|
||||
|
||||
// A captured crash report awaiting the user's decision, surfaced as a dialog
|
||||
// over the app on the next launch (the single-crash path). A startup
|
||||
// crash-loop is handled out of band, before setContent — see below.
|
||||
@@ -50,13 +63,27 @@ class MainActivity : ComponentActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
enableEdgeToEdge()
|
||||
|
||||
// Surface a single captured crash as a dialog on the next launch.
|
||||
if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this)
|
||||
|
||||
setContent {
|
||||
ClockulaTheme {
|
||||
// Until the first snapshot arrives, the family's defaults stand in —
|
||||
// follow the system, dynamic colour on — so the first frame is never
|
||||
// blocked on disk.
|
||||
val appearance by settingsPrefs.appearance
|
||||
.collectAsStateWithLifecycle(initialValue = Appearance())
|
||||
val darkTheme = appearance.themeMode.isDark(isSystemInDarkTheme())
|
||||
|
||||
// The system bars follow the *resolved* theme, not the device night
|
||||
// flag: with the device dark and ThemeMode.LIGHT stored, deriving the
|
||||
// icon colours from the system would paint white icons onto a light
|
||||
// background. Re-applied whenever the stored preference changes.
|
||||
LaunchedEffect(darkTheme) { applyEdgeToEdge(darkTheme) }
|
||||
|
||||
ClockulaTheme(
|
||||
darkTheme = darkTheme,
|
||||
dynamicColor = appearance.dynamicColor,
|
||||
) {
|
||||
Placeholder(Modifier.fillMaxSize())
|
||||
pendingCrashReport?.let { report ->
|
||||
CrashReportDialog(
|
||||
@@ -76,6 +103,13 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyEdgeToEdge(darkTheme: Boolean) {
|
||||
enableEdgeToEdge(
|
||||
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT) { darkTheme },
|
||||
navigationBarStyle = SystemBarStyle.auto(NAV_BAR_LIGHT_SCRIM, NAV_BAR_DARK_SCRIM) { darkTheme },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
// A successful start breaks any loop; reset the timing trail so a later
|
||||
@@ -98,6 +132,10 @@ private fun Placeholder(modifier: Modifier = Modifier) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The scrims androidx uses for a three-button navigation bar, matched exactly. */
|
||||
private val NAV_BAR_LIGHT_SCRIM = Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
||||
private val NAV_BAR_DARK_SCRIM = Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
||||
|
||||
@Composable
|
||||
private fun stringResourceAppName(): String =
|
||||
androidx.compose.ui.res.stringResource(R.string.app_name)
|
||||
|
||||
Reference in New Issue
Block a user