From 21c770afb38141fd2756043ec9ca31ebc5526d1b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 25 Sep 2026 20:59:17 +0200 Subject: [PATCH] Open week view from month week numbers (#197) --- CHANGELOG.md | 1 + .../calendula/ui/CalendarHost.kt | 9 +++++ .../calendula/ui/common/CalendarView.kt | 4 ++ .../calendula/ui/month/MonthScreen.kt | 12 +++++- .../calendula/ui/week/WeekScreen.kt | 40 ++++++++++++------- .../calendula/ui/week/WeekViewModel.kt | 1 + app/src/main/res/values/strings.xml | 1 + .../calendula/ui/common/ViewBackStackTest.kt | 8 ++++ 8 files changed, 59 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6206c7f..804dab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Pages can open the split agenda.** Swipe up from a full month to reveal the compact grid and month agenda; tap a day to filter it, then swipe down to return. The transition gives haptic feedback ([#111]). +- **Week numbers open their week.** Tap a week number in the month grid to open Week at that row; Back returns to Month ([#197]). ### Changed - **Split view opens with the month's agenda.** It starts at today in the current month; tapping a day filters the list, and the filter chip restores the full month. Swiping the list moves by day when filtered and by month otherwise ([#142]). 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 5f445ac..d0ece8a 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/CalendarHost.kt @@ -34,6 +34,7 @@ import de.jeanlucmakiola.calendula.ui.common.LocalEventMove import de.jeanlucmakiola.calendula.ui.common.LocalManageCalendars import de.jeanlucmakiola.calendula.ui.common.RescheduleViewModel import de.jeanlucmakiola.calendula.ui.common.drillToDay +import de.jeanlucmakiola.calendula.ui.common.drillToWeek import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec import de.jeanlucmakiola.calendula.ui.common.selectView import de.jeanlucmakiola.calendula.ui.common.viewBaseStack @@ -112,6 +113,11 @@ fun CalendarHost( pendingDayIso = date.toString() viewStack = viewStack.drillToDay() } + var pendingWeekIso by rememberSaveable { mutableStateOf(null) } + val onOpenWeek: (LocalDate) -> Unit = { date -> + pendingWeekIso = date.toString() + viewStack = viewStack.drillToWeek() + } // The event-detail screen (S4) is a full-screen destination hoisted here so // it overlays whichever calendar view is active. We forward the tapped @@ -380,6 +386,8 @@ fun CalendarHost( quickSwitchViews = quickSwitchViews, drawerViewOrder = drawerViewOrder, todayInToolbar = todayInToolbar, + initialDateIso = pendingWeekIso, + onInitialDateConsumed = { pendingWeekIso = null }, ) CalendarView.MultiDay -> MultiDayScreen( selectedView = currentView, @@ -409,6 +417,7 @@ fun CalendarHost( selectedView = currentView, onSelectView = onSelectView, onOpenDay = onOpenDay, + onOpenWeek = onOpenWeek, onEventClick = onEventClick, onOpenSettings = onOpenSettings, onOpenSearch = onOpenSearch, 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 01033d9..8574285 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 @@ -129,3 +129,7 @@ fun viewBaseStack(default: CalendarView, source: CalendarView): List.drillToDay(): List = if (lastOrNull() == CalendarView.Day) this else this + CalendarView.Day + +/** Push the week view over the month after a week-number tap. */ +fun List.drillToWeek(): List = + if (lastOrNull() == CalendarView.Week) this else this + CalendarView.Week diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt index 08c4c25..daeee4f 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/month/MonthScreen.kt @@ -206,6 +206,7 @@ import java.time.format.TextStyle as JavaTextStyle import java.util.Locale private val LocalTodayPulse = staticCompositionLocalOf { 0 } +private val LocalOpenWeek = staticCompositionLocalOf<((LocalDate) -> Unit)?> { null } /** A brief push from today's full cell into its immediate neighbours. */ @Composable @@ -252,6 +253,7 @@ fun MonthScreen( selectedView: CalendarView, onSelectView: (CalendarView) -> Unit, onOpenDay: (LocalDate) -> Unit, + onOpenWeek: (LocalDate) -> Unit, onEventClick: (EventInstance) -> Unit, onOpenSettings: () -> Unit, onOpenSearch: () -> Unit, @@ -515,6 +517,7 @@ fun MonthScreen( LocalDimCutoff provides dimCutoff, LocalMonthDrag provides chipDrag, LocalTodayPulse provides todayPulse, + LocalOpenWeek provides onOpenWeek, ) { if (scrolling) { ContinuousMonthContent( @@ -933,7 +936,7 @@ private fun rememberWeekNumberGutter(): Dp { val style = weekNumberStyle() return remember(style, density, measurer) { val text = with(density) { measurer.measure(WEEK_NUMBER_SAMPLE, style).size.width.toDp() } - text + (WEEK_NUMBER_PADDING + CELL_GAP) * 2 + (text + (WEEK_NUMBER_PADDING + CELL_GAP) * 2).coerceAtLeast(48.dp) } } @@ -1675,7 +1678,7 @@ private fun SplitExpandHandle( * it's roughly a third of a paged row — which is the point: the space it gives * up goes to the day pane below. */ -private val SPLIT_ROW_HEIGHT = 46.dp +private val SPLIT_ROW_HEIGHT = 48.dp /** Vertical padding and row gap of [SplitMonthGrid], which [splitGridHeight] pays for. */ private val SPLIT_GRID_VERTICAL_PADDING = 4.dp @@ -2742,8 +2745,13 @@ private fun monthChipDragModifier( private fun WeekNumberGutter(weekStart: LocalDate, modifier: Modifier = Modifier) { val weekNumber = remember(weekStart) { weekStart.toJavaLocalDate().isoWeekNumber() } val label = stringResource(R.string.week_number_label) + val openLabel = stringResource(R.string.month_open_week, weekNumber) + val onOpenWeek = LocalOpenWeek.current Box( modifier = modifier + .then(if (onOpenWeek != null) Modifier.clickable(onClickLabel = openLabel) { + onOpenWeek(weekStart) + } else Modifier) .padding(horizontal = CELL_GAP, vertical = 1.dp) .background(MaterialTheme.colorScheme.secondaryContainer, CELL_SHAPE) .semantics { contentDescription = "$label $weekNumber" }, diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt index 8d157a5..b2360e0 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekScreen.kt @@ -199,23 +199,33 @@ fun WeekScreen( quickSwitchViews: List = IMPLEMENTED_VIEWS, drawerViewOrder: List = IMPLEMENTED_VIEWS, todayInToolbar: Boolean = false, + initialDateIso: String? = null, + onInitialDateConsumed: () -> Unit = {}, modifier: Modifier = Modifier, viewModel: WeekViewModel = hiltViewModel(), -) = PagedTimelineScreen( - viewModel = viewModel, - showWeekNumber = true, - selectedView = selectedView, - onSelectView = onSelectView, - onOpenDay = onOpenDay, - onEventClick = onEventClick, - onOpenSettings = onOpenSettings, - onOpenSearch = onOpenSearch, - onCreateEvent = onCreateEvent, - quickSwitchViews = quickSwitchViews, - drawerViewOrder = drawerViewOrder, - todayInToolbar = todayInToolbar, - modifier = modifier, -) +) { + LaunchedEffect(initialDateIso) { + initialDateIso?.let { + viewModel.goToDate(LocalDate.parse(it)) + onInitialDateConsumed() + } + } + PagedTimelineScreen( + viewModel = viewModel, + showWeekNumber = true, + selectedView = selectedView, + onSelectView = onSelectView, + onOpenDay = onOpenDay, + onEventClick = onEventClick, + onOpenSettings = onOpenSettings, + onOpenSearch = onOpenSearch, + onCreateEvent = onCreateEvent, + quickSwitchViews = quickSwitchViews, + drawerViewOrder = drawerViewOrder, + todayInToolbar = todayInToolbar, + modifier = modifier, + ) +} /** The multi-day view (#193): the week view's timeline over the day count from Settings. */ @Composable diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt index fef5385..df4d21b 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/week/WeekViewModel.kt @@ -150,6 +150,7 @@ abstract class PagedTimelineViewModel( } fun goToToday() = jumpTo(todayDate) + fun goToDate(date: LocalDate) = jumpTo(date) private fun buildState( span: DaySpan, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63e0484..cf9413d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,6 +54,7 @@ Previous month Next month + Open week %1$d Today Show full month agenda More actions 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 fb73dc5..aae8d28 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 @@ -65,6 +65,14 @@ class ViewBackStackTest { assertThat(stack.drillToDay()).isEqualTo(stack) } + @Test + fun `week number drills into week and back returns to month`() { + val stack = listOf(CalendarView.Month).drillToWeek() + assertThat(stack).containsExactly(CalendarView.Month, CalendarView.Week).inOrder() + assertThat(stack.dropLast(1).last()).isEqualTo(CalendarView.Month) + assertThat(stack.drillToWeek()).isEqualTo(stack) + } + @Test fun `agenda widget date tap backs out agenda then home`() { // openDate(source = Agenda): root in Agenda over the Week home, then drill.