diff --git a/CHANGELOG.md b/CHANGELOG.md index e2da984b..0a7c7cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.22.5] — 2026-10-07 + +### Fixed +- **Reopening the app the next morning shows the right day.** The month view used to mark yesterday as today and keep the day you picked the evening before selected, so you had to tap today to see its events. Every view now moves on to the new day at midnight and when you come back to the app. A view you browsed to another date stays where it is ([#373]). + ## [2.22.4] — 2026-10-07 ### Fixed @@ -1843,3 +1848,4 @@ automatically, with zero telemetry and no internet permission. [#361]: https://codeberg.org/jlmakiola/calendula/issues/361 [#363]: https://codeberg.org/jlmakiola/calendula/issues/363 [#364]: https://codeberg.org/jlmakiola/calendula/issues/364 +[#373]: https://codeberg.org/jlmakiola/calendula/issues/373 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8cc02ad7..156b73fc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,8 +32,8 @@ android { // (up to 2.22.3: 2.22.3 -> 22203; from 2.22.4: 2.23.0-beta.1 -> 2230001, // 2.22.4 -> 2220499), and CI fails if the committed one doesn't match. // See docs/RELEASING.md. - versionCode = 2220499 - versionName = "2.22.4" + versionCode = 2220599 + versionName = "2.22.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/di/DataModule.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/di/DataModule.kt index 963f11c7..e11dabb3 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/di/DataModule.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/di/DataModule.kt @@ -20,6 +20,8 @@ import de.jeanlucmakiola.calendula.data.contacts.ContactSpecialDatesDataSource import de.jeanlucmakiola.calendula.data.contacts.SpecialDatesCalendarSpec import de.jeanlucmakiola.calendula.data.reminders.ProviderReminderInstanceSource import de.jeanlucmakiola.calendula.data.reminders.ReminderInstanceSource +import de.jeanlucmakiola.calendula.data.time.AndroidTodaySource +import de.jeanlucmakiola.calendula.data.time.TodaySource import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import javax.inject.Singleton @@ -61,6 +63,12 @@ abstract class DataBindModule { abstract fun bindSpecialDatesCalendarSpec( impl: AndroidSpecialDatesCalendarSpec, ): SpecialDatesCalendarSpec + + @Binds + @Singleton + abstract fun bindTodaySource( + impl: AndroidTodaySource, + ): TodaySource } @Module 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 new file mode 100644 index 00000000..9ff1394f --- /dev/null +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/time/TodaySource.kt @@ -0,0 +1,63 @@ +package de.jeanlucmakiola.calendula.data.time + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.transformLatest +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import javax.inject.Inject +import kotlin.time.Clock + +/** The current local date, re-emitted whenever it changes (#373). */ +interface TodaySource { + val today: Flow +} + +/** + * Re-reads the date on every minute boundary while collected, and at once on the + * system's date, time and zone broadcasts, which can arrive late or not at all. + */ +@OptIn(ExperimentalCoroutinesApi::class) +class AndroidTodaySource @Inject constructor( + @ApplicationContext private val context: Context, +) : TodaySource { + + override val today: Flow = callbackFlow { + val receiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + trySend(Unit) + } + } + val filter = IntentFilter().apply { + addAction(Intent.ACTION_DATE_CHANGED) + addAction(Intent.ACTION_TIME_CHANGED) + addAction(Intent.ACTION_TIMEZONE_CHANGED) + } + ContextCompat.registerReceiver(context, receiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED) + awaitClose { context.unregisterReceiver(receiver) } + } + .onStart { emit(Unit) } + .transformLatest { + while (true) { + emit(currentDate()) + delay(MINUTE_MILLIS - Clock.System.now().toEpochMilliseconds() % MINUTE_MILLIS) + } + } + .distinctUntilChanged() +} + +private const val MINUTE_MILLIS = 60_000L + +fun currentDate(): LocalDate = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt index 2fafe096..727a933f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -150,6 +150,15 @@ fun CalendarHost( // The date every view opens on and carries along (#184); tapping a day // anywhere focuses it and opens the day view there. val focus = rememberSaveable(saver = ViewFocusSaver) { ViewFocus(localToday()) } + // A focus still on yesterday follows the date over, so the next view opened + // doesn't land on the old day (#373); one moved elsewhere stays put. + val today = viewModel.today.collectAsStateWithLifecycle().value + var focusToday by rememberSaveable { mutableStateOf(today.toString()) } + LaunchedEffect(today) { + val old = LocalDate.parse(focusToday) + if (old != today && focus.date == old) focus.date = today + focusToday = today.toString() + } val switchScope = rememberCoroutineScope() val switchTo: ((List) -> List) -> Unit = { move -> diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt index 70cabf6a..789e9a71 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHostViewModel.kt @@ -4,12 +4,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs +import de.jeanlucmakiola.calendula.data.time.TodaySource +import de.jeanlucmakiola.calendula.data.time.currentDate import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.IMPLEMENTED_VIEWS import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.datetime.LocalDate import javax.inject.Inject /** @@ -21,8 +24,16 @@ import javax.inject.Inject @HiltViewModel class CalendarHostViewModel @Inject constructor( prefs: SettingsPrefs, + todaySource: TodaySource, ) : ViewModel() { + /** Today, kept current across midnight so the shared focus can follow it (#373). */ + val today: StateFlow = todaySource.today.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = currentDate(), + ) + val defaultView: StateFlow = prefs.defaultView.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), 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 e894f409..ae372712 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 @@ -8,6 +8,8 @@ import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.prefs.PastEventDisplay 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.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.FailureReason @@ -21,6 +23,7 @@ import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek @@ -30,8 +33,6 @@ import kotlinx.datetime.atStartOfDayIn import kotlinx.datetime.atTime import kotlinx.datetime.plus import kotlinx.datetime.toInstant -import kotlinx.datetime.toLocalDateTime -import kotlin.time.Clock import kotlin.time.Instant import javax.inject.Inject @@ -40,6 +41,7 @@ import javax.inject.Inject class AgendaViewModel @Inject constructor( private val repository: CalendarRepository, settingsPrefs: SettingsPrefs, + todaySource: TodaySource, @IoDispatcher private val io: CoroutineDispatcher, ) : ViewModel() { @@ -84,11 +86,25 @@ class AgendaViewModel @Inject constructor( private val zone = TimeZone.currentSystemDefault() private val todayDate: LocalDate - get() = Clock.System.now().toLocalDateTime(zone).date + get() = currentDate() private val _anchor = MutableStateFlow(todayDate) val anchor: StateFlow = _anchor + 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 && _anchor.value == old) _anchor.value = date + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), 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 // session but resets to the saved default when the app is relaunched. @@ -113,8 +129,9 @@ class AgendaViewModel @Inject constructor( combine( repository.calendars(), repository.instances(window), - ) { calendars, instances -> - buildState(params, calendars, instances) + today, + ) { calendars, instances, today -> + buildState(params, calendars, instances, today) } } .catch { emit(AgendaUiState.Failure(FailureReason.ProviderUnavailable)) } @@ -156,6 +173,7 @@ class AgendaViewModel @Inject constructor( params: AgendaParams, calendars: List, instances: List, + today: LocalDate, ): AgendaUiState { calendarListFailure(calendars)?.let { return AgendaUiState.Failure(it) } val anchor = params.anchor @@ -166,7 +184,7 @@ class AgendaViewModel @Inject constructor( val days = groupAgendaDays(anchor, rangeEnd, instances, zone) return AgendaUiState.Success( anchor = anchor, - today = todayDate, + today = today, days = days, range = params.range, rangeIsOverride = params.rangeIsOverride, 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 8dd73bb4..d4ed1cb3 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 @@ -87,6 +87,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -197,13 +198,17 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.plus import kotlinx.datetime.YearMonth import kotlinx.datetime.toJavaLocalDate -import kotlinx.datetime.toLocalDateTime -import kotlin.time.Clock import kotlin.time.Instant import java.time.format.TextStyle as JavaTextStyle import java.util.Locale private val LocalTodayPulse = staticCompositionLocalOf { 0 } + +/** Live today for the pages, whose cached states may have been built before midnight (#373). */ +private val LocalMonthToday = compositionLocalOf { null } + +private fun MonthUiState.Success.withToday(today: LocalDate?): MonthUiState.Success = + if (today == null || today == this.today) this else copy(today = today) private val LocalOpenWeek = staticCompositionLocalOf<((LocalDate) -> Unit)?> { null } /** A brief push from today's full cell into its immediate neighbours. */ @@ -319,12 +324,7 @@ fun MonthScreen( } val dense = viewStyle == MonthViewStyle.Dense - // Today, from whichever state is driving; the clock only covers the first - // frame, before either has loaded. - val today = when { - scrolling -> (continuousState as? ContinuousMonthUiState.Success)?.today - else -> (state as? MonthUiState.Success)?.today - } ?: Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + val today by viewModel.today.collectAsStateWithLifecycle() // The two scrolling styles index their items differently — Continuous by // month (which the week start can't move), Dense by week (which it can) — so @@ -520,6 +520,7 @@ fun MonthScreen( LocalDimCutoff provides dimCutoff, LocalMonthDrag provides chipDrag, LocalTodayPulse provides todayPulse, + LocalMonthToday provides today, LocalOpenWeek provides onOpenWeek, ) { if (scrolling) { @@ -773,6 +774,7 @@ private fun MonthPage( ) { val ym = yearMonthForIndex(index) val state by remember(ym, weekStart) { monthState(ym, weekStart) }.collectAsStateWithLifecycle() + val today = LocalMonthToday.current val chipDrag = LocalMonthDrag.current val morph = LocalMonthMorph.current val viewMorph = LocalViewMorph.current @@ -782,7 +784,7 @@ private fun MonthPage( LocalViewMorph provides viewMorph.takeIf { morphActive }, ) { when (val s = state) { - is MonthUiState.Success -> page(index, s) + is MonthUiState.Success -> page(index, s.withToday(today)) else -> loading() } } @@ -1388,7 +1390,8 @@ private fun SplitMonthContent( val ym = yearMonthForIndex(index) val pageState by remember(ym, weekStart) { monthState(ym, weekStart) } .collectAsStateWithLifecycle() - val page = pageState as? MonthUiState.Success ?: return@HorizontalPager + val page = (pageState as? MonthUiState.Success)?.withToday(LocalMonthToday.current) + ?: return@HorizontalPager // Only the settled page carries the drag and the morph tags: its // neighbours repeat some of its dates and would claim them. val active = index == pagerState.settledPage 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 cd227711..7964eec9 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 @@ -7,6 +7,8 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.di.IoDispatcher 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.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.FailureReason @@ -45,7 +47,6 @@ import kotlinx.datetime.minus import kotlinx.datetime.plus import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime -import kotlin.time.Clock import kotlin.time.Instant import javax.inject.Inject @@ -57,6 +58,7 @@ private const val MONTH_CACHE_SPAN = 2 class MonthViewModel @Inject constructor( private val repository: CalendarRepository, settingsPrefs: SettingsPrefs, + todaySource: TodaySource, @IoDispatcher private val io: CoroutineDispatcher, ) : ViewModel() { @@ -90,7 +92,18 @@ class MonthViewModel @Inject constructor( ) private val todayDate: LocalDate - get() = Clock.System.now().toLocalDateTime(zone).date + 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) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate) private val _month = MutableStateFlow(YearMonth(todayDate.year, todayDate.month)) val month: StateFlow = _month @@ -139,8 +152,9 @@ class MonthViewModel @Inject constructor( combine( repository.calendars(), repository.instances(monthGridRange(ym, ws, zone)), - ) { calendars, instances -> - buildState(ym, ws, calendars, instances) + today, + ) { calendars, instances, today -> + buildState(ym, ws, calendars, instances, today) } .catch { emit(MonthUiState.Failure(FailureReason.ProviderUnavailable)) } .flowOn(io) @@ -211,8 +225,9 @@ class MonthViewModel @Inject constructor( combine( repository.calendars(), repository.instances(range), - ) { calendars, instances -> - buildContinuousState(window, ws, style, calendars, instances) + today, + ) { calendars, instances, today -> + buildContinuousState(window, ws, style, calendars, instances, today) } } // A load landing is the cue to reach further out. Widening from here @@ -257,6 +272,7 @@ class MonthViewModel @Inject constructor( style: MonthViewStyle, calendars: List, instances: List, + today: LocalDate, ): ContinuousMonthUiState { calendarListFailure(calendars)?.let { return ContinuousMonthUiState.Failure(it) } // Bucketed once for the whole window: a row then takes the handful of @@ -283,7 +299,7 @@ class MonthViewModel @Inject constructor( emptyMap() } return ContinuousMonthUiState.Success( - today = todayDate, + today = today, monthsByIndex = months, weeksByIndex = weeks, weekStart = weekStart, @@ -333,6 +349,18 @@ class MonthViewModel @Inject constructor( _selectedDate.value = null } + /** + * The date rolled over from [old] to [new]. A paged grid still on [old]'s + * month moves on to [new]'s and drops the day picked yesterday; one browsed + * elsewhere stays put. The scrolling styles only repaint the today marker. + */ + private fun followToday(old: LocalDate, new: LocalDate) { + if (viewStyle.value.isScrolling) return + if (_month.value != YearMonth(old.year, old.month)) return + _month.value = YearMonth(new.year, new.month) + _selectedDate.value = null + } + /** * Track [_month] to the month a scrolling style is showing. Those styles * navigate by scroll position and never set [_month] themselves, so it would @@ -358,12 +386,13 @@ class MonthViewModel @Inject constructor( weekStart: DayOfWeek, calendars: List, instances: List, + today: LocalDate, ): MonthUiState { calendarListFailure(calendars)?.let { return MonthUiState.Failure(it) } val weeks = layoutMonthWeeks(ym, weekStart, instances, zone) return MonthUiState.Success( month = ym, - today = todayDate, + today = today, weeks = weeks, instancesByDay = instancesByDay(weeks.flatMap { it.days }, instances, zone), zone = zone, 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 007fd06c..384ed636 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 @@ -461,6 +461,7 @@ private fun PagedTimelineScreen( CompositionLocalProvider(LocalDimCutoff provides dimCutoff) { WeekContent( state = state, + today = today, pagerState = pagerState, grid = grid, kind = kind, @@ -482,6 +483,7 @@ private fun PagedTimelineScreen( @Composable private fun WeekContent( state: WeekUiState, + today: LocalDate, pagerState: PagerState, grid: PageGrid, kind: TimelineKind, @@ -536,7 +538,7 @@ private fun WeekContent( is WeekUiState.Success -> if (morph != null) { TimelineMorphFrame( morph = morph, - today = s.today, + today = today, scrollState = scrollState, dragController = dragController, onEventClick = onEventClick, @@ -549,7 +551,7 @@ private fun WeekContent( grid = grid, kind = kind, span = span, - today = s.today, + today = today, initialStripHeight = handoffStrip ?: s.allDayStripHeight(), scrollState = scrollState, dragController = dragController, @@ -618,7 +620,10 @@ private fun WeekPager( } ViewMorphPage(active = morphPageActive(page, pagerState.settledPage)) { WeekPage( - state = loaded ?: empty, + // A cached page may predate midnight; today comes off the clock (#373). + state = remember(loaded, empty, today) { + loaded?.takeIf { it.today != today }?.copy(today = today) ?: loaded ?: empty + }, headerBias = kind.headerBias, allDayHeight = allDayHeight, scrollState = scrollState, 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 e226fe67..5561e973 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 @@ -7,6 +7,8 @@ import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay +import de.jeanlucmakiola.calendula.data.time.TodaySource +import de.jeanlucmakiola.calendula.data.time.currentDate import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.domain.displayPriority @@ -27,6 +29,7 @@ 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 @@ -40,7 +43,6 @@ import kotlinx.datetime.plus import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import java.util.Locale -import kotlin.time.Clock import kotlin.time.Instant import javax.inject.Inject import kotlin.math.abs @@ -73,6 +75,7 @@ fun positionOf(anchor: LocalDate, rule: (LocalDate) -> PageGrid): PagerPosition abstract class PagedTimelineViewModel( private val repository: CalendarRepository, settingsPrefs: SettingsPrefs, + todaySource: TodaySource, private val io: CoroutineDispatcher, gridRule: Flow<(LocalDate) -> PageGrid>, initialRule: (LocalDate) -> PageGrid, @@ -89,7 +92,7 @@ abstract class PagedTimelineViewModel( ) private val todayDate: LocalDate - get() = Clock.System.now().toLocalDateTime(zone).date + get() = currentDate() // A day on the visible page; the page itself comes from the grid, so a // settings change re-frames the same days instead of jumping. @@ -103,6 +106,17 @@ 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) + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000L), todayDate) + private val position: StateFlow = combine(_anchor, rule) { anchor, rule -> positionOf(anchor, rule) } .distinctUntilChanged() @@ -141,8 +155,9 @@ abstract class PagedTimelineViewModel( combine( repository.calendars(), repository.instances(spanRange(span.start, span.count, zone)), - ) { calendars, instances -> - buildState(span, calendars, instances) + today, + ) { calendars, instances, today -> + buildState(span, calendars, instances, today) } .catch { emit(WeekUiState.Failure(FailureReason.ProviderUnavailable)) } .flowOn(io) @@ -163,12 +178,20 @@ abstract class PagedTimelineViewModel( } fun goToToday() = jumpTo(todayDate) + + /** The date rolled over; a page still showing [old] follows on to [new], one browsed elsewhere stays. */ + private fun followToday(old: LocalDate, new: LocalDate) { + val anchor = _anchor.value + val grid = rule.value(anchor) + if (grid.pageFor(anchor) == grid.pageFor(old)) _anchor.value = new + } fun goToDate(date: LocalDate) = jumpTo(date) private fun buildState( span: DaySpan, calendars: List, instances: List, + today: LocalDate, ): WeekUiState { calendarListFailure(calendars)?.let { return WeekUiState.Failure(it) } val days = (0 until span.count).map { span.start.plus(it, DateTimeUnit.DAY) } @@ -176,7 +199,7 @@ abstract class PagedTimelineViewModel( val timed = instances.filterNot { it.isAllDay } return WeekUiState.Success( weekStart = span.start, - today = todayDate, + today = today, days = days, allDaySpans = layoutAllDay(allDay, days, zone), timedByDay = days.associateWith { day -> layoutDay(timed, day, zone) }, @@ -189,10 +212,12 @@ abstract class PagedTimelineViewModel( class WeekViewModel @Inject constructor( repository: CalendarRepository, settingsPrefs: SettingsPrefs, + todaySource: TodaySource, @IoDispatcher io: CoroutineDispatcher, ) : PagedTimelineViewModel( repository = repository, settingsPrefs = settingsPrefs, + todaySource = todaySource, io = io, gridRule = settingsPrefs.weekStart.map { pref -> val grid = WeekGrid(pref.resolveFirstDay(Locale.getDefault())) @@ -211,10 +236,12 @@ class WeekViewModel @Inject constructor( class MultiDayViewModel @Inject constructor( repository: CalendarRepository, settingsPrefs: SettingsPrefs, + todaySource: TodaySource, @IoDispatcher io: CoroutineDispatcher, ) : PagedTimelineViewModel( repository = repository, settingsPrefs = settingsPrefs, + todaySource = todaySource, io = io, gridRule = settingsPrefs.multiDayCount.map { count -> { anchor: LocalDate -> DaysGrid.startingAt(anchor, count) } @@ -227,10 +254,12 @@ class MultiDayViewModel @Inject constructor( class DayViewModel @Inject constructor( repository: CalendarRepository, settingsPrefs: SettingsPrefs, + todaySource: TodaySource, @IoDispatcher io: CoroutineDispatcher, ) : PagedTimelineViewModel( repository = repository, settingsPrefs = settingsPrefs, + todaySource = todaySource, io = io, gridRule = flowOf { _: LocalDate -> DayGrid }, initialRule = { DayGrid }, diff --git a/fastlane/metadata/android/ar/changelogs/2220599.txt b/fastlane/metadata/android/ar/changelogs/2220599.txt new file mode 100644 index 00000000..a616c5e5 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/2220599.txt @@ -0,0 +1,2 @@ +إصلاحات +• عند فتح التطبيق في صباح اليوم التالي يظهر اليوم الصحيح. كان عرض الشهر يعلّم الأمس على أنه اليوم ويُبقي اليوم الذي اخترته في المساء السابق محددًا. الآن تنتقل كل العروض إلى اليوم الجديد. diff --git a/fastlane/metadata/android/be/changelogs/2220599.txt b/fastlane/metadata/android/be/changelogs/2220599.txt new file mode 100644 index 00000000..4db4a406 --- /dev/null +++ b/fastlane/metadata/android/be/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Выпраўлена +• Калі адкрыць праграму наступнай раніцай, яна паказвае правільны дзень. Раней месяц адзначаў учорашні дзень як сённяшні і пакідаў выбраным дзень, які вы абралі ўвечары. Цяпер усе віды пераходзяць на новы дзень. diff --git a/fastlane/metadata/android/cs-CZ/changelogs/2220599.txt b/fastlane/metadata/android/cs-CZ/changelogs/2220599.txt new file mode 100644 index 00000000..1d4f4cb7 --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Opraveno +• Když aplikaci otevřete druhý den ráno, ukáže správný den. Měsíc dřív označoval včerejšek jako dnešek a nechával vybraný den, který jste zvolili večer předtím. Všechna zobrazení teď přejdou na nový den. diff --git a/fastlane/metadata/android/de-DE/changelogs/2220599.txt b/fastlane/metadata/android/de-DE/changelogs/2220599.txt new file mode 100644 index 00000000..58bfe644 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Behoben +• Wird die App am nächsten Morgen geöffnet, zeigt sie den richtigen Tag. Die Monatsansicht markierte bisher gestern als heute und ließ den am Vorabend gewählten Tag ausgewählt. Jetzt wechseln alle Ansichten auf den neuen Tag. diff --git a/fastlane/metadata/android/en-US/changelogs/2220599.txt b/fastlane/metadata/android/en-US/changelogs/2220599.txt new file mode 100644 index 00000000..90b03343 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Fixed +• Reopening the app the next morning shows the right day. The month view used to mark yesterday as today and keep the day you picked the evening before selected. All views now move on to the new day. diff --git a/fastlane/metadata/android/es-ES/changelogs/2220599.txt b/fastlane/metadata/android/es-ES/changelogs/2220599.txt new file mode 100644 index 00000000..4f9aaa1e --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Corregido +• Al abrir la app a la mañana siguiente, se muestra el día correcto. La vista de mes marcaba ayer como hoy y dejaba seleccionado el día que elegiste la noche anterior. Ahora todas las vistas pasan al nuevo día. diff --git a/fastlane/metadata/android/fr-FR/changelogs/2220599.txt b/fastlane/metadata/android/fr-FR/changelogs/2220599.txt new file mode 100644 index 00000000..94962d54 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Corrigé +• En ouvrant l'app le lendemain matin, vous voyez le bon jour. La vue mois marquait la veille comme aujourd'hui et gardait sélectionné le jour choisi la veille au soir. Toutes les vues passent maintenant au nouveau jour. diff --git a/fastlane/metadata/android/hu-HU/changelogs/2220599.txt b/fastlane/metadata/android/hu-HU/changelogs/2220599.txt new file mode 100644 index 00000000..ae3ba356 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Javítva +• Ha másnap reggel megnyitod az alkalmazást, a helyes napot mutatja. A havi nézet eddig a tegnapot jelölte mai napként, és kijelölve hagyta az előző este választott napot. Most minden nézet átvált az új napra. diff --git a/fastlane/metadata/android/it-IT/changelogs/2220599.txt b/fastlane/metadata/android/it-IT/changelogs/2220599.txt new file mode 100644 index 00000000..ccdbf414 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Corretto +• Aprendo l'app la mattina dopo, viene mostrato il giorno giusto. La vista mese segnava ieri come oggi e lasciava selezionato il giorno scelto la sera prima. Ora tutte le viste passano al nuovo giorno. diff --git a/fastlane/metadata/android/ja-JP/changelogs/2220599.txt b/fastlane/metadata/android/ja-JP/changelogs/2220599.txt new file mode 100644 index 00000000..450e99f0 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/2220599.txt @@ -0,0 +1,2 @@ +修正 +• 翌朝アプリを開くと、正しい日が表示されるようになりました。これまで月表示では前日が今日として表示され、前の晩に選んだ日も選択されたままでした。今はすべての表示が新しい日に切り替わります。 diff --git a/fastlane/metadata/android/nl-NL/changelogs/2220599.txt b/fastlane/metadata/android/nl-NL/changelogs/2220599.txt new file mode 100644 index 00000000..cea0f723 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Opgelost +• Open je de app de volgende ochtend, dan zie je de juiste dag. De maandweergave markeerde gisteren als vandaag en hield de dag die je de avond ervoor koos geselecteerd. Alle weergaven gaan nu mee naar de nieuwe dag. diff --git a/fastlane/metadata/android/pl-PL/changelogs/2220599.txt b/fastlane/metadata/android/pl-PL/changelogs/2220599.txt new file mode 100644 index 00000000..b54f4be6 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Poprawki +• Po otwarciu aplikacji następnego ranka widać właściwy dzień. Widok miesiąca oznaczał wczoraj jako dziś i zostawiał zaznaczony dzień wybrany poprzedniego wieczoru. Teraz wszystkie widoki przechodzą na nowy dzień. diff --git a/fastlane/metadata/android/pt-BR/changelogs/2220599.txt b/fastlane/metadata/android/pt-BR/changelogs/2220599.txt new file mode 100644 index 00000000..2187edfc --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Corrigido +• Ao abrir o app na manhã seguinte, aparece o dia certo. A visualização do mês marcava ontem como hoje e deixava selecionado o dia escolhido na noite anterior. Agora todas as visualizações passam para o novo dia. diff --git a/fastlane/metadata/android/pt-PT/changelogs/2220599.txt b/fastlane/metadata/android/pt-PT/changelogs/2220599.txt new file mode 100644 index 00000000..9fb0d4fb --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Corrigido +• Ao abrir a app na manhã seguinte, aparece o dia certo. A vista do mês marcava ontem como hoje e deixava selecionado o dia escolhido na noite anterior. Agora todas as vistas passam para o novo dia. diff --git a/fastlane/metadata/android/ru-RU/changelogs/2220599.txt b/fastlane/metadata/android/ru-RU/changelogs/2220599.txt new file mode 100644 index 00000000..a6a7973e --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Исправлено +• Если открыть приложение на следующее утро, оно показывает правильный день. Раньше месяц отмечал вчерашний день как сегодняшний и не снимал выделение с дня, выбранного накануне вечером. Теперь все виды переходят на новый день. diff --git a/fastlane/metadata/android/sk/changelogs/2220599.txt b/fastlane/metadata/android/sk/changelogs/2220599.txt new file mode 100644 index 00000000..811a6b64 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/2220599.txt @@ -0,0 +1,2 @@ +Opravené +• Keď aplikáciu otvoríte na druhý deň ráno, ukáže správny deň. Mesiac predtým označoval včerajšok ako dnešok a nechával vybraný deň, ktorý ste zvolili večer predtým. Všetky zobrazenia teraz prejdú na nový deň. diff --git a/fastlane/metadata/android/zh-CN/changelogs/2220599.txt b/fastlane/metadata/android/zh-CN/changelogs/2220599.txt new file mode 100644 index 00000000..2f6f4bfe --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/2220599.txt @@ -0,0 +1,2 @@ +修复 +• 第二天早上打开应用时,会显示正确的日期。以前月视图会把昨天标为今天,并保留前一天晚上选中的日期。现在所有视图都会切换到新的一天。