feat(nav): interactive month widget + history-retracing back
On-device review follow-ups: - Month widget grid is now tappable. Day numbers open that day and event bars open the event's detail, both rooted in the month view so back returns to the grid. Previously only the prev/next/today header controls responded — the grid cells were never clickable. - Pill/drawer view switches now build a visit history instead of collapsing to the default view. Back retraces the views you moved through (a not-yet-visited view is pushed; revisiting one collapses the loop back to it), down to the default, then exits. Widget launches still reset to their own view context. Refs #1, #2 (reported by @devinside). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
CHANGELOG.md
16
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
|
||||
|
||||
|
||||
@@ -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<String?>(null) }
|
||||
|
||||
@@ -52,19 +52,27 @@ fun CalendarView.next(available: List<CalendarView> = 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<CalendarView> =
|
||||
if (top == default) listOf(default) else listOf(default, top)
|
||||
fun List<CalendarView>.selectView(target: CalendarView): List<CalendarView> {
|
||||
val existing = indexOf(target)
|
||||
return if (existing >= 0) take(existing + 1) else this + target
|
||||
}
|
||||
|
||||
fun viewBaseStack(default: CalendarView, source: CalendarView): List<CalendarView> =
|
||||
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<CalendarView>.drillToDay(): List<CalendarView> =
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user