Move the prefs classes onto floret's PrefStore

This commit is contained in:
2026-10-16 13:02:04 +02:00
parent b627ed04b7
commit 52857a7d3b
12 changed files with 755 additions and 1084 deletions
+1
View File
@@ -176,6 +176,7 @@ dependencies {
implementation("de.jeanlucmakiola.floret:core-locale")
implementation("de.jeanlucmakiola.floret:core-crash")
implementation("de.jeanlucmakiola.floret:core-di")
implementation("de.jeanlucmakiola.floret:core-prefs")
implementation("de.jeanlucmakiola.floret:core-reminders")
implementation("de.jeanlucmakiola.floret:core-android")
implementation("de.jeanlucmakiola.floret:identity")
@@ -29,7 +29,9 @@ 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.floret.prefs.ThemeMode
import de.jeanlucmakiola.floret.prefs.isDark
import de.jeanlucmakiola.floret.prefs.isPureBlack
import de.jeanlucmakiola.calendula.data.prefs.is24Hour
import de.jeanlucmakiola.calendula.domain.EventForm
import de.jeanlucmakiola.calendula.domain.buildInsertEventForm
@@ -137,11 +139,7 @@ class MainActivity : AppCompatActivity() {
// 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
}
systemBarsDark = (cachedTheme ?: ThemeMode.SYSTEM).isDark(systemDark)
applyEdgeToEdge()
lifecycleScope.launch {
settingsPrefs.themeMode.collect { mode ->
@@ -180,11 +178,7 @@ class MainActivity : AppCompatActivity() {
// and the Settings screen, so a theme change applies app-wide at once.
val settingsViewModel: SettingsViewModel = hiltViewModel()
val settings by settingsViewModel.state.collectAsStateWithLifecycle()
val darkTheme = when (settings.themeMode) {
ThemeMode.SYSTEM -> isSystemInDarkTheme()
ThemeMode.LIGHT -> false
ThemeMode.DARK, ThemeMode.BLACK -> true
}
val darkTheme = settings.themeMode.isDark(isSystemInDarkTheme())
// onCreate can only see the night resource qualifier, not the in-app
// override — re-apply from the resolved theme so the bar icons follow
// the app's light/dark choice.
@@ -223,7 +217,7 @@ class MainActivity : AppCompatActivity() {
CalendulaTheme(
darkTheme = darkTheme,
dynamicColor = settings.dynamicColor,
pureBlack = settings.themeMode == ThemeMode.BLACK,
pureBlack = settings.themeMode.isPureBlack,
typography = typography,
) {
Box(modifier = Modifier.fillMaxSize()) {
@@ -1,15 +1,13 @@
package de.jeanlucmakiola.calendula.data.prefs
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import de.jeanlucmakiola.floret.prefs.Pref
import de.jeanlucmakiola.floret.prefs.PrefStore
import de.jeanlucmakiola.floret.prefs.nullableBooleanPref
import de.jeanlucmakiola.floret.prefs.nullableLongPref
import de.jeanlucmakiola.floret.prefs.nullableStringPref
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
@@ -26,18 +24,13 @@ import javax.inject.Singleton
*/
@Singleton
class CalendarPrefs @Inject constructor(
private val store: DataStore<Preferences>,
store: DataStore<Preferences>,
) {
private val prefs = PrefStore(store)
// Both id sets are deduped: the store is shared with SettingsPrefs, so any
// unrelated write would otherwise re-emit an identical set.
val hiddenCalendarIds: Flow<Set<Long>> = store.data
.map { prefs -> prefs[HIDDEN_IDS_KEY].parseIds() }
.distinctUntilChanged()
val hiddenCalendarIds: Flow<Set<Long>> = prefs.flow(HIDDEN_IDS)
suspend fun setHiddenCalendarIds(ids: Set<Long>) {
store.edit { prefs -> prefs.writeIds(HIDDEN_IDS_KEY, ids) }
}
suspend fun setHiddenCalendarIds(ids: Set<Long>) = prefs.set(HIDDEN_IDS, ids)
/**
* Switch-offs the provider does not know about yet, because writing
@@ -48,24 +41,18 @@ class CalendarPrefs @Inject constructor(
* second visibility model: `CalendarVisibilityReconciler` drains it entry by
* entry as soon as the app may write, and nothing adds to it while it may.
*/
val pendingDisabledCalendarIds: Flow<Set<Long>> = store.data
.map { prefs -> prefs[DISABLED_IDS_KEY].parseIds() }
.distinctUntilChanged()
val pendingDisabledCalendarIds: Flow<Set<Long>> = prefs.flow(DISABLED_IDS)
suspend fun addPendingDisabledCalendarIds(ids: Collection<Long>) =
editPendingDisabled { it + ids }
suspend fun addPendingDisabledCalendarIds(ids: Collection<Long>) {
prefs.update(DISABLED_IDS) { it + ids }
}
/**
* Drop [ids] from the pending set, one at a time as the reconciler flushes
* them, so a run that fails part-way never re-applies what already landed.
*/
suspend fun removePendingDisabledCalendarIds(ids: Collection<Long>) =
editPendingDisabled { it - ids.toSet() }
private suspend fun editPendingDisabled(transform: (Set<Long>) -> Set<Long>) {
store.edit { prefs ->
prefs.writeIds(DISABLED_IDS_KEY, transform(prefs[DISABLED_IDS_KEY].parseIds()))
}
suspend fun removePendingDisabledCalendarIds(ids: Collection<Long>) {
prefs.update(DISABLED_IDS) { it - ids.toSet() }
}
/**
@@ -73,39 +60,28 @@ class CalendarPrefs @Inject constructor(
* owed. Null until the reconciler has evaluated it (which needs the calendar
* permission), false once it has been shown or was never needed.
*/
val visibilityNoticePending: Flow<Boolean?> = store.data.map { prefs ->
prefs[VISIBILITY_NOTICE_KEY]
}
val visibilityNoticePending: Flow<Boolean?> = prefs.flow(VISIBILITY_NOTICE)
suspend fun setVisibilityNoticePending(pending: Boolean) {
store.edit { prefs -> prefs[VISIBILITY_NOTICE_KEY] = pending }
}
suspend fun setVisibilityNoticePending(pending: Boolean) = prefs.set(VISIBILITY_NOTICE, pending)
/**
* The calendar the user last created an event in; preselected in the
* event form. Null until the first event is created.
*/
val lastUsedCalendarId: Flow<Long?> = store.data.map { prefs ->
prefs[LAST_USED_CALENDAR_KEY]
}
val lastUsedCalendarId: Flow<Long?> = prefs.flow(LAST_USED_CALENDAR)
suspend fun setLastUsedCalendarId(id: Long) {
store.edit { prefs -> prefs[LAST_USED_CALENDAR_KEY] = id }
}
suspend fun setLastUsedCalendarId(id: Long) = prefs.set(LAST_USED_CALENDAR, id)
companion object {
internal val HIDDEN_IDS_KEY = stringPreferencesKey("hidden_calendar_ids")
internal val DISABLED_IDS_KEY = stringPreferencesKey("disabled_calendar_ids")
internal val VISIBILITY_NOTICE_KEY = booleanPreferencesKey("visibility_notice_pending")
internal val LAST_USED_CALENDAR_KEY = longPreferencesKey("last_used_calendar_id")
private companion object {
val HIDDEN_IDS = idSetPref("hidden_calendar_ids")
val DISABLED_IDS = idSetPref("disabled_calendar_ids")
val VISIBILITY_NOTICE = nullableBooleanPref("visibility_notice_pending")
val LAST_USED_CALENDAR = nullableLongPref("last_used_calendar_id")
}
}
private fun String?.parseIds(): Set<Long> = orEmpty()
.split(',')
.mapNotNull { it.trim().toLongOrNull() }
.toSet()
private fun MutablePreferences.writeIds(key: Preferences.Key<String>, ids: Set<Long>) {
if (ids.isEmpty()) remove(key) else set(key, ids.sorted().joinToString(","))
}
/** A comma-joined id set; non-numeric tokens are dropped, and an empty set removes the key. */
private fun idSetPref(name: String): Pref<Set<Long>> = nullableStringPref(name).map(
{ stored -> stored.orEmpty().split(',').mapNotNull { it.trim().toLongOrNull() }.toSet() },
{ ids -> if (ids.isEmpty()) null else ids.sorted().joinToString(",") },
)
@@ -2,10 +2,8 @@ package de.jeanlucmakiola.calendula.data.prefs
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.longPreferencesKey
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import de.jeanlucmakiola.floret.prefs.PrefStore
import de.jeanlucmakiola.floret.prefs.nullableLongPref
import javax.inject.Inject
import javax.inject.Singleton
@@ -20,17 +18,16 @@ import javax.inject.Singleton
*/
@Singleton
class ReminderStatePrefs @Inject constructor(
private val store: DataStore<Preferences>,
store: DataStore<Preferences>,
) {
private val prefs = PrefStore(store)
/** The watermark, or `null` before the first scan has ever run. */
suspend fun lastScanMillis(): Long? = store.data.map { it[LAST_SCAN_KEY] }.first()
suspend fun lastScanMillis(): Long? = prefs.get(LAST_SCAN)
suspend fun setLastScanMillis(millis: Long) {
store.edit { prefs -> prefs[LAST_SCAN_KEY] = millis }
}
suspend fun setLastScanMillis(millis: Long) = prefs.set(LAST_SCAN, millis)
private companion object {
val LAST_SCAN_KEY = longPreferencesKey("reminder_last_scan_millis")
val LAST_SCAN = nullableLongPref("reminder_last_scan_millis")
}
}
File diff suppressed because it is too large Load Diff
@@ -166,15 +166,6 @@ class OnboardingViewModel @Inject constructor(
hiddenTouched,
) { hidden, touched -> if (touched) null else hidden }
/** Both calendar steps' inputs: what each applies to, and whether it is answered. */
private val calendarsFlags: Flow<CalendarsFlags> = combine(
calendarsApplies,
prefs.onboardingCalendarsDone,
hiddenApplies,
prefs.onboardingHiddenCalendarsDone,
::CalendarsFlags,
)
/**
* The calendars the hidden-calendars step lists, so the user can judge which
* to switch on rather than take the app's word that there are some. Live: a
@@ -188,47 +179,35 @@ class OnboardingViewModel @Inject constructor(
initialValue = emptyList(),
)
private val flags: Flow<OnboardingFlags> = combine(
prefs.reminderOnboardingDone,
prefs.onboardingWizardArmed,
prefs.onboardingBackupDone,
prefs.onboardingViewDone,
prefs.onboardingMonthStyleDone,
::OnboardingFlags,
)
/** The closing steps' own flags, and whether the notice applies at all. */
private val closingFlags: Flow<ClosingFlags> = combine(
calendarPrefs.visibilityNoticePending.map { it == true },
prefs.onboardingVisibilityDone,
prefs.onboardingDoneShown,
::ClosingFlags,
)
/** What the two calendar steps apply to (#287). */
private val calendarStepsApply: Flow<Pair<Boolean?, Boolean?>> =
combine(calendarsApplies, hiddenApplies, ::Pair)
val plan: StateFlow<OnboardingPlan?> =
combine(
hasPermission,
flags,
closingFlags,
prefs.onboardingProgress,
calendarPrefs.visibilityNoticePending.map { it == true },
backupApplies,
calendarsFlags,
) { granted, stored, closing, backup, calendars ->
calendarStepsApply,
) { granted, progress, visibilityArmed, backup, (calendars, hidden) ->
val answered = progress.answered
granted?.let {
onboardingPlan(
hasPermission = it,
remindersDone = stored.remindersDone,
wizardArmed = stored.wizardArmed,
backupDone = stored.backupDone,
viewDone = stored.viewDone,
monthStyleDone = stored.monthStyleDone,
remindersDone = OnboardingStep.Reminders in answered,
wizardArmed = progress.wizardArmed,
backupDone = OnboardingStep.Backup in answered,
viewDone = OnboardingStep.View in answered,
monthStyleDone = OnboardingStep.MonthStyle in answered,
backupApplies = backup,
hiddenDone = calendars.hiddenDone,
hiddenApplies = calendars.hiddenApplies,
calendarsDone = calendars.done,
calendarsApplies = calendars.applies,
visibilityArmed = closing.visibilityArmed,
visibilityDone = closing.visibilityDone,
doneShown = closing.doneShown,
hiddenDone = OnboardingStep.HiddenCalendars in answered,
hiddenApplies = hidden,
calendarsDone = OnboardingStep.Calendars in answered,
calendarsApplies = calendars,
visibilityArmed = visibilityArmed,
visibilityDone = OnboardingStep.Visibility in answered,
doneShown = OnboardingStep.Done in answered,
)
}
}.stateWhileSubscribed(
@@ -258,7 +237,7 @@ class OnboardingViewModel @Inject constructor(
* to a restarted process, leaving them the reminder step alone.
*/
private suspend fun armWizardIfFresh() {
if (!prefs.reminderOnboardingDone.first()) prefs.armOnboardingWizard()
prefs.armOnboardingWizard()
}
/**
@@ -274,7 +253,7 @@ class OnboardingViewModel @Inject constructor(
fun finishReminders(remindersEnabled: Boolean) {
viewModelScope.launch {
prefs.setRemindersEnabled(remindersEnabled)
prefs.setReminderOnboardingDone()
prefs.setOnboardingDone(OnboardingStep.Reminders)
// Nothing else re-arms the scan: turning reminders off cancels the
// alarm (#75).
scanner.scan()
@@ -288,24 +267,13 @@ class OnboardingViewModel @Inject constructor(
* returned to, which [OnboardingPlan.canGoBack] already refuses.
*/
fun goBack() {
viewModelScope.launch {
when (plan.value?.previous) {
OnboardingStep.HiddenCalendars -> prefs.setOnboardingHiddenCalendarsDone(false)
OnboardingStep.Calendars -> prefs.setOnboardingCalendarsDone(false)
OnboardingStep.Reminders -> prefs.setReminderOnboardingDone(false)
OnboardingStep.Backup -> prefs.setOnboardingBackupDone(false)
OnboardingStep.View -> prefs.setOnboardingViewDone(false)
OnboardingStep.MonthStyle -> prefs.setOnboardingMonthStyleDone(false)
OnboardingStep.Visibility -> prefs.setOnboardingVisibilityDone(false)
OnboardingStep.Done -> prefs.setOnboardingDoneShown(false)
OnboardingStep.Permission, null -> Unit
}
}
val previous = plan.value?.previous ?: return
viewModelScope.launch { prefs.setOnboardingDone(previous, done = false) }
}
/** Close the backup step without setting anything up. */
fun skipBackup() {
viewModelScope.launch { prefs.setOnboardingBackupDone() }
viewModelScope.launch { prefs.setOnboardingDone(OnboardingStep.Backup) }
}
/**
@@ -337,7 +305,7 @@ class OnboardingViewModel @Inject constructor(
/** Close the calendar step once its confirmation has been read. */
fun finishCalendars() {
viewModelScope.launch {
prefs.setOnboardingCalendarsDone()
prefs.setOnboardingDone(OnboardingStep.Calendars)
_creation.value = null
}
}
@@ -352,7 +320,7 @@ class OnboardingViewModel @Inject constructor(
* choice, and so is going off to set up an account and coming back.
*/
fun skipCalendars() {
viewModelScope.launch { prefs.setOnboardingCalendarsDone() }
viewModelScope.launch { prefs.setOnboardingDone(OnboardingStep.Calendars) }
}
/**
@@ -378,7 +346,7 @@ class OnboardingViewModel @Inject constructor(
)
// Immediate feedback: the first backup lands while onboarding runs.
BackupScheduler.runNow(context)
prefs.setOnboardingBackupDone()
prefs.setOnboardingDone(OnboardingStep.Backup)
}
}
@@ -392,11 +360,11 @@ class OnboardingViewModel @Inject constructor(
/** Close a picker step — the choice itself is saved as it is made. */
fun finishView() {
viewModelScope.launch { prefs.setOnboardingViewDone() }
viewModelScope.launch { prefs.setOnboardingDone(OnboardingStep.View) }
}
fun finishMonthStyle() {
viewModelScope.launch { prefs.setOnboardingMonthStyleDone() }
viewModelScope.launch { prefs.setOnboardingDone(OnboardingStep.MonthStyle) }
}
/** Switch one listed calendar on or off, from the hidden-calendars step (#287). */
@@ -433,7 +401,7 @@ class OnboardingViewModel @Inject constructor(
/** Close the hidden-calendars step, switched on or left as they were (#287). */
fun finishHiddenCalendars() {
viewModelScope.launch {
prefs.setOnboardingHiddenCalendarsDone()
prefs.setOnboardingDone(OnboardingStep.HiddenCalendars)
// The error belongs to the attempt, not to the step: stepping back
// into it later must not find a refusal nothing has asked for yet.
_hiddenWriteFailed.value = false
@@ -448,7 +416,7 @@ class OnboardingViewModel @Inject constructor(
/** Acknowledge the calendar-visibility notice (#75). */
fun finishVisibility() {
viewModelScope.launch {
prefs.setOnboardingVisibilityDone()
prefs.setOnboardingDone(OnboardingStep.Visibility)
// Retiring the notice drops its step, so it waits until nothing
// follows — the count must not fall on the way to a later screen.
if (plan.value?.steps?.last() == OnboardingStep.Visibility) retireVisibilityNotice()
@@ -485,36 +453,12 @@ class OnboardingViewModel @Inject constructor(
}
}
/** The stored answers the plan is derived from. */
private data class OnboardingFlags(
val remindersDone: Boolean,
val wizardArmed: Boolean,
val backupDone: Boolean,
val viewDone: Boolean,
val monthStyleDone: Boolean,
)
/** How the calendar step's create attempt ended (#287). */
sealed interface CalendarCreation {
data class Created(val name: String) : CalendarCreation
data object Failed : CalendarCreation
}
/** Both calendar steps' inputs (#287). */
private data class CalendarsFlags(
val applies: Boolean?,
val done: Boolean,
val hiddenApplies: Boolean?,
val hiddenDone: Boolean,
)
/** The tail of the flow: the visibility notice and the closing screen. */
private data class ClosingFlags(
val visibilityArmed: Boolean,
val visibilityDone: Boolean,
val doneShown: Boolean,
)
/** What the view step starts from, and renders its previews with. */
data class ViewChoice(
val defaultView: CalendarView,
@@ -49,7 +49,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import de.jeanlucmakiola.calendula.R
import de.jeanlucmakiola.calendula.data.appname.LauncherName
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
import de.jeanlucmakiola.floret.prefs.ThemeMode
import de.jeanlucmakiola.calendula.domain.FontRole
import de.jeanlucmakiola.calendula.ui.theme.BundledFont
import de.jeanlucmakiola.calendula.ui.theme.FONT_CUSTOM_TOKEN
@@ -3,7 +3,7 @@ package de.jeanlucmakiola.calendula.ui.settings
import de.jeanlucmakiola.floret.reminders.ReminderOverride
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
import de.jeanlucmakiola.floret.prefs.ThemeMode
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
import de.jeanlucmakiola.calendula.domain.CalendarSource
@@ -25,7 +25,7 @@ import de.jeanlucmakiola.floret.reminders.reminderOverrideFor
import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.data.prefs.SpecialDatesStalledReason
import de.jeanlucmakiola.calendula.data.prefs.ThemeMode
import de.jeanlucmakiola.floret.prefs.ThemeMode
import de.jeanlucmakiola.calendula.data.prefs.TimeFormatPref
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
import de.jeanlucmakiola.calendula.data.reminders.ReminderScanner
@@ -4,7 +4,7 @@ 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
import de.jeanlucmakiola.floret.prefs.ThemeMode
/**
* A synchronous copy of the theme choice, for the window backdrop drawn before
@@ -1,5 +1,6 @@
package de.jeanlucmakiola.calendula.data.prefs
import androidx.datastore.preferences.core.stringPreferencesKey
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.testDataStore
import de.jeanlucmakiola.calendula.testPrefs
@@ -39,7 +40,7 @@ class CalendarPrefsTest {
val prefs = CalendarPrefs(store)
store.updateData { p ->
val mutable = p.toMutablePreferences()
mutable[CalendarPrefs.HIDDEN_IDS_KEY] = "1,abc,3"
mutable[stringPreferencesKey("hidden_calendar_ids")] = "1,abc,3"
mutable
}
assertThat(prefs.hiddenCalendarIds.first()).isEqualTo(setOf(1L, 3L))
@@ -54,7 +55,7 @@ class CalendarPrefsTest {
val store = testDataStore(tempDir, "test_prefs")
val prefs = CalendarPrefs(store)
store.updateData { p ->
p.toMutablePreferences().apply { this[CalendarPrefs.DISABLED_IDS_KEY] = "2,9" }
p.toMutablePreferences().apply { this[stringPreferencesKey("disabled_calendar_ids")] = "2,9" }
}
assertThat(prefs.pendingDisabledCalendarIds.first()).isEqualTo(setOf(2L, 9L))
}
@@ -5,6 +5,7 @@ import de.jeanlucmakiola.calendula.testSettings
import de.jeanlucmakiola.floret.reminders.ReminderOverride
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.stringPreferencesKey
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.domain.EventFormField
import de.jeanlucmakiola.calendula.domain.FontRole
@@ -16,6 +17,8 @@ import de.jeanlucmakiola.calendula.ui.common.QuickSwitchConfig
import de.jeanlucmakiola.calendula.ui.common.TimelineHours
import de.jeanlucmakiola.calendula.ui.common.TimelineScale
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.onboarding.OnboardingStep
import de.jeanlucmakiola.floret.prefs.ThemeMode
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.DayOfWeek
@@ -68,7 +71,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.WEEK_START_KEY] = "MONDAY"
m[stringPreferencesKey("week_start")] = "MONDAY"
m
}
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Day(DayOfWeek.MONDAY))
@@ -80,7 +83,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.WEEK_START_KEY] = "FUNDAY"
m[stringPreferencesKey("week_start")] = "FUNDAY"
m
}
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.Auto)
@@ -191,7 +194,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.MONTH_VIEW_STYLE_KEY] = "Carousel"
m[stringPreferencesKey("month_view_style")] = "Carousel"
m
}
assertThat(prefs.monthViewStyle.first()).isEqualTo(MonthViewStyle.Paged)
@@ -203,7 +206,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.THEME_MODE_KEY] = "PLAID"
m[stringPreferencesKey("theme_mode")] = "PLAID"
m
}
assertThat(prefs.themeMode.first()).isEqualTo(ThemeMode.SYSTEM)
@@ -269,7 +272,7 @@ class SettingsPrefsTest {
// Only Day (disabled) and Week stored, plus a bogus name; Month & Agenda absent.
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.QUICK_SWITCH_VIEWS_KEY] = "!Day,Week,Hologram"
m[stringPreferencesKey("quick_switch_views")] = "!Day,Week,Hologram"
m
}
val config = prefs.quickSwitchConfig.first()
@@ -322,7 +325,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.DRAWER_VIEW_ORDER_KEY] = "Agenda,Nope,Week"
m[stringPreferencesKey("drawer_view_order")] = "Agenda,Nope,Week"
m
}
assertThat(prefs.drawerViewOrder.first()).containsExactly(
@@ -337,7 +340,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.FORM_FIELDS_KEY] = "Location,Hologram"
m[stringPreferencesKey("event_form_default_fields")] = "Location,Hologram"
m
}
assertThat(prefs.defaultFormFields.first()).containsExactly(EventFormField.Location)
@@ -347,7 +350,7 @@ class SettingsPrefsTest {
fun `reminders default to enabled, onboarding to not done`(@TempDir tempDir: Path) = runTest {
val prefs = testSettings(tempDir)
assertThat(prefs.remindersEnabled.first()).isTrue()
assertThat(prefs.reminderOnboardingDone.first()).isFalse()
assertThat(prefs.onboardingProgress.first().answered).isEmpty()
}
@Test
@@ -360,24 +363,35 @@ class SettingsPrefsTest {
@Test
fun `reminder onboarding completes one-way`(@TempDir tempDir: Path) = runTest {
val prefs = testSettings(tempDir)
prefs.setReminderOnboardingDone()
assertThat(prefs.reminderOnboardingDone.first()).isTrue()
prefs.setOnboardingDone(OnboardingStep.Reminders)
assertThat(prefs.onboardingProgress.first().answered).containsExactly(OnboardingStep.Reminders)
}
@Test
fun `a step's answer can be cleared again`(@TempDir tempDir: Path) = runTest {
val prefs = testSettings(tempDir)
prefs.setOnboardingDone(OnboardingStep.Backup)
prefs.setOnboardingDone(OnboardingStep.View)
prefs.setOnboardingDone(OnboardingStep.Backup, done = false)
assertThat(prefs.onboardingProgress.first().answered).containsExactly(OnboardingStep.View)
}
@Test
fun `the wizard arms on a first run`(@TempDir tempDir: Path) = runTest {
val prefs = testSettings(tempDir)
prefs.armOnboardingWizard()
assertThat(prefs.onboardingWizardArmed.first()).isTrue()
assertThat(prefs.onboardingProgress.first().wizardArmed).isTrue()
}
@Test
fun `the wizard does not arm once the reminder step is answered`(@TempDir tempDir: Path) = runTest {
// An existing install re-granting the permission is not re-onboarded.
val prefs = testSettings(tempDir)
prefs.setReminderOnboardingDone()
prefs.setOnboardingDone(OnboardingStep.Reminders)
prefs.armOnboardingWizard()
assertThat(prefs.onboardingWizardArmed.first()).isFalse()
assertThat(prefs.onboardingProgress.first().wizardArmed).isFalse()
}
@Test
@@ -386,8 +400,8 @@ class SettingsPrefsTest {
prefs.armOnboardingWizard()
prefs.finishOnboardingWizard()
assertThat(prefs.onboardingDoneShown.first()).isTrue()
assertThat(prefs.onboardingWizardArmed.first()).isFalse()
assertThat(prefs.onboardingProgress.first().answered).containsExactly(OnboardingStep.Done)
assertThat(prefs.onboardingProgress.first().wizardArmed).isFalse()
}
@Test
@@ -415,7 +429,7 @@ class SettingsPrefsTest {
// The pre-multi-reminder format stored a bare integer string.
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.DEFAULT_REMINDER_KEY] = "30"
m[stringPreferencesKey("default_reminder_minutes")] = "30"
m
}
assertThat(prefs.defaultReminderMinutes.first()).containsExactly(30)
@@ -427,7 +441,7 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.DEFAULT_REMINDER_KEY] = "soon"
m[stringPreferencesKey("default_reminder_minutes")] = "soon"
m
}
assertThat(prefs.defaultReminderMinutes.first()).isEmpty()
@@ -464,7 +478,7 @@ class SettingsPrefsTest {
// reminders. Partially-valid values keep their salvageable minutes.
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.CALENDAR_REMINDER_OVERRIDE_KEY] = "5=none;7=abc;8=;9=30,abc;11=15"
m[stringPreferencesKey("per_calendar_reminder_override")] = "5=none;7=abc;8=;9=30,abc;11=15"
m
}
assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(
@@ -591,7 +605,7 @@ class SettingsPrefsTest {
store.updateData { p ->
val m = p.toMutablePreferences()
// A bad id, a non-numeric length, one out of range, and a good entry.
m[SettingsPrefs.CALENDAR_EVENT_DURATION_KEY] = "x=60;7=soon;8=99999;9=45"
m[stringPreferencesKey("per_calendar_event_duration")] = "x=60;7=soon;8=99999;9=45"
m
}
assertThat(prefs.perCalendarEventDuration.first()).containsExactly(9L, 45)