diff --git a/CHANGELOG.md b/CHANGELOG.md index c83fddb..8b4accf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.20.4] — 2026-09-20 + +### Fixed +- **The app no longer reopens the event you last came in on.** Opening an event + from a reminder or a home-screen widget made the app return to that same + event on every later launch — and show an error screen once the event had + been deleted. Android hands an app the launch it was started with again each + time it is rebuilt, and Calendula was acting on it every time instead of + once ([#309]). +- **"Today" works in the agenda again.** Scrolling a few days ahead and tapping + today did nothing, because the agenda is browsed by scrolling rather than by + moving its date, and the button only moved the date. It now brings the list + back to today, and it stays offered while the list is scrolled away — before, + it was hidden in exactly the situation it is for ([#305]). +- **Calendula is filed under "Calendar & Agenda" in the self-hosted repo.** + Browsing by category in an F-Droid client set to prefer that repo listed the + app under "Time" instead ([#294]). + ## [2.20.3] — 2026-09-12 ### Fixed @@ -1693,3 +1711,6 @@ automatically, with zero telemetry and no internet permission. [#239]: https://codeberg.org/jlmakiola/calendula/issues/239 [#297]: https://codeberg.org/jlmakiola/calendula/issues/297 [#298]: https://codeberg.org/jlmakiola/calendula/issues/298 +[#309]: https://codeberg.org/jlmakiola/calendula/issues/309 +[#305]: https://codeberg.org/jlmakiola/calendula/issues/305 +[#294]: https://codeberg.org/jlmakiola/calendula/issues/294 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 503e828..c25434a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -28,8 +28,8 @@ android { // which builds this version and then creates the matching vX.Y.Z tag + // release itself (versionCode is pinned to MAJOR*10000 + MINOR*100 + // PATCH from versionName, e.g. 2.7.2 -> 20702). See docs/RELEASING.md. - versionCode = 22003 - versionName = "2.20.3" + versionCode = 22004 + versionName = "2.20.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 b2a9d07..5de78db 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/MainActivity.kt @@ -113,11 +113,26 @@ class MainActivity : AppCompatActivity() { systemBarsDark = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES applyEdgeToEdge() - requestedDetailKey = intent.detailKeyOrNull() ?: intent.viewEventKeyOrNull() + // Android hands the intent that started the task back to onCreate every + // time the activity is recreated — on a rotation, or on a restart after + // the system reclaimed the process. Re-running a launch that opens one + // specific event is what made a single reminder or widget tap re-open + // that event on every later launch, an error screen once the event had + // been deleted (#309); the user left it long ago, and the activity + // restores where they actually were along with its saved state. + // + // Only those channels are held back. An .ics import or a prefilled + // create form keeps no saved state of its own, so re-reading the intent + // is what carries one through a recreation. + val replaying = savedInstanceState != null || intent.isRelaunch() + if (!replaying) { + requestedDetailKey = intent.detailKeyOrNull() ?: intent.viewEventKeyOrNull() + requestedEditKey = intent.editEventKeyOrNull() + } requestedNav = intent.navRequestOrNull() + ?.takeUnless { replaying && it is WidgetNavRequest.OpenEvent } requestedImportUri = intent.importUriOrNull() requestedInsert = intent.insertRequestOrNull() - requestedEditKey = intent.editEventKeyOrNull() if (CrashReporter.shouldPrompt(this)) pendingCrashReport = CrashReporter.pendingReport(this) setContent { // One activity-scoped SettingsViewModel drives both the theme here @@ -242,6 +257,16 @@ class MainActivity : AppCompatActivity() { intent.editEventKeyOrNull()?.let { requestedEditKey = it } } + /** + * Whether this launch is the task being resumed rather than a fresh + * delivery — Android sets the flag when an activity is started from the + * recents list, where the intent it carries is the one the task was started + * with however long ago. `onNewIntent` is the channel a real new request + * arrives on while the task lives, so nothing is lost by ignoring these. + */ + private fun Intent.isRelaunch(): Boolean = + flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0 + /** * The `.ics` Uri an external app asked us to open (file manager `ACTION_VIEW`) * or share into us (`ACTION_SEND`). Restricted to content/file schemes so the diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt index 76e340b..bb4ef71 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaScreen.kt @@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Coffee import androidx.compose.material.icons.filled.Menu @@ -32,6 +34,8 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDrawerState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -106,6 +110,27 @@ fun AgendaScreen( } val successState = state as? AgendaUiState.Success + // The agenda window always starts at the anchor, so moving through it is a + // list scroll, not an anchor change — "today" has to bring the list back to + // the top as well, and stay offered while it is scrolled away (#305). + val listState = rememberLazyListState() + val scrolledAway by remember { + derivedStateOf { + listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0 + } + } + // A new window is rendered from its first day, so the list belongs at the + // top. Keyed on the anchor the days arrived with, not the one just + // requested: scrolling before the new rows compose leaves the list keyed to + // a row that reappears further down, and it follows it there. + LaunchedEffect(successState?.anchor) { listState.scrollToItem(0) } + val jumpToToday = { + // Off today, moving the anchor is what resets the list (above); on today + // the window doesn't change, so the scroll back is the whole action. + if (isOnToday) scope.launch { listState.animateScrollToItem(0) } + viewModel.goToToday() + } + ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -139,14 +164,15 @@ fun AgendaScreen( onOpenDrawer = { scope.launch { drawerState.open() } }, onOpenSearch = onOpenSearch, showTodayButton = todayInToolbar, - onToday = viewModel::goToToday, + onToday = jumpToToday, ) }, floatingActionButton = { CalendarFabColumn( - todayVisible = !isOnToday && !todayInToolbar, + todayVisible = (!isOnToday || (scrolledAway && successState != null)) && + !todayInToolbar, todayText = stringResource(R.string.agenda_today_action), - onToday = viewModel::goToToday, + onToday = jumpToToday, onCreate = { onCreateEvent(anchor, null) }, ) }, @@ -195,6 +221,7 @@ fun AgendaScreen( state = state, pastDisplay = pastDisplay, showToday = showToday, + listState = listState, onRetry = viewModel::goToToday, onEventClick = onEventClick, onOpenDay = onOpenDay, @@ -298,6 +325,7 @@ internal fun AgendaContent( onEventClick: (EventInstance) -> Unit, onOpenDay: (LocalDate) -> Unit, modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), ) { when (state) { AgendaUiState.Loading -> Box(modifier) @@ -326,12 +354,16 @@ internal fun AgendaContent( enabled = showToday && state.anchor == state.today, ) if (days.isEmpty()) { + // Nothing to scroll: drop the position a previous list left, so + // the today FAB doesn't linger over an empty screen. + LaunchedEffect(Unit) { listState.scrollToItem(0) } AgendaEmpty(modifier) } else { AgendaList( days = days, today = state.today, zone = state.zone, + listState = listState, dimPast = pastDisplay == PastEventDisplay.DIM, now = now, onEventClick = onEventClick, @@ -349,6 +381,7 @@ private fun AgendaList( days: List, today: LocalDate, zone: TimeZone, + listState: LazyListState, dimPast: Boolean, now: Instant, onEventClick: (EventInstance) -> Unit, @@ -357,6 +390,7 @@ private fun AgendaList( ) { LazyColumn( modifier = modifier, + state = listState, // Bottom inset clears the FAB stack so the last row stays tappable. contentPadding = PaddingValues(top = 8.dp, bottom = 96.dp), ) { diff --git a/fastlane/metadata/android/en-US/changelogs/22004.txt b/fastlane/metadata/android/en-US/changelogs/22004.txt new file mode 100644 index 0000000..0ecc26a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/22004.txt @@ -0,0 +1,3 @@ +Fixed +• The app no longer reopens the event you last came in on. Opening an event from a reminder or a widget made it return to that event on every later launch, or show an error screen once the event was gone. +• "Today" works in the Agenda again. It now scrolls the list back to today, and stays offered while you are scrolled away from it. diff --git a/fdroid-metadata/de.jeanlucmakiola.calendula.yml b/fdroid-metadata/de.jeanlucmakiola.calendula.yml index 61c667b..2a90a44 100644 --- a/fdroid-metadata/de.jeanlucmakiola.calendula.yml +++ b/fdroid-metadata/de.jeanlucmakiola.calendula.yml @@ -4,7 +4,7 @@ Name: Calendula Summary: A modern Material 3 Expressive calendar for Android. Categories: - - Time + - Calendar & Agenda SourceCode: https://codeberg.org/jlmakiola/calendula IssueTracker: https://codeberg.org/jlmakiola/calendula/issues