diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c94340..0cc686e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,14 +12,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Appearance) lets you pick Month, Week, Day or Agenda as the view shown each time you start the app, instead of always opening on Week. Thanks to @devinside for the suggestion ([#1]). +- The month widget is now interactive: tap any day to open it, or tap an event to + open its details. Previously only the month's prev/next/today controls + responded. Thanks to @devinside for spotting this ([#2]). ### Fixed -- Home-screen widgets now back out the way you came in. Opening a day or event - from the Agenda widget keeps you in the agenda context, and the Month widget - keeps you in the month context — pressing Back returns to that view instead of - dropping you on the week view. The app now keeps a proper view history: Back - steps from a drilled-in day to the view you opened it from, then to your - default view, then leaves the app. Thanks to @devinside for reporting ([#2]). +- Home-screen widgets now back out the way you came in. A day or event opened + from the Agenda widget keeps you in the agenda context, and from the Month + widget in the month context — pressing Back returns there instead of dropping + you on the week view. More broadly, the app now keeps a real view history: + switching views and drilling into a day are retraced by Back one step at a + time, down to your default view before the app exits. Thanks to @devinside for + reporting ([#2]). ## [2.8.0] — 2026-06-23 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 8bc1c63..d57b04b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -26,6 +26,7 @@ import de.jeanlucmakiola.calendula.ui.calendars.CalendarsScreen import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.drillToDay import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec +import de.jeanlucmakiola.calendula.ui.common.selectView import de.jeanlucmakiola.calendula.ui.common.viewBaseStack import de.jeanlucmakiola.calendula.ui.day.DayScreen import de.jeanlucmakiola.calendula.ui.detail.EventDetailScreen @@ -46,11 +47,12 @@ import kotlin.time.Clock * pill in their top bars writes back here via [onSelectView]. * * The stack's bottom is the user's [CalendarHostViewModel.defaultView] home view. - * A lateral move (pill / drawer / a widget establishing its own view) replaces - * the non-home top; a date tap drills the day view on top. Pressing back pops - * one level, and the base-level [BackHandler] hands off to the system to exit - * once only the home view remains. So back from a widget-opened screen returns - * to that widget's view, then home, then out. + * A lateral switch (pill / drawer) builds a visit history so back retraces it + * (see [selectView]); a widget launch resets the stack to its own view; a date + * tap drills the day view on top. Pressing back pops one level, and the + * base-level [BackHandler] hands off to the system to exit once only the home + * view remains. So back from a widget-opened screen returns to that widget's + * view, then home; and back through pill switches walks the views in reverse. * * [requestedDetailKey] is an externally requested occurrence (a tapped * reminder notification routed through MainActivity): it opens the detail @@ -79,7 +81,7 @@ fun CalendarHost( mutableStateOf(listOf(defaultView)) } val view = viewStack.last() - val onSelectView: (CalendarView) -> Unit = { viewStack = viewBaseStack(defaultView, it) } + val onSelectView: (CalendarView) -> Unit = { viewStack = viewStack.selectView(it) } // Tapping a day in the month grid opens the day view anchored to that date. var pendingDayIso by rememberSaveable { mutableStateOf(null) } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt index bcf25e2..57151ab 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/CalendarView.kt @@ -52,19 +52,27 @@ fun CalendarView.next(available: List = IMPLEMENTED_VIEWS): Calend /** * The top-level view back stack (bottom → top): the [default] home view always - * sits at the bottom, with a laterally-selected or widget-entered view layered - * above it. Pressing back pops one level until only the home view remains, then - * the system exits the app. + * sits at the bottom. Pressing back pops one level until only the home view + * remains, then the system exits the app. Three operations move it: * - * [viewBaseStack] is the stack after a lateral move to [top] (the switcher pill, - * the drawer, or a widget establishing its own view): just `[default]` when - * [top] is the home view, else `[default, top]`. It collapses any prior drill-in - * so the depth never grows past a single lateral level. [drillToDay] pushes the - * day view on top — a date tap from the month grid or a widget — unless the day - * view is already current. + * - [selectView] — a lateral switch (the pill or the drawer). It keeps a visit + * history so back retraces it: switching to a not-yet-visited view pushes it on + * top; switching to one already in the stack pops back to it (collapsing the + * loop, so the depth stays bounded by the number of distinct views). Selecting + * the home view collapses to just `[default]`. + * - [viewBaseStack] — the stack a widget launch resets to: just `[default]` when + * the widget's view is the home view, else `[default, source]`. A widget entry + * starts a fresh context rather than extending in-app history. + * - [drillToDay] — pushes the day view on top (a date tap from the month grid or + * a widget), unless the day view is already current. */ -fun viewBaseStack(default: CalendarView, top: CalendarView): List = - if (top == default) listOf(default) else listOf(default, top) +fun List.selectView(target: CalendarView): List { + val existing = indexOf(target) + return if (existing >= 0) take(existing + 1) else this + target +} + +fun viewBaseStack(default: CalendarView, source: CalendarView): List = + if (source == default) listOf(default) else listOf(default, source) /** Push the day view as a drill-in over the current stack (no-op if already on it). */ fun List.drillToDay(): List = 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 10089fc..1799bd5 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 @@ -303,8 +303,16 @@ private fun WeekRow( @Composable private fun DayNumber(date: LocalDate, isToday: Boolean, inMonth: Boolean, colW: Dp) { + val context = LocalContext.current Box( - modifier = GlanceModifier.width(colW).height(DAY_NUMBER_HEIGHT), + modifier = GlanceModifier + .width(colW) + .height(DAY_NUMBER_HEIGHT) + // Tap a day number to open that day, rooted in the month view so back + // returns to the month grid. + .clickable( + actionStartActivity(MainActivity.openDateIntent(context, date, CalendarView.Month)), + ), contentAlignment = Alignment.Center, ) { Box( @@ -356,7 +364,24 @@ private fun LaneRow(week: MonthWeek, lane: Int, dark: Boolean, colW: Dp) { @Composable private fun SpanBar(event: EventInstance, dark: Boolean, width: Dp) { val context = LocalContext.current - Box(modifier = GlanceModifier.width(width).height(LANE_HEIGHT).padding(horizontal = 1.dp)) { + Box( + modifier = GlanceModifier + .width(width) + .height(LANE_HEIGHT) + .padding(horizontal = 1.dp) + // Tap an event bar to open its detail, rooted in the month view. + .clickable( + actionStartActivity( + MainActivity.openEventIntent( + context = context, + eventId = event.eventId, + beginMillis = event.start.toEpochMilliseconds(), + endMillis = event.end.toEpochMilliseconds(), + source = CalendarView.Month, + ), + ), + ), + ) { Box( modifier = GlanceModifier .fillMaxSize() diff --git a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt index a1e2c73..7c4f118 100644 --- a/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/calendula/ui/common/ViewBackStackTest.kt @@ -12,25 +12,44 @@ import org.junit.jupiter.api.Test class ViewBackStackTest { @Test - fun `lateral move to the home view collapses to just the home`() { + fun `a widget whose view is the home view resets to just the home`() { assertThat(viewBaseStack(CalendarView.Week, CalendarView.Week)) .containsExactly(CalendarView.Week) } @Test - fun `lateral move to a non-home view layers it over the home`() { + fun `a widget launch resets to its view over the home`() { assertThat(viewBaseStack(CalendarView.Week, CalendarView.Month)) .containsExactly(CalendarView.Week, CalendarView.Month) .inOrder() } @Test - fun `a lateral move never grows past one level above home`() { - // Switching pill targets in turn replaces the top rather than stacking. - val afterMonth = viewBaseStack(CalendarView.Week, CalendarView.Month) - val afterAgenda = viewBaseStack(CalendarView.Week, CalendarView.Agenda) - assertThat(afterMonth).hasSize(2) - assertThat(afterAgenda).containsExactly(CalendarView.Week, CalendarView.Agenda).inOrder() + fun `pill switches build a visit history that back retraces`() { + // Agenda → Day → Week → Month, each a new view, stacks the full history. + val stack = listOf(CalendarView.Agenda) + .selectView(CalendarView.Day) + .selectView(CalendarView.Week) + .selectView(CalendarView.Month) + assertThat(stack).containsExactly( + CalendarView.Agenda, CalendarView.Day, CalendarView.Week, CalendarView.Month, + ).inOrder() + // Back walks the views in reverse, not straight to the home view. + assertThat(stack.dropLast(1).last()).isEqualTo(CalendarView.Week) + } + + @Test + fun `selecting an already-visited view collapses the loop back to it`() { + val stack = listOf(CalendarView.Week, CalendarView.Agenda, CalendarView.Day) + .selectView(CalendarView.Agenda) + assertThat(stack).containsExactly(CalendarView.Week, CalendarView.Agenda).inOrder() + } + + @Test + fun `selecting the home view collapses to just the home`() { + val stack = listOf(CalendarView.Week, CalendarView.Agenda, CalendarView.Day) + .selectView(CalendarView.Week) + assertThat(stack).containsExactly(CalendarView.Week) } @Test