From 366059b0129a21c3b4a06dc491c257ea79b6fd05 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sat, 27 Jun 2026 15:59:56 +0200 Subject: [PATCH] feat(agenda): split range picker into two lists with a concrete header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Group the range options into calendar-aligned (today / this week / this month) and rolling (next 7 / 30 days / custom) lists. Add a header showing the concrete span currently in effect — a single date for Today, the month and year for This month, otherwise a start–end span — so it's clear what the agenda is showing. The header appears only where a window is supplied (the agenda pill), not in Settings. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/agenda/AgendaRange.kt | 26 ++++++ .../calendula/ui/agenda/AgendaScreen.kt | 4 + .../calendula/ui/agenda/AgendaUiState.kt | 2 + .../calendula/ui/agenda/AgendaViewModel.kt | 5 ++ .../calendula/ui/common/Picker.kt | 86 +++++++++++++------ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 97 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt index e1f3bee..1ae2046 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaRange.kt @@ -3,6 +3,9 @@ package de.jeanlucmakiola.calendula.ui.agenda import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import java.time.YearMonth +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle +import java.util.Locale /** * How far ahead the agenda (screen or widget) shows events, starting at today. @@ -82,4 +85,27 @@ fun parseAgendaRange(stored: String?, default: AgendaRange): AgendaRange = when else -> default } +/** + * The concrete span the range covers, starting at [start] through [end] + * (inclusive), for a human-readable header: + * - [AgendaRange.Day] → a single medium date ("27 Jun 2026") + * - [AgendaRange.ThisMonth] → month and year ("June 2026") + * - everything else → "start – end" ("27 Jun – 3 Jul 2026") + */ +fun agendaRangeWindowSummary( + range: AgendaRange, + start: LocalDate, + end: LocalDate, + locale: Locale, +): String { + val javaStart = java.time.LocalDate.of(start.year, start.month.ordinal + 1, start.day) + val javaEnd = java.time.LocalDate.of(end.year, end.month.ordinal + 1, end.day) + val medium = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(locale) + return when (range) { + AgendaRange.Day -> medium.format(javaStart) + AgendaRange.ThisMonth -> DateTimeFormatter.ofPattern("LLLL yyyy", locale).format(javaStart) + else -> "${DateTimeFormatter.ofPattern("d MMM", locale).format(javaStart)} – ${medium.format(javaEnd)}" + } +} + private const val CUSTOM_PREFIX = "CUSTOM:" 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 317d2a7..4b76191 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 @@ -176,12 +176,16 @@ fun AgendaScreen( } if (showRangePicker) { + val locale = currentLocale() AgendaRangePicker( title = stringResource(R.string.settings_agenda_range), description = stringResource(R.string.agenda_range_override_hint), selected = successState?.range ?: AgendaRange.Month, onSelect = viewModel::setRangeOverride, onDismiss = { showRangePicker = false }, + currentWindow = successState?.let { + agendaRangeWindowSummary(it.range, it.anchor, it.rangeEnd, locale) + }, ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt index 594b8f8..5101f75 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaUiState.kt @@ -57,5 +57,7 @@ sealed interface AgendaUiState { val range: AgendaRange, /** True when [range] is a temporary in-view override of the saved default. */ val rangeIsOverride: Boolean, + /** Last day the current [range] covers (inclusive), for the range header. */ + val rangeEnd: LocalDate, ) : AgendaUiState } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt index 7b00397..ae82858 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/agenda/AgendaViewModel.kt @@ -123,12 +123,17 @@ class AgendaViewModel @Inject constructor( } val anchor = params.anchor val days = groupAgendaDays(anchor, instances, zone) + val rangeEnd = anchor.plus( + params.range.dayCount(anchor, params.weekStart) - 1, + DateTimeUnit.DAY, + ) return AgendaUiState.Success( anchor = anchor, today = todayDate, days = days, range = params.range, rangeIsOverride = params.rangeIsOverride, + rangeEnd = rangeEnd, ) } } diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt index 78df630..e1f8d19 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/common/Picker.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -283,9 +284,11 @@ private fun SelectedCheck() { } /** - * Agenda-range picker, full-screen: the fixed 1 day / 1 week / 1 month windows - * as grouped rows, plus a "Custom" row that expands an inline day-count editor - * (1–365). Mirrors [ReminderDefaultPicker]'s custom-expand pattern. + * Agenda-range picker, full-screen. Two grouped lists — the calendar-aligned + * options (today / this week / this month) and the rolling windows (next 7 / 30 + * days), with a "Custom" row that expands an inline day-count editor (1–365). + * When [currentWindow] is given, a header shows the concrete span in effect. + * Mirrors [ReminderDefaultPicker]'s custom-expand pattern. */ @Composable fun AgendaRangePicker( @@ -294,40 +297,50 @@ fun AgendaRangePicker( selected: AgendaRange, onSelect: (AgendaRange) -> Unit, onDismiss: () -> Unit, + currentWindow: String? = null, ) { - val presets = listOf( - AgendaRange.Day, - AgendaRange.ThisWeek, - AgendaRange.ThisMonth, - AgendaRange.Week, - AgendaRange.Month, - ) + val calendarAligned = listOf(AgendaRange.Day, AgendaRange.ThisWeek, AgendaRange.ThisMonth) + val rolling = listOf(AgendaRange.Week, AgendaRange.Month) + val rollingRowCount = rolling.size + 1 // + the custom row val customSelected = selected is AgendaRange.Custom - val rowCount = presets.size + 1 // + the custom row var customExpanded by rememberSaveable { mutableStateOf(false) } var amountText by rememberSaveable { mutableStateOf((selected as? AgendaRange.Custom)?.days?.toString() ?: "") } + val rangeRow: @Composable (AgendaRange, Position) -> Unit = { option, position -> + val isSelected = option == selected + GroupedRow( + title = agendaRangeLabel(option), + position = position, + selected = isSelected, + trailing = if (isSelected) { + { SelectedCheck() } + } else { + null + }, + onClick = { + onSelect(option) + onDismiss() + }, + ) + } + FullScreenPicker(title = title, onDismiss = onDismiss) { + if (currentWindow != null) AgendaRangeHeader(currentWindow) PickerDescription(description) - presets.forEachIndexed { index, option -> - val isSelected = option == selected - GroupedRow( - title = agendaRangeLabel(option), - position = positionOf(index, rowCount), - selected = isSelected, - trailing = if (isSelected) { - { SelectedCheck() } - } else { - null - }, - onClick = { - onSelect(option) - onDismiss() - }, - ) + + // Calendar-aligned windows. + calendarAligned.forEachIndexed { index, option -> + rangeRow(option, positionOf(index, calendarAligned.size)) + } + + Spacer(Modifier.height(12.dp)) + + // Rolling windows + custom. + rolling.forEachIndexed { index, option -> + rangeRow(option, positionOf(index, rollingRowCount)) } GroupedRow( title = if (customSelected) { @@ -335,7 +348,7 @@ fun AgendaRangePicker( } else { stringResource(R.string.agenda_range_custom) }, - position = if (customExpanded) Position.Top else positionOf(presets.size, rowCount), + position = if (customExpanded) Position.Top else positionOf(rolling.size, rollingRowCount), selected = customSelected, trailing = if (customSelected) { { SelectedCheck() } @@ -361,6 +374,23 @@ fun AgendaRangePicker( } } +/** The "Showing all events for " header at the top of the range picker. */ +@Composable +private fun AgendaRangeHeader(window: String) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + Text( + text = stringResource(R.string.agenda_range_showing_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = window, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } +} + /** The expanded "Custom" day-count editor: an amount field (1–365) and Set. */ @Composable private fun CustomDaysEditor( diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a5907ef..51d28c3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -294,6 +294,7 @@ Benutzerdefiniert… Tage Nur vorübergehend — wird beim erneuten Öffnen von Calendula auf deinen gespeicherten Zeitraum zurückgesetzt. + Alle Termine für %d Tag %d Tage diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dff94df..4cd9e56 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -287,6 +287,7 @@ Custom… Days Just for now — resets to your saved range when you reopen Calendula. + Showing all events for %d day %d days