From 20a6548ac820945f83f4f0a917e54a232c51581d Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 7 Oct 2026 09:05:32 +0200 Subject: [PATCH] Release v2.22.4 (#374) Promotes 2.22.4-beta.1 to stable. The app code is the same as the beta. - versionName 2.22.4, versionCode 2220499 (new scheme) - CHANGELOG.md: the Unreleased entries move under [2.22.4] - What's New `changelogs/2220499.txt` in all 17 store locales Includes #370 (re-query after own writes and on return to foreground) and #371 (widgets load only what they show). Closes #363 Closes #364 Co-authored-by: Jean-Luc Makiola Reviewed-on: https://codeberg.org/jlmakiola/calendula/pulls/374 --- CHANGELOG.md | 8 + app/build.gradle.kts | 4 +- .../jeanlucmakiola/calendula/MainActivity.kt | 10 ++ .../data/calendar/CalendarDataSource.kt | 7 +- .../data/calendar/CalendarRepository.kt | 3 + .../data/calendar/CalendarRepositoryImpl.kt | 51 ++++-- .../calendula/widget/WidgetData.kt | 67 +++++--- .../calendula/widget/agenda/AgendaWidget.kt | 160 +++++++++++------- .../calendula/widget/month/MonthWidget.kt | 34 +++- .../calendar/CalendarRepositoryImplTest.kt | 39 +++++ docs/ARCHITECTURE.md | 6 +- .../android/ar/changelogs/2220499.txt | 3 + .../android/be/changelogs/2220499.txt | 3 + .../android/cs-CZ/changelogs/2220499.txt | 3 + .../android/de-DE/changelogs/2220499.txt | 3 + .../android/en-US/changelogs/2220499.txt | 3 + .../android/es-ES/changelogs/2220499.txt | 3 + .../android/fr-FR/changelogs/2220499.txt | 3 + .../android/hu-HU/changelogs/2220499.txt | 3 + .../android/it-IT/changelogs/2220499.txt | 3 + .../android/ja-JP/changelogs/2220499.txt | 3 + .../android/nl-NL/changelogs/2220499.txt | 3 + .../android/pl-PL/changelogs/2220499.txt | 3 + .../android/pt-BR/changelogs/2220499.txt | 3 + .../android/pt-PT/changelogs/2220499.txt | 3 + .../android/ru-RU/changelogs/2220499.txt | 3 + .../android/sk/changelogs/2220499.txt | 3 + .../android/zh-CN/changelogs/2220499.txt | 3 + 28 files changed, 323 insertions(+), 117 deletions(-) create mode 100644 fastlane/metadata/android/ar/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/be/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/cs-CZ/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/de-DE/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/en-US/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/es-ES/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/hu-HU/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/it-IT/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/ja-JP/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/nl-NL/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/pl-PL/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/pt-BR/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/ru-RU/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/sk/changelogs/2220499.txt create mode 100644 fastlane/metadata/android/zh-CN/changelogs/2220499.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index d83d518b..e2da984b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.22.4] — 2026-10-07 + +### Fixed +- **Edits and deletions show up straight away on every phone.** On some phones, Samsung among them, an edited, moved or deleted event stayed where it was until the app was restarted, so a second edit could act on an event that had already changed. Calendula now refreshes its views itself after every change, and again when you come back to the app ([#364]). +- **The home-screen widgets do less work per update.** The month widget loads the months around the one it shows instead of more than two years of events, and the agenda widget only its chosen range. This should help widgets that stayed on "loading" on slower phones ([#363]). + ## [2.22.3] — 2026-10-02 ### Fixed @@ -1835,3 +1841,5 @@ automatically, with zero telemetry and no internet permission. [#357]: https://codeberg.org/jlmakiola/calendula/issues/357 [#359]: https://codeberg.org/jlmakiola/calendula/issues/359 [#361]: https://codeberg.org/jlmakiola/calendula/issues/361 +[#363]: https://codeberg.org/jlmakiola/calendula/issues/363 +[#364]: https://codeberg.org/jlmakiola/calendula/issues/364 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 99bb4fbe..8cc02ad7 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 = 22203 - versionName = "2.22.3" + versionCode = 2220499 + versionName = "2.22.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt index 347b9101..218feb3a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -26,6 +26,7 @@ import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import dagger.hilt.android.AndroidEntryPoint +import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository import de.jeanlucmakiola.calendula.data.prefs.ThemeMode import de.jeanlucmakiola.calendula.data.prefs.is24Hour import de.jeanlucmakiola.calendula.domain.EventForm @@ -56,6 +57,7 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlin.time.Clock import kotlin.time.Instant +import javax.inject.Inject /** A prefilled create form from an external launch, with the source it came from. */ private data class InsertRequest(val form: EventForm, val source: ImportSource) @@ -63,6 +65,8 @@ private data class InsertRequest(val form: EventForm, val source: ImportSource) @AndroidEntryPoint class MainActivity : AppCompatActivity() { + @Inject lateinit var calendarRepository: CalendarRepository + // 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 // picks up the in-app override instead of the night resource qualifier. @@ -245,6 +249,12 @@ class MainActivity : AppCompatActivity() { ) } + override fun onStart() { + super.onStart() + // Changes made while we were away may never have notified us (#364). + calendarRepository.refresh() + } + override fun onResume() { super.onResume() // Reaching a running UI means startup succeeded; reset the loop trail. diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt index 6f873afe..b5f3f5cf 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarDataSource.kt @@ -610,8 +610,11 @@ class AndroidCalendarDataSource @Inject constructor( // cancelled exception for the one instance (#47). A NULL status is a // normal, un-cancelled event, so it must survive the filter — a bare // `!= CANCELED` would drop it (NULL != 2 is NULL, not true). - "${CalendarContract.Instances.STATUS} IS NULL OR " + - "${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED}", + // DELETED = 0: a provider that soft-deletes may keep the row's + // instances until a purge (#364). + "(${CalendarContract.Instances.STATUS} IS NULL OR " + + "${CalendarContract.Instances.STATUS} != ${CalendarContract.Events.STATUS_CANCELED}) AND " + + "${CalendarContract.Events.DELETED} = 0", null, CalendarContract.Instances.BEGIN + " ASC", )?.use { c -> c.mapAllNotNull { CursorColumnReader(c).toEventInstance() } } ?: emptyList() diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt index ac33e137..e9236723 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepository.kt @@ -15,6 +15,9 @@ import kotlin.time.Instant interface CalendarRepository { fun calendars(): Flow> fun instances(range: ClosedRange): Flow> + + /** Re-query every open [calendars] / [instances] flow. */ + fun refresh() suspend fun eventDetail(eventId: Long): EventDetail /** diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt index e02bc1de..3c59714f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImpl.kt @@ -14,6 +14,7 @@ import de.jeanlucmakiola.calendula.domain.SearchCandidate import de.jeanlucmakiola.calendula.domain.ics.IcsImportSummary import de.jeanlucmakiola.calendula.domain.ics.ParsedIcsEvent import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine @@ -51,21 +52,37 @@ class CalendarRepositoryImpl @Inject constructor( private suspend fun allDayReminderTimeMinutes(): Int = settingsPrefs.allDayReminderTimeMinutes.first() + // DROP_OLDEST: tryEmit never fails, so a tick that lands while a subscriber + // still holds the previous one is never lost (#364). private val ticks = MutableSharedFlow( replay = 0, extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, ) /** - * Bumped on every provider notification, so one tick's calendar read can be + * Bumped on every [refresh], so one tick's calendar read can be * shared by everything that needs it (see [calendarsSnapshot]). */ private val generation = AtomicLong(0L) init { - dataSource.registerChangeListener { - generation.incrementAndGet() - ticks.tryEmit(Unit) + dataSource.registerChangeListener(::refresh) + } + + // Writes re-query on their own too: some providers never notify the + // observer, which left every view stale until a restart (#364). + override fun refresh() { + generation.incrementAndGet() + ticks.tryEmit(Unit) + } + + /** Run a provider write on [io], then re-query whether or not it threw. */ + private suspend fun write(block: suspend () -> T): T = withContext(io) { + try { + block() + } finally { + refresh() } } @@ -181,7 +198,7 @@ class CalendarRepositoryImpl @Inject constructor( displayName: String, color: Int, description: String?, - ): Long = withContext(io) { + ): Long = write { dataSource.createLocalCalendar(displayName, color, description) } @@ -190,13 +207,13 @@ class CalendarRepositoryImpl @Inject constructor( displayName: String, color: Int, description: String?, - ) = withContext(io) { dataSource.updateCalendar(id, displayName, color, description) } + ) = write { dataSource.updateCalendar(id, displayName, color, description) } override suspend fun deleteCalendar(id: Long) = - withContext(io) { dataSource.deleteCalendar(id) } + write { dataSource.deleteCalendar(id) } override suspend fun setCalendarsVisible(ids: Collection, visible: Boolean) = - withContext(io) { + write { if (dataSource.canWriteCalendars()) { ids.forEach { dataSource.setCalendarVisible(it, visible) } // Nothing of ours is left waiting for the provider once the @@ -217,7 +234,7 @@ class CalendarRepositoryImpl @Inject constructor( override suspend fun importEvents( targetCalendarId: Long, events: List, - ): IcsImportSummary = withContext(io) { + ): IcsImportSummary = write { val existing = dataSource.existingUids(targetCalendarId) // Both are per-calendar, not per-event: looking them up once keeps a // thousand-event restore to two extra queries. The palette is the @@ -268,7 +285,7 @@ class CalendarRepositoryImpl @Inject constructor( ) } - override suspend fun createEvent(form: EventForm): Long = withContext(io) { + override suspend fun createEvent(form: EventForm): Long = write { dataSource.insertEvent(form, allDayReminderTimeMinutes()) } @@ -276,11 +293,11 @@ class CalendarRepositoryImpl @Inject constructor( eventId: Long, original: EventForm, updated: EventForm, - ) = withContext(io) { + ) = write { dataSource.updateEvent(eventId, original, updated, allDayReminderTimeMinutes()) } - override suspend fun deleteEvent(eventId: Long) = withContext(io) { + override suspend fun deleteEvent(eventId: Long) = write { dataSource.deleteEvent(eventId) } @@ -289,7 +306,7 @@ class CalendarRepositoryImpl @Inject constructor( targetCalendarId: Long, original: EventForm, updated: EventForm, - ): Long = withContext(io) { + ): Long = write { dataSource.moveEvent( eventId, targetCalendarId, original, updated, allDayReminderTimeMinutes(), ) @@ -300,7 +317,7 @@ class CalendarRepositoryImpl @Inject constructor( beginMillis: Long, original: EventForm, form: EventForm, - ): Long = withContext(io) { + ): Long = write { dataSource.updateOccurrence( eventId, beginMillis, original, form, allDayReminderTimeMinutes(), ) @@ -311,20 +328,20 @@ class CalendarRepositoryImpl @Inject constructor( beginMillis: Long, original: EventForm, updated: EventForm, - ): Long = withContext(io) { + ): Long = write { dataSource.updateEventFromOccurrence( eventId, beginMillis, original, updated, allDayReminderTimeMinutes(), ) } - override suspend fun deleteOccurrence(eventId: Long, beginMillis: Long) = withContext(io) { + override suspend fun deleteOccurrence(eventId: Long, beginMillis: Long) = write { dataSource.deleteOccurrence(eventId, beginMillis) } override suspend fun deleteEventFromOccurrence( eventId: Long, beginMillis: Long, - ) = withContext(io) { + ) = write { dataSource.deleteEventFromOccurrence(eventId, beginMillis) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt index 0fa107e0..e902b59a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/WidgetData.kt @@ -11,6 +11,7 @@ import de.jeanlucmakiola.calendula.domain.EventInstance import de.jeanlucmakiola.calendula.ui.agenda.AgendaDay import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange import de.jeanlucmakiola.calendula.ui.agenda.agendaRange +import de.jeanlucmakiola.calendula.ui.agenda.dayCount import de.jeanlucmakiola.calendula.ui.agenda.groupAgendaDays import kotlinx.coroutines.flow.first import kotlinx.datetime.DateTimeUnit @@ -28,11 +29,12 @@ import kotlin.time.Clock import kotlin.time.Instant /** - * How far either side of today the month widget pre-loads. The displayed month - * is chosen reactively in the composition, so one wide read covers ~13 months of - * prev/next navigation without re-querying on every arrow tap. + * Months either side of the displayed one the month widget loads, so a few arrow + * taps page without a re-query. Kept small: every load expands recurrences across + * the whole window, and on slow devices a wide one held the widget on "loading" + * (#363). */ -private const val MONTH_WIDGET_RANGE_DAYS = 400 +private const val MONTH_WIDGET_PAD_MONTHS = 3 internal fun systemZone(): TimeZone = TimeZone.currentSystemDefault() @@ -76,6 +78,8 @@ sealed interface AgendaWidgetData { val savedShowToday: Boolean, /** Saved widget size step (#103) — the fallback before Glance state is set. */ val savedWidgetSize: WidgetSize, + /** Last day [days] covers; a wider range picked later needs a reload. */ + val loadedThrough: LocalDate, /** Snapshot instant the data was read at, for "has this event ended?" tests. */ val now: Instant, ) : AgendaWidgetData @@ -94,14 +98,15 @@ sealed interface MonthWidgetSource { val today: LocalDate, val weekStart: DayOfWeek, val instances: List, + /** Month indices (`year * 12 + monthOrdinal`) whose grids [instances] covers. */ + val months: IntRange, ) : MonthWidgetSource } /** - * Process-lived cache of the wide month window. Month navigation re-runs - * `provideGlance` (via `updateAll`), and re-querying ~13 months of instances on - * every arrow tap is what made paging feel sluggish — so we load once and reuse - * the same snapshot for every nearby month. Invalidated by + * Process-lived cache of the month window. Month navigation can re-run + * `provideGlance`, and re-querying on every arrow tap is what made paging feel + * sluggish — so we load once and reuse the same snapshot for every nearby month. Invalidated by * [invalidateMonthWidgetCache] when calendar data changes (the freshness * receiver), and automatically when the day rolls over (the `today` guard). */ @@ -118,8 +123,11 @@ internal fun invalidateMonthWidgetCache() { * One-shot read of the upcoming agenda for the widget. Reuses the app's * [agendaRange] window and [groupAgendaDays] grouping, and the repository's * [first]-emitted snapshot already has hidden calendars filtered out. + * + * Loads [range] (the widget's own, or the saved pref when null) rather than the + * widest selectable window (#363). */ -internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData { +internal suspend fun Context.loadAgendaWidgetData(range: AgendaRange? = null): AgendaWidgetData { if (!hasCalendarPermission()) return AgendaWidgetData.NeedsPermission val zone = systemZone() val anchor = today(zone) @@ -129,11 +137,11 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData { val savedPastDisplay = prefs.pastEventDisplay.first() val showToday = prefs.agendaShowToday.first() val weekStart = prefs.weekStart.first().resolveFirstDay(Locale.getDefault()) - // Load the widest selectable window once; the displayed range is sliced in - // the composition from Glance state, so changing the range is a plain - // recomposition and never depends on the widget session restarting. - val window = agendaRange(anchor, AgendaRange.MAX_CUSTOM_DAYS - 1, zone) - val windowEnd = anchor.plus(AgendaRange.MAX_CUSTOM_DAYS - 1, DateTimeUnit.DAY) + // The displayed range is still sliced in the composition from Glance state; + // a wider one picked later reloads there (see loadedThrough). + val dayCount = (range ?: savedRange).dayCount(anchor, weekStart) + val window = agendaRange(anchor, dayCount - 1, zone) + val windowEnd = anchor.plus(dayCount - 1, DateTimeUnit.DAY) val instances = ep.calendarRepository().instances(window).first() val is24Hour = prefs.timeFormat.first() .is24Hour(android.text.format.DateFormat.is24HourFormat(this)) @@ -148,22 +156,39 @@ internal suspend fun Context.loadAgendaWidgetData(): AgendaWidgetData { savedPastDisplay = savedPastDisplay, savedShowToday = showToday, savedWidgetSize = prefs.widgetSize.first(), + loadedThrough = windowEnd, now = Clock.System.now(), ) } -/** One-shot wide read backing the month widget's grid for any nearby month. */ -internal suspend fun Context.loadMonthWidgetSource(): MonthWidgetSource { +internal fun monthIndexOf(date: LocalDate): Int = date.year * 12 + date.month.ordinal + +private fun firstDayOfMonthIndex(index: Int): LocalDate = LocalDate(index / 12, index % 12 + 1, 1) + +/** Read backing the month widget's grid for [centerIndex] and the months around it. */ +internal suspend fun Context.loadMonthWidgetSource(centerIndex: Int? = null): MonthWidgetSource { if (!hasCalendarPermission()) return MonthWidgetSource.NeedsPermission val zone = systemZone() val anchor = today(zone) + val center = centerIndex ?: monthIndexOf(anchor) // Reuse the cached window unless the day changed (then it's stale for "today"). - MonthWidgetCache.data?.let { if (it.today == anchor) return it } + MonthWidgetCache.data?.let { if (it.today == anchor && center in it.months) return it } val ep = widgetEntryPoint() val weekStart = ep.settingsPrefs().weekStart.first().resolveFirstDay(Locale.getDefault()) - val from = anchor.minus(MONTH_WIDGET_RANGE_DAYS, DateTimeUnit.DAY).atStartOfDayIn(zone) - val to = anchor.plus(MONTH_WIDGET_RANGE_DAYS, DateTimeUnit.DAY).atTime(23, 59, 59).toInstant(zone) + val months = (center - MONTH_WIDGET_PAD_MONTHS)..(center + MONTH_WIDGET_PAD_MONTHS) + // Padded by the grid's spill into the neighbouring months. + val from = firstDayOfMonthIndex(months.first) + .minus(1, DateTimeUnit.WEEK) + .atStartOfDayIn(zone) + val to = firstDayOfMonthIndex(months.last + 1) + .plus(2, DateTimeUnit.WEEK) + .atTime(23, 59, 59) + .toInstant(zone) val instances = ep.calendarRepository().instances(from..to).first() - return MonthWidgetSource.Ready(today = anchor, weekStart = weekStart, instances = instances) - .also { MonthWidgetCache.data = it } + return MonthWidgetSource.Ready( + today = anchor, + weekStart = weekStart, + instances = instances, + months = months, + ).also { MonthWidgetCache.data = it } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt index 930c1ebc..548572ce 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/agenda/AgendaWidget.kt @@ -5,8 +5,14 @@ import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration import android.content.Context import android.content.res.Configuration import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.glance.ColorFilter @@ -28,6 +34,7 @@ import androidx.glance.appwidget.lazy.items import androidx.glance.appwidget.provideContent import androidx.glance.background import androidx.glance.currentState +import androidx.glance.appwidget.state.getAppWidgetState import androidx.glance.state.PreferencesGlanceStateDefinition import androidx.glance.layout.Alignment import androidx.glance.layout.Box @@ -125,7 +132,8 @@ class AgendaWidget : GlanceAppWidget() { override val sizeMode = SizeMode.Single override suspend fun provideGlance(context: Context, id: GlanceId) { - val data = context.loadAgendaWidgetData() + val stored = getAppWidgetState(context, id)[AGENDA_RANGE_KEY] + val data = context.loadAgendaWidgetData(stored?.let { parseAgendaRange(it, AgendaRange.Month) }) val dark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES provideContent { @@ -190,71 +198,95 @@ private fun AgendaWidgetBody(data: AgendaWidgetData, dark: Boolean) { AgendaWidgetData.NeedsPermission -> WidgetMessage(R.string.widget_needs_permission, metrics) is AgendaWidgetData.Ready -> { // Range read reactively from per-instance Glance state (falls back - // to the saved pref for a freshly placed widget), then the wide - // pre-loaded window is sliced to it — pure recomposition. + // to the saved pref for a freshly placed widget), then the loaded + // window is sliced to it — pure recomposition. val range = parseAgendaRange(currentState(AGENDA_RANGE_KEY), data.savedRange) - val pastDisplay = - parsePastEventDisplay(currentState(AGENDA_PAST_DISPLAY_KEY), data.savedPastDisplay) - val showToday = currentState(AGENDA_SHOW_TODAY_STATE_KEY) ?: data.savedShowToday - val rangeEnd = data.today.plus( - range.dayCount(data.today, data.weekStart) - 1, - DateTimeUnit.DAY, - ) - // Slice to the chosen range, then drop finished events (and any day - // they leave empty) when the mode is Hide — so "Upcoming" really is. - // Finally re-anchor today (as an empty placeholder day) when the - // "always show today" pref is on and today has no events left (#35). - val visibleDays = anchorTodayIfMissing( - days = data.days - .filter { it.date <= rangeEnd } - .let { days -> - if (pastDisplay == PastEventDisplay.HIDE) { - days.mapNotNull { day -> - val remaining = day.events.filterNot { it.hasEnded(data.now) } - if (remaining.isEmpty()) null else day.copy(events = remaining) - } - } else { - days - } - }, - today = data.today, - enabled = showToday, - ) - if (visibleDays.isEmpty()) { - WidgetMessage(R.string.agenda_empty_title, metrics) + val context = androidx.glance.LocalContext.current + var loaded by remember(data) { mutableStateOf(data) } + // A range wider than what was loaded reloads in a live session, + // where provideGlance doesn't run again (#363). + LaunchedEffect(range) { + val end = loaded.today.plus( + range.dayCount(loaded.today, loaded.weekStart) - 1, + DateTimeUnit.DAY, + ) + if (end > loaded.loadedThrough) { + (context.loadAgendaWidgetData(range) as? AgendaWidgetData.Ready) + ?.let { loaded = it } + } + } + AgendaRows(data = loaded, range = range, dark = dark, metrics = metrics) + } + } + } +} + +@Composable +private fun AgendaRows( + data: AgendaWidgetData.Ready, + range: AgendaRange, + dark: Boolean, + metrics: AgendaMetrics, +) { + val pastDisplay = + parsePastEventDisplay(currentState(AGENDA_PAST_DISPLAY_KEY), data.savedPastDisplay) + val showToday = currentState(AGENDA_SHOW_TODAY_STATE_KEY) ?: data.savedShowToday + val rangeEnd = data.today.plus( + range.dayCount(data.today, data.weekStart) - 1, + DateTimeUnit.DAY, + ) + // Slice to the chosen range, then drop finished events (and any day + // they leave empty) when the mode is Hide — so "Upcoming" really is. + // Finally re-anchor today (as an empty placeholder day) when the + // "always show today" pref is on and today has no events left (#35). + val visibleDays = anchorTodayIfMissing( + days = data.days + .filter { it.date <= rangeEnd } + .let { days -> + if (pastDisplay == PastEventDisplay.HIDE) { + days.mapNotNull { day -> + val remaining = day.events.filterNot { it.hasEnded(data.now) } + if (remaining.isEmpty()) null else day.copy(events = remaining) + } } else { - val rows = buildList { - visibleDays.forEach { day -> - add(AgendaRow.Header(day.date, data.today)) - if (day.events.isEmpty()) { - add(AgendaRow.Placeholder(day.date)) - } else { - day.events.forEach { add(AgendaRow.Event(day.date, it)) } - } - } - } - // Bound the payload, then drop a day header the cut left - // stranded with nothing under it. - .take(MAX_AGENDA_ROWS) - .dropLastWhile { it is AgendaRow.Header } - LazyColumn(modifier = GlanceModifier.fillMaxSize()) { - items(rows.size) { index -> - when (val row = rows[index]) { - is AgendaRow.Header -> DayHeaderRow(row.date, row.today, metrics) - is AgendaRow.Placeholder -> PlaceholderRow(row.date, metrics) - is AgendaRow.Event -> EventRow( - event = row.event, - day = row.date, - dark = dark, - soften = data.soften, - is24Hour = data.is24Hour, - dimmed = pastDisplay == PastEventDisplay.DIM && - row.event.hasEnded(data.now), - metrics = metrics, - ) - } - } - } + days + } + }, + today = data.today, + enabled = showToday, + ) + if (visibleDays.isEmpty()) { + WidgetMessage(R.string.agenda_empty_title, metrics) + } else { + val rows = buildList { + visibleDays.forEach { day -> + add(AgendaRow.Header(day.date, data.today)) + if (day.events.isEmpty()) { + add(AgendaRow.Placeholder(day.date)) + } else { + day.events.forEach { add(AgendaRow.Event(day.date, it)) } + } + } + } + // Bound the payload, then drop a day header the cut left + // stranded with nothing under it. + .take(MAX_AGENDA_ROWS) + .dropLastWhile { it is AgendaRow.Header } + LazyColumn(modifier = GlanceModifier.fillMaxSize()) { + items(rows.size) { index -> + when (val row = rows[index]) { + is AgendaRow.Header -> DayHeaderRow(row.date, row.today, metrics) + is AgendaRow.Placeholder -> PlaceholderRow(row.date, metrics) + is AgendaRow.Event -> EventRow( + event = row.event, + day = row.date, + dark = dark, + soften = data.soften, + is24Hour = data.is24Hour, + dimmed = pastDisplay == PastEventDisplay.DIM && + row.event.hasEnded(data.now), + metrics = metrics, + ) } } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt index 43455e68..c72e851b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/widget/month/MonthWidget.kt @@ -5,8 +5,14 @@ import de.jeanlucmakiola.calendula.widget.glanceDeclinedDecoration import android.content.Context import android.content.res.Configuration import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.intPreferencesKey import androidx.glance.ColorFilter import androidx.glance.GlanceId @@ -26,6 +32,7 @@ import androidx.glance.appwidget.action.actionRunCallback import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.provideContent +import androidx.glance.appwidget.state.getAppWidgetState import androidx.glance.appwidget.state.updateAppWidgetState import androidx.glance.background import androidx.glance.currentState @@ -59,6 +66,7 @@ import de.jeanlucmakiola.calendula.ui.common.eventInk import de.jeanlucmakiola.calendula.widget.CalendulaGlanceTheme import de.jeanlucmakiola.calendula.widget.MonthWidgetSource import de.jeanlucmakiola.calendula.widget.loadMonthWidgetSource +import de.jeanlucmakiola.calendula.widget.monthIndexOf import de.jeanlucmakiola.calendula.widget.systemZone import de.jeanlucmakiola.calendula.widget.today import de.jeanlucmakiola.calendula.widget.widgetEntryPoint @@ -81,10 +89,7 @@ private val LANE_HEIGHT = 14.dp private val DAY_NUMBER_HEIGHT = 18.dp private val GRID_HPADDING = 8.dp -private fun currentMonthIndex(zone: TimeZone): Int { - val t = today(zone) - return t.year * 12 + t.month.ordinal -} +private fun currentMonthIndex(zone: TimeZone): Int = monthIndexOf(today(zone)) private fun yearMonthOf(index: Int): YearMonth = YearMonth(index / 12, Month(index % 12 + 1)) @@ -123,7 +128,8 @@ class MonthWidget : GlanceAppWidget() { override val sizeMode = SizeMode.Exact override suspend fun provideGlance(context: Context, id: GlanceId) { - val source = context.loadMonthWidgetSource() + val shown = getAppWidgetState(context, id)[MONTH_INDEX_KEY] + val source = context.loadMonthWidgetSource(shown) val dark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES // Read fresh (not through the cached source) so toggling the softener @@ -198,18 +204,28 @@ private fun MonthWidgetBody(source: MonthWidgetSource, dark: Boolean, soften: Bo val zone = systemZone() val index = currentState(MONTH_INDEX_KEY) ?: currentMonthIndex(zone) val ym = yearMonthOf(index) + // Paging past the loaded months reloads around the new one in a + // live session, where provideGlance doesn't run again. + val context = LocalContext.current + var loaded by remember(source) { mutableStateOf(source) } + LaunchedEffect(index) { + if (index !in loaded.months) { + (context.loadMonthWidgetSource(index) as? MonthWidgetSource.Ready) + ?.let { loaded = it } + } + } // Column width from the live widget size, minus our H padding. val colW = (LocalSize.current.width - GRID_HPADDING * 2) / 7 - val weeks = layoutMonthWeeks(ym, source.weekStart, source.instances, zone) + val weeks = layoutMonthWeeks(ym, loaded.weekStart, loaded.instances, zone) - MonthHeader(label = monthLabel(ym, source.today.year)) + MonthHeader(label = monthLabel(ym, loaded.today.year)) Spacer(GlanceModifier.height(2.dp)) - WeekdayHeader(weekStart = source.weekStart, colW = colW) + WeekdayHeader(weekStart = loaded.weekStart, colW = colW) weeks.forEach { week -> WeekRow( week = week, currentMonth = ym.month, - today = source.today, + today = loaded.today, dark = dark, soften = soften, colW = colW, diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImplTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImplTest.kt index f67f6745..076d7167 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImplTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/data/calendar/CalendarRepositoryImplTest.kt @@ -95,6 +95,45 @@ class CalendarRepositoryImplTest { } } + @Test + fun `instances re-emit after a write the provider never notified about`(@TempDir tempDir: Path) = runTest { + var current = listOf(makeEvent(10L)) + val fake = FakeCalendarDataSource().apply { instancesResult = { _, _ -> current } } + val repo = CalendarRepositoryImpl(fake, newPrefs(tempDir), newSettings(tempDir), UnconfinedTestDispatcher(testScheduler)) + + val range = Instant.fromEpochMilliseconds(0)..Instant.fromEpochMilliseconds(10_000L) + repo.instances(range).test { + assertThat(awaitItem().map { it.eventId }).containsExactly(10L) + + current = emptyList() + repo.deleteEvent(10L) + + assertThat(awaitItem()).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `instances re-emit after a write that failed`(@TempDir tempDir: Path) = runTest { + var current = listOf(makeEvent(10L)) + val fake = FakeCalendarDataSource().apply { + instancesResult = { _, _ -> current } + writeError = WriteFailedException("boom") + } + val repo = CalendarRepositoryImpl(fake, newPrefs(tempDir), newSettings(tempDir), UnconfinedTestDispatcher(testScheduler)) + + val range = Instant.fromEpochMilliseconds(0)..Instant.fromEpochMilliseconds(10_000L) + repo.instances(range).test { + awaitItem() + + current = emptyList() + assertThrows { repo.deleteOccurrence(10L, 1_000L) } + + assertThat(awaitItem()).isEmpty() + cancelAndIgnoreRemainingEvents() + } + } + @Test fun `instances forwards epoch-millis bounds to data source`(@TempDir tempDir: Path) = runTest { var observedBegin: Long? = null diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f444241a..f5d31dcd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -12,8 +12,10 @@ the package list (recurring writes, save conflicts, reminder delivery). straight back to it. Sync is DAVx5's / Google's / the system's job. 2. **Observer-driven UI.** A `ContentObserver` on the provider triggers re-queries; every screen recomposes from fresh provider state. After a - write, nothing is patched by hand — the provider notifies, the views - refresh. This also covers external changes (sync) for free. + write, nothing is patched by hand — the views refresh from the provider. + This also covers external changes (sync) for free. Some providers don't + reliably notify (#364), so the repository also re-queries after each of + its own writes, and `MainActivity` does on every return to the foreground. 3. **JVM-first testing.** Everything between the UI and the `ContentResolver` is shaped so it runs as a plain JUnit 5 test: pure domain logic, cursor-free mappers, a `FakeCalendarDataSource` for diff --git a/fastlane/metadata/android/ar/changelogs/2220499.txt b/fastlane/metadata/android/ar/changelogs/2220499.txt new file mode 100644 index 00000000..eafc38d0 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/2220499.txt @@ -0,0 +1,3 @@ +إصلاحات +• الأحداث المعدّلة أو المنقولة أو المحذوفة تظهر الآن فورًا على كل الهواتف. على بعض الهواتف، ومنها سامسونج، كان الحدث المعدّل يبقى كما هو حتى إعادة تشغيل التطبيق. +• أدوات الشاشة الرئيسية تحمّل الآن الأحداث التي تعرضها فقط. هذا يُفترض أن يساعد الأدوات التي كانت تبقى على «جارٍ التحميل» في الهواتف الأبطأ. diff --git a/fastlane/metadata/android/be/changelogs/2220499.txt b/fastlane/metadata/android/be/changelogs/2220499.txt new file mode 100644 index 00000000..d8e82687 --- /dev/null +++ b/fastlane/metadata/android/be/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Выпраўлена +• Змененыя, перанесеныя і выдаленыя падзеі цяпер адразу адлюстроўваюцца на любым тэлефоне. На некаторых, у тым ліку Samsung, змененая падзея заставалася ранейшай да перазапуску праграмы. +• Віджэты на галоўным экране загружаюць толькі тыя падзеі, якія паказваюць. Гэта павінна дапамагчы віджэтам, якія на павольных тэлефонах затрымліваліся на «загрузцы». diff --git a/fastlane/metadata/android/cs-CZ/changelogs/2220499.txt b/fastlane/metadata/android/cs-CZ/changelogs/2220499.txt new file mode 100644 index 00000000..3a48939e --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Opraveno +• Upravené, přesunuté a smazané události se teď na každém telefonu projeví hned. Na některých, mimo jiné Samsungu, zůstala změněná událost beze změny až do restartu aplikace. +• Widgety na domovské obrazovce načítají jen události, které zobrazují. To by mělo pomoct widgetům, které na pomalejších telefonech zůstávaly na „načítání“. diff --git a/fastlane/metadata/android/de-DE/changelogs/2220499.txt b/fastlane/metadata/android/de-DE/changelogs/2220499.txt new file mode 100644 index 00000000..03b4ac51 --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Behoben +• Bearbeitete, verschobene und gelöschte Termine erscheinen jetzt auf jedem Handy sofort richtig. Auf manchen Geräten, darunter Samsung, blieb ein geänderter Termin unverändert, bis die App neu gestartet wurde. +• Die Widgets auf dem Startbildschirm laden nur noch die Termine, die sie anzeigen. Das sollte Widgets helfen, die auf langsameren Handys bei „Wird geladen“ hängen blieben. diff --git a/fastlane/metadata/android/en-US/changelogs/2220499.txt b/fastlane/metadata/android/en-US/changelogs/2220499.txt new file mode 100644 index 00000000..c35268ce --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Fixed +• Edits, moves and deletions show up straight away on every phone. On some phones, Samsung among them, a changed event stayed put until you restarted the app. +• The home-screen widgets load only the events they show. This should help widgets stuck on "loading" on slower phones. diff --git a/fastlane/metadata/android/es-ES/changelogs/2220499.txt b/fastlane/metadata/android/es-ES/changelogs/2220499.txt new file mode 100644 index 00000000..9beeddfa --- /dev/null +++ b/fastlane/metadata/android/es-ES/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Corregido +• Los eventos editados, movidos o eliminados se actualizan al momento en cualquier móvil. En algunos, como los Samsung, un evento cambiado no se actualizaba hasta reiniciar la app. +• Los widgets de la pantalla de inicio solo cargan los eventos que muestran. Esto debería ayudar a los widgets que se quedaban en «cargando» en móviles más lentos. diff --git a/fastlane/metadata/android/fr-FR/changelogs/2220499.txt b/fastlane/metadata/android/fr-FR/changelogs/2220499.txt new file mode 100644 index 00000000..2a02bdb7 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Corrigé +• Les événements modifiés, déplacés ou supprimés s'affichent tout de suite sur tous les téléphones. Sur certains, dont les Samsung, un événement modifié ne changeait qu'après un redémarrage de l'app. +• Les widgets de l'écran d'accueil ne chargent plus que les événements qu'ils affichent, ce qui devrait aider ceux qui restaient bloqués sur « chargement » sur les téléphones lents. diff --git a/fastlane/metadata/android/hu-HU/changelogs/2220499.txt b/fastlane/metadata/android/hu-HU/changelogs/2220499.txt new file mode 100644 index 00000000..1f934c85 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Javítva +• A szerkesztett, áthelyezett és törölt események most minden telefonon azonnal megjelennek. Egyes készülékeken, köztük a Samsungokon, a módosított esemény az alkalmazás újraindításáig változatlan maradt. +• A kezdőképernyő-widgetek csak a megjelenített eseményeket töltik be. Ez segíthet azokon a widgeteken, amelyek lassabb telefonokon a „betöltés” állapotban ragadtak. diff --git a/fastlane/metadata/android/it-IT/changelogs/2220499.txt b/fastlane/metadata/android/it-IT/changelogs/2220499.txt new file mode 100644 index 00000000..43f1bf74 --- /dev/null +++ b/fastlane/metadata/android/it-IT/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Corretto +• Gli eventi modificati, spostati o eliminati si aggiornano subito su ogni telefono. Su alcuni, tra cui i Samsung, un evento modificato restava invariato fino al riavvio dell'app. +• I widget della schermata Home caricano solo gli eventi che mostrano. Dovrebbe aiutare i widget che restavano su «caricamento» sui telefoni più lenti. diff --git a/fastlane/metadata/android/ja-JP/changelogs/2220499.txt b/fastlane/metadata/android/ja-JP/changelogs/2220499.txt new file mode 100644 index 00000000..dbc33b69 --- /dev/null +++ b/fastlane/metadata/android/ja-JP/changelogs/2220499.txt @@ -0,0 +1,3 @@ +修正 +• 予定の編集・移動・削除が、どの端末でもすぐに反映されるようになりました。Samsung など一部の端末では、アプリを再起動するまで変更前の予定が表示されたままでした。 +• ホーム画面のウィジェットは、表示する予定だけを読み込むようになりました。動作の遅い端末で「読み込み中」のまま止まるウィジェットが改善されるはずです。 diff --git a/fastlane/metadata/android/nl-NL/changelogs/2220499.txt b/fastlane/metadata/android/nl-NL/changelogs/2220499.txt new file mode 100644 index 00000000..4a40b580 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Opgelost +• Bewerkte, verplaatste en verwijderde afspraken zijn nu op elke telefoon meteen zichtbaar. Op sommige toestellen, waaronder Samsung, bleef een gewijzigde afspraak staan tot je de app opnieuw startte. +• De widgets op het startscherm laden alleen nog de afspraken die ze tonen. Dat zou widgets moeten helpen die op tragere telefoons op "laden" bleven hangen. diff --git a/fastlane/metadata/android/pl-PL/changelogs/2220499.txt b/fastlane/metadata/android/pl-PL/changelogs/2220499.txt new file mode 100644 index 00000000..63977d31 --- /dev/null +++ b/fastlane/metadata/android/pl-PL/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Poprawki +• Edytowane, przeniesione i usunięte wydarzenia od razu pokazują się poprawnie na każdym telefonie. Na niektórych, m.in. Samsungach, zmienione wydarzenie nie zmieniało się aż do ponownego uruchomienia aplikacji. +• Widżety na ekranie głównym wczytują tylko wydarzenia, które pokazują. To powinno pomóc widżetom, które na wolniejszych telefonach zostawały na „wczytywanie”. diff --git a/fastlane/metadata/android/pt-BR/changelogs/2220499.txt b/fastlane/metadata/android/pt-BR/changelogs/2220499.txt new file mode 100644 index 00000000..388b372c --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Corrigido +• Eventos editados, movidos ou excluídos aparecem atualizados na hora em qualquer celular. Em alguns aparelhos, incluindo Samsung, um evento alterado só mudava depois de reiniciar o app. +• Os widgets da tela inicial carregam apenas os eventos que mostram. Isso deve ajudar widgets que ficavam em "carregando" em celulares mais lentos. diff --git a/fastlane/metadata/android/pt-PT/changelogs/2220499.txt b/fastlane/metadata/android/pt-PT/changelogs/2220499.txt new file mode 100644 index 00000000..56ce748e --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Corrigido +• Os eventos editados, movidos ou eliminados aparecem logo atualizados em qualquer telemóvel. Em alguns, incluindo Samsung, um evento alterado só mudava depois de reiniciar a app. +• Os widgets do ecrã inicial carregam apenas os eventos que mostram. Isto deve ajudar os widgets que ficavam em «a carregar» em telemóveis mais lentos. diff --git a/fastlane/metadata/android/ru-RU/changelogs/2220499.txt b/fastlane/metadata/android/ru-RU/changelogs/2220499.txt new file mode 100644 index 00000000..5e986016 --- /dev/null +++ b/fastlane/metadata/android/ru-RU/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Исправлено +• Изменённые, перенесённые и удалённые события теперь сразу отображаются на любом телефоне. На некоторых, в том числе Samsung, изменённое событие оставалось прежним до перезапуска приложения. +• Виджеты на главном экране загружают только те события, которые показывают. Это должно помочь виджетам, которые на медленных телефонах зависали на «загрузке». diff --git a/fastlane/metadata/android/sk/changelogs/2220499.txt b/fastlane/metadata/android/sk/changelogs/2220499.txt new file mode 100644 index 00000000..a6e333d5 --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/2220499.txt @@ -0,0 +1,3 @@ +Opravené +• Upravené, presunuté a odstránené udalosti sa teraz na každom telefóne prejavia hneď. Na niektorých, okrem iného Samsungu, zostala zmenená udalosť bez zmeny až do reštartu aplikácie. +• Widgety na domovskej obrazovke načítavajú len udalosti, ktoré zobrazujú. To by malo pomôcť widgetom, ktoré na pomalších telefónoch zostávali na „načítava sa“. diff --git a/fastlane/metadata/android/zh-CN/changelogs/2220499.txt b/fastlane/metadata/android/zh-CN/changelogs/2220499.txt new file mode 100644 index 00000000..716fe5bd --- /dev/null +++ b/fastlane/metadata/android/zh-CN/changelogs/2220499.txt @@ -0,0 +1,3 @@ +修复 +• 编辑、移动或删除的日程现在在所有手机上都会立即显示。在部分手机(包括三星)上,修改后的日程要等重启应用后才会更新。 +• 主屏幕小部件现在只加载其显示的日程。这应能改善在较慢手机上一直停在“加载中”的小部件。