Settings → Appearance → Theme gets a fourth option, Black, next to System, Light and Dark. It's the dark theme with pure black backgrounds, and it works with Material You as well as Calendula's own palette. - The month cells and the hour grid are `surfaceContainer` tiles on a `surface` ground, so making every container black would erase the grid lines. floret-kit's `toPureBlack()` sets the ground to #000 and scales the containers down to about #13, #1A, #25 and #30. The tiles stay a shade above black, and sheets and menus still stand out from the page. - Black always means dark. It's an option in the theme picker, so it doesn't follow the system's light/dark setting. - Event colours read `isSystemInDarkTheme()` in 15 places, so an in-app Dark or Black on a phone in light mode got light-theme tints. They now read `LocalFloretDarkTheme`, the theme's own decision. The bug also affected Dark, so the changelog has a Fixed entry for it. - The window backdrop came from the night resource qualifier, so an in-app theme flashed the system's light or dark background before the first frame. `WindowThemeCache` keeps a SharedPreferences copy of the theme choice, and `onCreate` reads it to set the backdrop and bar icons. The copy is written when the stored setting loads, so the first cold start after updating still shows the old backdrop. - Widgets keep their current themes. They get their theme work in the next milestone. Needs jlmakiola/floret-kit#13, which is merged. The submodule is pinned to its merge commit on floret-kit `main` (`b1905da`). Tested with new unit tests for the transform and for saving and reloading `BLACK`, and by hand on the emulator. Closes #181 Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/399
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
<!-- Dark-scheme window backdrop, matching the Compose dark background/surface
|
||||
(#101316) so activity recreation (e.g. language switch) doesn't flash a
|
||||
lighter grey. See values/colors.xml. -->
|
||||
<color name="window_background">#FF101316</color>
|
||||
<color name="window_background">@color/window_background_dark</color>
|
||||
</resources>
|
||||
|
||||
@@ -6,5 +6,10 @@
|
||||
<!-- Window backdrop shown during activity recreation (e.g. on a language
|
||||
switch). Matches the Compose light scheme background/surface so the
|
||||
recreation is seamless; overridden for dark in values-night. -->
|
||||
<color name="window_background">#FFFBFCFE</color>
|
||||
<color name="window_background">@color/window_background_light</color>
|
||||
<!-- The same backdrops pinned for an in-app theme override, which the night
|
||||
qualifier can't see. MainActivity applies one before the first frame. -->
|
||||
<color name="window_background_light">#FFFBFCFE</color>
|
||||
<color name="window_background_dark">#FF101316</color>
|
||||
<color name="window_background_black">#FF000000</color>
|
||||
</resources>
|
||||
|
||||
@@ -454,6 +454,8 @@
|
||||
<string name="settings_theme_system">System</string>
|
||||
<string name="settings_theme_light">Light</string>
|
||||
<string name="settings_theme_dark">Dark</string>
|
||||
<string name="settings_theme_black">Black</string>
|
||||
<string name="settings_theme_black_summary">Dark with pure black backgrounds, for AMOLED screens</string>
|
||||
<string name="settings_theme_hint">Whether the app is light or dark. The choice applies immediately.</string>
|
||||
<!-- %1$s is the theme the system currently resolves to, e.g. "Dark". -->
|
||||
<string name="settings_theme_system_summary">Currently %1$s</string>
|
||||
|
||||
@@ -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))
|
||||
|
||||
+1
-1
Submodule floret-kit updated: 8b33635a85...b1905dac00
Reference in New Issue
Block a user