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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user