From 3ed12f1feee401e5e66c5f87178e79ce7b0107b4 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 16 Oct 2026 13:59:52 +0200 Subject: [PATCH] One rollover-following today flow for the view models --- .../calendula/data/time/TodaySource.kt | 22 +++++++++++++++++++ .../calendula/ui/agenda/AgendaViewModel.kt | 15 ++++--------- .../calendula/ui/month/MonthViewModel.kt | 12 +++------- .../calendula/ui/week/WeekViewModel.kt | 13 +++-------- 4 files changed, 32 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt index 0653c41..83f03c3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt @@ -4,10 +4,14 @@ import android.content.Context import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext import de.jeanlucmakiola.floret.android.broadcastTicks +import de.jeanlucmakiola.floret.android.stateWhileSubscribed +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.transformLatest import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone @@ -43,3 +47,21 @@ class AndroidTodaySource @Inject constructor( private const val MINUTE_MILLIS = 60_000L fun currentDate(): LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + +/** + * Today as a hot flow seeded with [initial]. [onRollover] runs with the previous + * and the new date each time the day changes while collected, so a screen left + * open over midnight can move off yesterday (#373). + */ +fun Flow.stateFollowingDays( + scope: CoroutineScope, + initial: LocalDate, + onRollover: (old: LocalDate, new: LocalDate) -> Unit, +): StateFlow { + var last = initial + return onEach { date -> + val old = last + last = date + if (old != date) onRollover(old, date) + }.stateWhileSubscribed(scope, initial) +} diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt index aee15c6..06ddf5d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt @@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.firstDayOfWeek import de.jeanlucmakiola.calendula.data.time.TodaySource import de.jeanlucmakiola.calendula.data.time.currentDate +import de.jeanlucmakiola.calendula.data.time.stateFollowingDays import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.FailureReason @@ -26,7 +27,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek @@ -98,21 +98,14 @@ class AgendaViewModel @Inject constructor( var lastTopDate: LocalDate? = null private set - private var lastToday = todayDate - /** * Today, kept current across midnight (#373). An agenda still starting on * yesterday moves on to the new day; one jumped elsewhere stays put. */ - private val today: StateFlow = todaySource.today - .onEach { date -> - val old = lastToday - lastToday = date - if (old != date && _window.value.anchor == old) { - _window.value = _window.value.copy(anchor = date) - } + private val today: StateFlow = + todaySource.today.stateFollowingDays(viewModelScope, todayDate) { old, date -> + if (_window.value.anchor == old) _window.value = _window.value.copy(anchor = date) } - .stateWhileSubscribed(viewModelScope, todayDate) // A transient, in-view override of the saved agenda range. Held in memory // (not persisted), so it survives view switches and rotation within the diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt index 4ea3034..e84d382 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthViewModel.kt @@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.firstDayOfWeek import de.jeanlucmakiola.calendula.data.time.TodaySource import de.jeanlucmakiola.calendula.data.time.currentDate +import de.jeanlucmakiola.calendula.data.time.stateFollowingDays import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.FailureReason @@ -90,16 +91,9 @@ class MonthViewModel @Inject constructor( private val todayDate: LocalDate get() = currentDate() - private var lastToday = todayDate - /** Today, kept current across midnight so a grid left open doesn't mark yesterday (#373). */ - val today: StateFlow = todaySource.today - .onEach { date -> - val old = lastToday - lastToday = date - if (old != date) followToday(old, date) - } - .stateWhileSubscribed(viewModelScope, todayDate) + val today: StateFlow = + todaySource.today.stateFollowingDays(viewModelScope, todayDate, ::followToday) private val _month = MutableStateFlow(todayDate.yearMonth) val month: StateFlow = _month diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt index 3655532..4128ef7 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt @@ -10,6 +10,7 @@ import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay import de.jeanlucmakiola.calendula.data.time.TodaySource import de.jeanlucmakiola.calendula.data.time.ZoneSource import de.jeanlucmakiola.calendula.data.time.currentDate +import de.jeanlucmakiola.calendula.data.time.stateFollowingDays import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.displayPriority @@ -32,7 +33,6 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek @@ -124,16 +124,9 @@ abstract class PagedTimelineViewModel( val rule: StateFlow<(LocalDate) -> PageGrid> = gridRule.stateIn(viewModelScope, SharingStarted.Eagerly, initialRule) - private var lastToday = todayDate - /** Today, kept current across midnight so a page left open doesn't mark yesterday (#373). */ - private val today: StateFlow = todaySource.today - .onEach { date -> - val old = lastToday - lastToday = date - if (old != date) followToday(old, date) - } - .stateWhileSubscribed(viewModelScope, todayDate) + private val today: StateFlow = + todaySource.today.stateFollowingDays(viewModelScope, todayDate, ::followToday) private val position: StateFlow = combine(_anchor, rule) { anchor, rule -> positionOf(anchor, rule) }