diff --git a/CHANGELOG.md b/CHANGELOG.md
index efadd073..12c952a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,8 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Tap a phone number in an event to call it.** Phone numbers in an event's title, location and description are now links. Tapping one opens your phone app with the number filled in, ready to call. A conference dial-in like "+1 646 558 8656,,850123#" brings its meeting ID along. Meeting IDs and PINs on their own, dates, times and amounts stay plain text. Long-pressing a field copies it, even when you press on a link ([#339]).
- **Formatted event descriptions read properly.** Descriptions from Outlook, Exchange and Google Calendar are often stored as HTML, which used to show up as a wall of tags. The event view now shows them as formatted text: bold, italics, lists and links work, table rows sit on one line each, and the fonts and colours the sender picked are left out so the text matches the app. Search matches the text you see. The edit form still shows the original, so saving an event never changes its formatting ([#271]).
- **Scroll back to earlier days in the agenda.** Scroll up past today to see earlier days' events, loaded a month at a time. Past days follow the past-events setting: Dim fades them, and with Hide the agenda still starts at today ([#367]).
+- **A black theme for AMOLED screens.** Settings → Appearance → Theme now has Black next to System, Light and Dark. It's the dark theme with pure black backgrounds, which saves battery on AMOLED screens and makes event colours stand out more. Day cells and the hour grid stay a shade above black so the grid lines still show, and sheets and menus still stand out from the page. It works with Material You colours too. The home-screen widgets aren't affected ([#181]).
### Fixed
+- **Event colours follow the theme you picked in Calendula.** With the app set to Dark on a phone in light mode, or the other way round, events took their colours from the phone's setting, so they could come out too pale or too dark. They now match the app's theme. Starting the app with a theme set in Calendula also no longer flashes the phone's light or dark background first ([#181]).
- **Events on the day the clocks change show at their real time.** In the day, 3-day and week views, events later on the day of a daylight saving change were drawn an hour off, so a 14:45 event showed at 15:45. They now sit at their own time. When the clocks go back, 02:00 to 03:00 happens twice: an event in that hour now stays in the right one when you move, resize or edit it, and the editor and event details add the zone, like CEST or CET, so you can tell the two apart ([#387]).
- **The agenda always starts today.** Coming from the month or day view, the agenda used to start at the day you had open there, so "Next 30 days" could show the 1st to the 30th of the month. It now opens on today with your chosen range. Backing out of a day you opened from the agenda still returns you to where you were ([#367]).
- **Multi-day events dim on the days already behind you.** With past events dimmed, an event running over several days stayed at full strength on its earlier days until it ended. Those days now dim in the agenda, month, week and day views ([#367]).
@@ -1867,3 +1869,4 @@ automatically, with zero telemetry and no internet permission.
[#339]: https://codeberg.org/jlmakiola/calendula/issues/339
[#387]: https://codeberg.org/jlmakiola/calendula/issues/387
[#271]: https://codeberg.org/jlmakiola/calendula/issues/271
+[#181]: https://codeberg.org/jlmakiola/calendula/issues/181
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
index ef240199..5f00251a 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt
@@ -25,8 +25,10 @@ import androidx.core.content.IntentCompat
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
+import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
import de.jeanlucmakiola.calendula.domain.EventForm
@@ -51,8 +53,11 @@ import de.jeanlucmakiola.floret.crash.CrashReportDialog
import de.jeanlucmakiola.floret.crash.CrashReporter
import de.jeanlucmakiola.floret.crash.submitCrashReport
import de.jeanlucmakiola.calendula.ui.theme.CalendulaTheme
+import de.jeanlucmakiola.calendula.ui.theme.WindowThemeCache
+import de.jeanlucmakiola.calendula.ui.theme.windowBackgroundRes
import de.jeanlucmakiola.calendula.ui.theme.calendulaTypography
import de.jeanlucmakiola.calendula.ui.theme.resolveFontFamily
+import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
@@ -67,6 +72,7 @@ private data class InsertRequest(val form: EventForm, val source: ImportSource)
class MainActivity : AppCompatActivity() {
@Inject lateinit var calendarRepository: CalendarRepository
+ @Inject lateinit var settingsPrefs: SettingsPrefs
// Which of light/dark the system bars are drawn for. The styles installed
// in onCreate read this field live, so androidx's config-change replay
@@ -124,9 +130,25 @@ class MainActivity : AppCompatActivity() {
return
}
- systemBarsDark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+ val systemDark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
Configuration.UI_MODE_NIGHT_YES
+ // The stored theme only arrives after the first frame, so a Light, Dark
+ // or Black override starts from its last mirrored copy: the matching
+ // backdrop and bar icons instead of the night qualifier's (#181).
+ val cachedTheme = WindowThemeCache.read(this)
+ cachedTheme?.windowBackgroundRes()?.let { window.setBackgroundDrawableResource(it) }
+ systemBarsDark = when (cachedTheme) {
+ null, ThemeMode.SYSTEM -> systemDark
+ ThemeMode.LIGHT -> false
+ ThemeMode.DARK, ThemeMode.BLACK -> true
+ }
applyEdgeToEdge()
+ lifecycleScope.launch {
+ settingsPrefs.themeMode.collect { mode ->
+ WindowThemeCache.write(this@MainActivity, mode)
+ window.setBackgroundDrawableResource(mode.windowBackgroundRes() ?: R.color.window_background)
+ }
+ }
// Android hands the intent that started the task back to onCreate every
// time the activity is recreated — on a rotation, or on a restart after
// the system reclaimed the process. Re-running a launch that opens one
@@ -161,7 +183,7 @@ class MainActivity : AppCompatActivity() {
val darkTheme = when (settings.themeMode) {
ThemeMode.SYSTEM -> isSystemInDarkTheme()
ThemeMode.LIGHT -> false
- ThemeMode.DARK -> true
+ ThemeMode.DARK, ThemeMode.BLACK -> true
}
// onCreate can only see the night resource qualifier, not the in-app
// override — re-apply from the resolved theme so the bar icons follow
@@ -201,6 +223,7 @@ class MainActivity : AppCompatActivity() {
CalendulaTheme(
darkTheme = darkTheme,
dynamicColor = settings.dynamicColor,
+ pureBlack = settings.themeMode == ThemeMode.BLACK,
typography = typography,
) {
Box(modifier = Modifier.fillMaxSize()) {
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt
index 25ced1ce..0fad52fd 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefs.kt
@@ -41,8 +41,11 @@ import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
-/** Light/dark override. SYSTEM follows the device setting. */
-enum class ThemeMode { SYSTEM, LIGHT, DARK }
+/**
+ * Light/dark override. SYSTEM follows the device setting; BLACK is dark with
+ * pure-black surfaces, for AMOLED screens (#181).
+ */
+enum class ThemeMode { SYSTEM, LIGHT, DARK, BLACK }
/**
* Week-start override. [Auto] derives the first day from the active locale;
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRows.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRows.kt
index 31ac272e..34ab3681 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRows.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRows.kt
@@ -2,7 +2,6 @@ package de.jeanlucmakiola.calendula.ui.agenda
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -39,6 +38,7 @@ import de.jeanlucmakiola.calendula.ui.common.eventFill
import de.jeanlucmakiola.calendula.ui.common.formatTimeOfDay
import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.floret.components.Position
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.locale.currentLocale
import de.jeanlucmakiola.floret.locale.localizedDateFormatter
import kotlinx.datetime.DateTimeUnit
@@ -127,7 +127,7 @@ internal fun AgendaEventRow(
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val soften = LocalSoftenColors.current
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
GroupedRow(
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt
index 1c3f66f4..a99bf791 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/calendars/CalendarsScreen.kt
@@ -6,7 +6,6 @@ import android.content.Intent
import android.provider.Settings
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -95,6 +94,7 @@ import de.jeanlucmakiola.calendula.ui.common.LeadingAvatar
import de.jeanlucmakiola.calendula.ui.common.SourceLogo
import de.jeanlucmakiola.calendula.ui.common.curatedSourcePackage
import de.jeanlucmakiola.floret.components.CollapsingScaffold
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.identity.collapseExit
import de.jeanlucmakiola.floret.identity.expandEnter
import de.jeanlucmakiola.floret.identity.predictiveBack
@@ -478,7 +478,7 @@ private fun CalendarNameField(
onNameChange: (String) -> Unit,
onImeAction: () -> Unit,
) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val soften = LocalSoftenColors.current
GroupedSurface(position = Position.Alone, modifier = Modifier.padding(horizontal = 16.dp)) {
Row(
@@ -522,7 +522,7 @@ private fun CalendarNameField(
*/
@Composable
private fun CalendarColorGrid(selected: Int, onSelect: (Int) -> Unit) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val soften = LocalSoftenColors.current
GroupedSurface(position = Position.Alone, modifier = Modifier.padding(horizontal = 16.dp)) {
Column(
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarColors.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarColors.kt
index b37b64ad..87122c4a 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarColors.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarColors.kt
@@ -1,7 +1,6 @@
package de.jeanlucmakiola.calendula.ui.common
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
@@ -14,6 +13,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
/**
* Leading avatar for a calendar: a neutral chip holding a calendar glyph tinted
@@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
*/
@Composable
fun CalendarColorChip(color: Int, modifier: Modifier = Modifier) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val soften = LocalSoftenColors.current
Box(
modifier = modifier
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt
index 2b902375..e4377bd7 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/TimelineDrag.kt
@@ -6,7 +6,6 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
@@ -56,6 +55,7 @@ import de.jeanlucmakiola.calendula.ui.week.TimedBlock
import de.jeanlucmakiola.calendula.ui.week.minutesPast
import de.jeanlucmakiola.calendula.ui.week.wallClockEndMinutes
import de.jeanlucmakiola.calendula.ui.week.wallClockMinutes
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.identity.rememberReduceMotion
import de.jeanlucmakiola.floret.locale.currentLocale
import kotlinx.coroutines.delay
@@ -1152,7 +1152,7 @@ const val SETTLE_FADE_MILLIS: Int = 250
fun TimelineDragOverlay(controller: TimelineDragController, modifier: Modifier = Modifier) {
var origin by remember { mutableStateOf(Offset.Zero) }
val density = LocalDensity.current
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val use24Hour = LocalUse24HourFormat.current
val locale = currentLocale()
val reduceMotion = rememberReduceMotion()
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt
index 952f70f8..0d99c05e 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/detail/EventDetailScreen.kt
@@ -5,7 +5,6 @@ import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -93,6 +92,7 @@ import de.jeanlucmakiola.calendula.domain.RichText
import de.jeanlucmakiola.calendula.domain.TimeZoneOption
import de.jeanlucmakiola.calendula.domain.timeZoneOptionOf
import de.jeanlucmakiola.calendula.domain.zoneDescriptor
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.identity.predictiveBack
import de.jeanlucmakiola.calendula.ui.common.CalendarFailure
import de.jeanlucmakiola.calendula.ui.common.DeleteEventDialog
@@ -303,7 +303,7 @@ private fun EventDetailContent(
) {
val detail = state.detail
val instance = detail.instance
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val locale = currentDetailLocale()
val accent = eventAccent(instance.color, dark, LocalSoftenColors.current)
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt
index 3e170fd2..ea31c6f5 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt
@@ -13,7 +13,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@@ -36,6 +35,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Notes
import androidx.compose.material.icons.filled.Add
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import kotlinx.coroutines.launch
import de.jeanlucmakiola.calendula.ui.common.sendEventShare
import de.jeanlucmakiola.calendula.ui.common.rememberWriteGate
@@ -703,7 +703,7 @@ private fun EventEditContent(
val locale = currentLocale()
val deviceZone by viewModel.zone.collectAsStateWithLifecycle()
val firstDayOfWeek by viewModel.firstDayOfWeek.collectAsStateWithLifecycle()
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
// The title, date and recurrence are managed by the special-dates sync, so
// they're locked here; everything else (reminders, location, notes) is the
// user's to edit.
@@ -2299,7 +2299,7 @@ private fun ColorPickerDialog(
onClear: () -> Unit,
onDismiss: () -> Unit,
) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
FullScreenPicker(
title = stringResource(R.string.event_edit_color),
onDismiss = onDismiss,
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt
index 07830045..2e778c2d 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt
@@ -24,6 +24,7 @@ import de.jeanlucmakiola.calendula.ui.common.morphEvent
import de.jeanlucmakiola.calendula.ui.common.morphPageActive
import de.jeanlucmakiola.calendula.ui.common.jumpToPage
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarPageSpec
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.drop
import androidx.compose.animation.ExperimentalSharedTransitionApi
@@ -46,7 +47,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@@ -589,7 +589,7 @@ private const val MONTH_SETTLE_TIMEOUT_MILLIS = 450L
@Composable
private fun MonthDragOverlay(controller: MonthDragController) {
var origin by remember { mutableStateOf(Offset.Zero) }
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val density = LocalDensity.current
val timeChipWidth = rememberMonthTimeChipWidth()
val reduceMotion = rememberReduceMotion()
@@ -1722,7 +1722,7 @@ internal fun SplitMonthGrid(
onSelectDay: (LocalDate) -> Unit,
modifier: Modifier = Modifier,
) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val month = state.month
val hasToday = state.today.year == month.year && state.today.month == month.month
val ripple = rememberTodayRipple(active = hasToday)
@@ -2231,7 +2231,7 @@ private fun MonthWeekRow(
*/
selected: LocalDate? = null,
) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val todayCol = week.days.indexOf(today).takeIf { it >= 0 && inMonth(today) }
val ripple = rememberTodayRipple(active = todayCol != null)
val overflowRow = rememberOverflowRowHeight()
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt
index 84539050..2c456c02 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/search/SearchScreen.kt
@@ -8,7 +8,6 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -83,6 +82,7 @@ import de.jeanlucmakiola.calendula.domain.RecurringWriteScope
import de.jeanlucmakiola.calendula.domain.SearchHit
import de.jeanlucmakiola.calendula.domain.SearchMonth
import de.jeanlucmakiola.calendula.domain.isDeclined
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import de.jeanlucmakiola.floret.identity.animateItemMotion
import de.jeanlucmakiola.floret.identity.fadeThrough
import de.jeanlucmakiola.floret.identity.predictiveBack
@@ -513,7 +513,7 @@ private fun SearchResultRow(
// Spans are empty for a blank title — the query never matches the
// placeholder — so marking the substitute is safe.
val title = event.title.ifBlank { stringResource(R.string.event_untitled) }
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val soften = LocalSoftenColors.current
// On a picked row the headline is already recoloured for the secondary
// container, so the match is carried by weight alone there.
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppearanceSettings.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppearanceSettings.kt
index 5774e5a8..6febcc4d 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppearanceSettings.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/AppearanceSettings.kt
@@ -215,13 +215,13 @@ internal fun AppearanceScreen(
selected = state.themeMode,
label = { themeLabel(it) },
summary = { mode ->
- if (mode == ThemeMode.SYSTEM) {
- stringResource(
+ when (mode) {
+ ThemeMode.SYSTEM -> stringResource(
R.string.settings_theme_system_summary,
themeLabel(if (systemDark) ThemeMode.DARK else ThemeMode.LIGHT),
)
- } else {
- null
+ ThemeMode.BLACK -> stringResource(R.string.settings_theme_black_summary)
+ ThemeMode.LIGHT, ThemeMode.DARK -> null
}
},
onSelect = viewModel::setThemeMode,
@@ -258,6 +258,7 @@ private fun themeLabel(mode: ThemeMode): String = stringResource(
ThemeMode.SYSTEM -> R.string.settings_theme_system
ThemeMode.LIGHT -> R.string.settings_theme_light
ThemeMode.DARK -> R.string.settings_theme_dark
+ ThemeMode.BLACK -> R.string.settings_theme_black
},
)
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt
index 5e56f5b2..e7908610 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/Theme.kt
@@ -16,13 +16,15 @@ import de.jeanlucmakiola.floret.identity.FloretExpressiveTheme
*
* The mechanics live in floret-kit's identity module; the look stays here, so a
* sibling app reuses the same theming without inheriting Calendula's palette.
- * The Settings screen overrides darkTheme, dynamicColor and typography; the bare
- * defaults (used by the crash screen) just follow the system.
+ * The Settings screen overrides darkTheme, dynamicColor, pureBlack (the
+ * AMOLED theme, #181) and typography; the bare defaults (used by the crash
+ * screen) just follow the system.
*/
@Composable
fun CalendulaTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
+ pureBlack: Boolean = false,
typography: Typography = CalendulaTypography,
content: @Composable () -> Unit,
) {
@@ -31,6 +33,7 @@ fun CalendulaTheme(
darkScheme = CalendulaDarkFallback,
darkTheme = darkTheme,
dynamicColor = dynamicColor,
+ pureBlack = pureBlack,
typography = typography,
content = content,
)
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/WindowTheme.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/WindowTheme.kt
new file mode 100644
index 00000000..ef3c603b
--- /dev/null
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/theme/WindowTheme.kt
@@ -0,0 +1,45 @@
+package de.jeanlucmakiola.calendula.ui.theme
+
+import android.content.Context
+import androidx.annotation.ColorRes
+import androidx.core.content.edit
+import de.jeanlucmakiola.calendula.R
+import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
+
+/**
+ * A synchronous copy of the theme choice, for the window backdrop drawn before
+ * the first Compose frame. The real setting lives in DataStore, which only
+ * answers asynchronously — too late for onCreate, so a Light, Dark or Black
+ * override flashed the night-qualifier backdrop on a cold start or a
+ * recreation (#181). MainActivity mirrors every stored value here and reads it
+ * back before setContent.
+ */
+internal object WindowThemeCache {
+
+ private const val FILE = "window_theme"
+ private const val KEY_THEME_MODE = "theme_mode"
+
+ /** The last mirrored theme choice, or null before the first mirror. */
+ fun read(context: Context): ThemeMode? =
+ context.getSharedPreferences(FILE, Context.MODE_PRIVATE)
+ .getString(KEY_THEME_MODE, null)
+ ?.let { name -> ThemeMode.entries.firstOrNull { it.name == name } }
+
+ fun write(context: Context, mode: ThemeMode) {
+ val prefs = context.getSharedPreferences(FILE, Context.MODE_PRIVATE)
+ if (prefs.getString(KEY_THEME_MODE, null) == mode.name) return
+ prefs.edit { putString(KEY_THEME_MODE, mode.name) }
+ }
+}
+
+/**
+ * The window backdrop matching this theme choice's Compose background, or null
+ * for [ThemeMode.SYSTEM], whose night-qualified theme default already matches.
+ */
+@ColorRes
+internal fun ThemeMode.windowBackgroundRes(): Int? = when (this) {
+ ThemeMode.SYSTEM -> null
+ ThemeMode.LIGHT -> R.color.window_background_light
+ ThemeMode.DARK -> R.color.window_background_dark
+ ThemeMode.BLACK -> R.color.window_background_black
+}
diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt
index d7b17527..85d3799c 100644
--- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt
+++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt
@@ -20,12 +20,12 @@ import de.jeanlucmakiola.calendula.ui.common.rememberCalendarPageSpec
import de.jeanlucmakiola.calendula.ui.common.rememberViewSwitchSpec
import de.jeanlucmakiola.calendula.ui.common.jumpToPage
import de.jeanlucmakiola.calendula.ui.common.pagedHeight
+import de.jeanlucmakiola.floret.identity.LocalFloretDarkTheme
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.drop
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
@@ -1188,7 +1188,7 @@ private fun AllDayStrip(
animatedHeight: (() -> Dp)? = null,
onEventClick: (EventInstance) -> Unit,
) {
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
Row(
modifier = Modifier
@@ -1295,7 +1295,7 @@ private fun Timeline(
onResize: (TimelineResizeDrop) -> Unit,
) {
val fullViewportHeight = remember(geometry) { intArrayOf(-1) }
- val dark = isSystemInDarkTheme()
+ val dark = LocalFloretDarkTheme.current
val zoom = LocalTimelineZoom.current
val density = LocalDensity.current
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 15fc9905..db4932f2 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -2,5 +2,5 @@
- #FF101316
+ @color/window_background_dark
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 3435ae81..f941fbd3 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -6,5 +6,10 @@
- #FFFBFCFE
+ @color/window_background_light
+
+ #FFFBFCFE
+ #FF101316
+ #FF000000
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8a49eb1a..95bdd57a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -454,6 +454,8 @@
System
Light
Dark
+ Black
+ Dark with pure black backgrounds, for AMOLED screens
Whether the app is light or dark. The choice applies immediately.
Currently %1$s
diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt
index 42fc64f6..ad38ee34 100644
--- a/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt
+++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/prefs/SettingsPrefsTest.kt
@@ -47,6 +47,13 @@ class SettingsPrefsTest {
assertThat(prefs.themeMode.first()).isEqualTo(ThemeMode.DARK)
}
+ @Test
+ fun `black theme mode round-trips`(@TempDir tempDir: Path) = runTest {
+ val prefs = SettingsPrefs(newDataStore(tempDir))
+ prefs.setThemeMode(ThemeMode.BLACK)
+ assertThat(prefs.themeMode.first()).isEqualTo(ThemeMode.BLACK)
+ }
+
@Test
fun `dynamic colour round-trips`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
diff --git a/floret-kit b/floret-kit
index 8b33635a..b1905dac 160000
--- a/floret-kit
+++ b/floret-kit
@@ -1 +1 @@
-Subproject commit 8b33635a852d404cd72d2483dd5bdc4926f13276
+Subproject commit b1905dac00e554fcb80eb73049e27c2e9b9553f7