feat(agenda): range header banner + toggles for banner and pill
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 4m29s

Move the "Showing all events for <window>" line out of the picker and into
the agenda view as an always-visible header above the list. Add two
settings (Agenda group, both ON by default) to toggle the range header
banner and the bottom-left range pill independently. The picker keeps its
two-list grouping but no longer carries the header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 16:08:23 +02:00
parent 366059b012
commit 6ba7f38887
10 changed files with 142 additions and 38 deletions

View File

@@ -137,6 +137,24 @@ class SettingsPrefs @Inject constructor(
store.edit { it[AGENDA_WIDGET_RANGE_KEY] = range.storageValue() } store.edit { it[AGENDA_WIDGET_RANGE_KEY] = range.storageValue() }
} }
/** Whether the agenda shows its range-header banner (v2.11). Default ON. */
val agendaShowRangeBanner: Flow<Boolean> = store.data.map { prefs ->
prefs[AGENDA_SHOW_RANGE_BANNER_KEY] ?: true
}
suspend fun setAgendaShowRangeBanner(enabled: Boolean) {
store.edit { it[AGENDA_SHOW_RANGE_BANNER_KEY] = enabled }
}
/** Whether the agenda shows the bottom-left range pill (v2.11). Default ON. */
val agendaShowRangePill: Flow<Boolean> = store.data.map { prefs ->
prefs[AGENDA_SHOW_RANGE_PILL_KEY] ?: true
}
suspend fun setAgendaShowRangePill(enabled: Boolean) {
store.edit { it[AGENDA_SHOW_RANGE_PILL_KEY] = enabled }
}
/** /**
* The calendar view the app opens on (M1). Defaults to [CalendarView.Week] — * The calendar view the app opens on (M1). Defaults to [CalendarView.Week] —
* the historical hard-coded startup view — so existing users see no change * the historical hard-coded startup view — so existing users see no change
@@ -328,6 +346,8 @@ class SettingsPrefs @Inject constructor(
internal val WEEK_START_KEY = stringPreferencesKey("week_start") internal val WEEK_START_KEY = stringPreferencesKey("week_start")
internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range") internal val AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_range")
internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range") internal val AGENDA_WIDGET_RANGE_KEY = stringPreferencesKey("agenda_widget_range")
internal val AGENDA_SHOW_RANGE_BANNER_KEY = booleanPreferencesKey("agenda_show_range_banner")
internal val AGENDA_SHOW_RANGE_PILL_KEY = booleanPreferencesKey("agenda_show_range_pill")
internal val TIME_FORMAT_KEY = stringPreferencesKey("time_format") internal val TIME_FORMAT_KEY = stringPreferencesKey("time_format")
internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines") internal val SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines")
internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view") internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")

View File

@@ -150,17 +150,27 @@ fun AgendaScreen(
}, },
) { innerPadding -> ) { innerPadding ->
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
AgendaContent( Column(
state = state,
onRetry = viewModel::goToToday,
onEventClick = onEventClick,
modifier = Modifier modifier = Modifier
.padding(innerPadding) .fillMaxSize()
.fillMaxSize(), .padding(innerPadding),
) ) {
// A header naming the concrete window currently shown.
successState?.takeIf { it.showRangeBanner }?.let { s ->
AgendaRangeBanner(range = s.range, start = s.anchor, end = s.rangeEnd)
}
AgendaContent(
state = state,
onRetry = viewModel::goToToday,
onEventClick = onEventClick,
modifier = Modifier
.weight(1f)
.fillMaxWidth(),
)
}
// A bottom-left pill to peek at a different range for this // A bottom-left pill to peek at a different range for this
// session, mirroring the FAB column on the right. // session, mirroring the FAB column on the right.
successState?.let { s -> successState?.takeIf { it.showRangePill }?.let { s ->
AgendaRangePill( AgendaRangePill(
range = s.range, range = s.range,
isOverride = s.rangeIsOverride, isOverride = s.rangeIsOverride,
@@ -176,16 +186,12 @@ fun AgendaScreen(
} }
if (showRangePicker) { if (showRangePicker) {
val locale = currentLocale()
AgendaRangePicker( AgendaRangePicker(
title = stringResource(R.string.settings_agenda_range), title = stringResource(R.string.settings_agenda_range),
description = stringResource(R.string.agenda_range_override_hint), description = stringResource(R.string.agenda_range_override_hint),
selected = successState?.range ?: AgendaRange.Month, selected = successState?.range ?: AgendaRange.Month,
onSelect = viewModel::setRangeOverride, onSelect = viewModel::setRangeOverride,
onDismiss = { showRangePicker = false }, onDismiss = { showRangePicker = false },
currentWindow = successState?.let {
agendaRangeWindowSummary(it.range, it.anchor, it.rangeEnd, locale)
},
) )
} }
} }
@@ -236,6 +242,33 @@ private fun AgendaRangePill(
} }
} }
/**
* A header naming the concrete window currently shown, e.g. "Showing all events
* for · Today, 27 Jun 2026" / "This week, 27 Jun 3 Jul" / "This month, June 2026".
*/
@Composable
private fun AgendaRangeBanner(
range: AgendaRange,
start: LocalDate,
end: LocalDate,
modifier: Modifier = Modifier,
) {
val locale = currentLocale()
val window = agendaRangeWindowSummary(range, start, end, locale)
Column(modifier = modifier.padding(horizontal = 28.dp, vertical = 8.dp)) {
Text(
text = stringResource(R.string.agenda_range_showing_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = "${agendaRangeLabel(range)}, $window",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
)
}
}
@Composable @Composable
private fun AgendaContent( private fun AgendaContent(
state: AgendaUiState, state: AgendaUiState,

View File

@@ -59,5 +59,9 @@ sealed interface AgendaUiState {
val rangeIsOverride: Boolean, val rangeIsOverride: Boolean,
/** Last day the current [range] covers (inclusive), for the range header. */ /** Last day the current [range] covers (inclusive), for the range header. */
val rangeEnd: LocalDate, val rangeEnd: LocalDate,
/** Whether to show the range header banner (settings toggle, on by default). */
val showRangeBanner: Boolean,
/** Whether to show the bottom-left range pill (settings toggle, on by default). */
val showRangePill: Boolean,
) : AgendaUiState ) : AgendaUiState
} }

View File

@@ -43,7 +43,12 @@ class AgendaViewModel @Inject constructor(
@IoDispatcher private val io: CoroutineDispatcher, @IoDispatcher private val io: CoroutineDispatcher,
) : ViewModel() { ) : ViewModel() {
private val screenRange = settingsPrefs.agendaScreenRange // The saved agenda range plus the banner/pill visibility toggles.
private val agendaSettings = combine(
settingsPrefs.agendaScreenRange,
settingsPrefs.agendaShowRangeBanner,
settingsPrefs.agendaShowRangePill,
) { range, showBanner, showPill -> AgendaSettings(range, showBanner, showPill) }
// First day of the week, for the calendar-aligned "this week" range. // First day of the week, for the calendar-aligned "this week" range.
private val weekStartDay = settingsPrefs.weekStart private val weekStartDay = settingsPrefs.weekStart
@@ -63,12 +68,14 @@ class AgendaViewModel @Inject constructor(
private val _rangeOverride = MutableStateFlow<AgendaRange?>(null) private val _rangeOverride = MutableStateFlow<AgendaRange?>(null)
val state: StateFlow<AgendaUiState> = val state: StateFlow<AgendaUiState> =
combine(_anchor, screenRange, _rangeOverride, weekStartDay) { anchor, default, override, weekStart -> combine(_anchor, agendaSettings, _rangeOverride, weekStartDay) { anchor, settings, override, weekStart ->
AgendaParams( AgendaParams(
anchor = anchor, anchor = anchor,
range = override ?: default, range = override ?: settings.range,
rangeIsOverride = override != null && override != default, rangeIsOverride = override != null && override != settings.range,
weekStart = weekStart, weekStart = weekStart,
showRangeBanner = settings.showBanner,
showRangePill = settings.showPill,
) )
} }
.flatMapLatest { params -> .flatMapLatest { params ->
@@ -106,11 +113,19 @@ class AgendaViewModel @Inject constructor(
_rangeOverride.value = range _rangeOverride.value = range
} }
private data class AgendaSettings(
val range: AgendaRange,
val showBanner: Boolean,
val showPill: Boolean,
)
private data class AgendaParams( private data class AgendaParams(
val anchor: LocalDate, val anchor: LocalDate,
val range: AgendaRange, val range: AgendaRange,
val rangeIsOverride: Boolean, val rangeIsOverride: Boolean,
val weekStart: DayOfWeek, val weekStart: DayOfWeek,
val showRangeBanner: Boolean,
val showRangePill: Boolean,
) )
private fun buildState( private fun buildState(
@@ -134,6 +149,8 @@ class AgendaViewModel @Inject constructor(
range = params.range, range = params.range,
rangeIsOverride = params.rangeIsOverride, rangeIsOverride = params.rangeIsOverride,
rangeEnd = rangeEnd, rangeEnd = rangeEnd,
showRangeBanner = params.showRangeBanner,
showRangePill = params.showRangePill,
) )
} }
} }

View File

@@ -287,7 +287,6 @@ private fun SelectedCheck() {
* Agenda-range picker, full-screen. Two grouped lists — the calendar-aligned * Agenda-range picker, full-screen. Two grouped lists — the calendar-aligned
* options (today / this week / this month) and the rolling windows (next 7 / 30 * 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 (1365). * days), with a "Custom" row that expands an inline day-count editor (1365).
* When [currentWindow] is given, a header shows the concrete span in effect.
* Mirrors [ReminderDefaultPicker]'s custom-expand pattern. * Mirrors [ReminderDefaultPicker]'s custom-expand pattern.
*/ */
@Composable @Composable
@@ -297,7 +296,6 @@ fun AgendaRangePicker(
selected: AgendaRange, selected: AgendaRange,
onSelect: (AgendaRange) -> Unit, onSelect: (AgendaRange) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
currentWindow: String? = null,
) { ) {
val calendarAligned = listOf(AgendaRange.Day, AgendaRange.ThisWeek, AgendaRange.ThisMonth) val calendarAligned = listOf(AgendaRange.Day, AgendaRange.ThisWeek, AgendaRange.ThisMonth)
val rolling = listOf(AgendaRange.Week, AgendaRange.Month) val rolling = listOf(AgendaRange.Week, AgendaRange.Month)
@@ -328,7 +326,6 @@ fun AgendaRangePicker(
} }
FullScreenPicker(title = title, onDismiss = onDismiss) { FullScreenPicker(title = title, onDismiss = onDismiss) {
if (currentWindow != null) AgendaRangeHeader(currentWindow)
PickerDescription(description) PickerDescription(description)
// Calendar-aligned windows. // Calendar-aligned windows.
@@ -374,23 +371,6 @@ fun AgendaRangePicker(
} }
} }
/** The "Showing all events for <span>" 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 (1365) and Set. */ /** The expanded "Custom" day-count editor: an amount field (1365) and Set. */
@Composable @Composable
private fun CustomDaysEditor( private fun CustomDaysEditor(

View File

@@ -514,9 +514,33 @@ private fun AppearanceScreen(
GroupedRow( GroupedRow(
title = stringResource(R.string.settings_agenda_widget_range), title = stringResource(R.string.settings_agenda_widget_range),
summary = agendaRangeLabel(state.agendaWidgetRange), summary = agendaRangeLabel(state.agendaWidgetRange),
position = Position.Bottom, position = Position.Middle,
onClick = { showAgendaWidgetRange = true }, onClick = { showAgendaWidgetRange = true },
) )
GroupedRow(
title = stringResource(R.string.settings_agenda_range_title),
summary = stringResource(R.string.settings_agenda_range_title_hint),
position = Position.Middle,
trailing = {
Switch(
checked = state.agendaShowRangeBanner,
onCheckedChange = viewModel::setAgendaShowRangeBanner,
)
},
onClick = { viewModel.setAgendaShowRangeBanner(!state.agendaShowRangeBanner) },
)
GroupedRow(
title = stringResource(R.string.settings_agenda_range_button),
summary = stringResource(R.string.settings_agenda_range_button_hint),
position = Position.Bottom,
trailing = {
Switch(
checked = state.agendaShowRangePill,
onCheckedChange = viewModel::setAgendaShowRangePill,
)
},
onClick = { viewModel.setAgendaShowRangePill(!state.agendaShowRangePill) },
)
} }
if (showTheme) { if (showTheme) {

View File

@@ -28,6 +28,10 @@ data class SettingsUiState(
val agendaScreenRange: AgendaRange = AgendaRange.Month, val agendaScreenRange: AgendaRange = AgendaRange.Month,
/** How far ahead the agenda widget shows events (v2.11). */ /** How far ahead the agenda widget shows events (v2.11). */
val agendaWidgetRange: AgendaRange = AgendaRange.Month, val agendaWidgetRange: AgendaRange = AgendaRange.Month,
/** Whether the agenda shows its range-header banner (v2.11). */
val agendaShowRangeBanner: Boolean = true,
/** Whether the agenda shows the bottom-left range pill (v2.11). */
val agendaShowRangePill: Boolean = true,
/** The calendar view the app opens on, and the home of the view back stack (M1). */ /** The calendar view the app opens on, and the home of the view back stack (M1). */
val defaultView: CalendarView = CalendarView.Week, val defaultView: CalendarView = CalendarView.Week,
/** Optional event-form fields shown by default (rest behind "more fields"). */ /** Optional event-form fields shown by default (rest behind "more fields"). */

View File

@@ -84,13 +84,19 @@ class SettingsViewModel @Inject constructor(
) { view, screenRange, widgetRange, timeFormat, showHourLines -> ) { view, screenRange, widgetRange, timeFormat, showHourLines ->
ViewSettings(view, screenRange, widgetRange, timeFormat, showHourLines) ViewSettings(view, screenRange, widgetRange, timeFormat, showHourLines)
}, },
) { base, defaults, overrides, views -> combine(
prefs.agendaShowRangeBanner,
prefs.agendaShowRangePill,
) { banner, pill -> banner to pill },
) { base, defaults, overrides, views, agendaToggles ->
base.copy( base.copy(
defaultView = views.defaultView, defaultView = views.defaultView,
agendaScreenRange = views.agendaScreenRange, agendaScreenRange = views.agendaScreenRange,
agendaWidgetRange = views.agendaWidgetRange, agendaWidgetRange = views.agendaWidgetRange,
timeFormat = views.timeFormat, timeFormat = views.timeFormat,
showHourLines = views.showHourLines, showHourLines = views.showHourLines,
agendaShowRangeBanner = agendaToggles.first,
agendaShowRangePill = agendaToggles.second,
allowColorOnUnsupportedCalendars = defaults.allowColor, allowColorOnUnsupportedCalendars = defaults.allowColor,
defaultReminderMinutes = defaults.defaultReminder, defaultReminderMinutes = defaults.defaultReminder,
defaultAllDayReminderMinutes = defaults.allDayReminder, defaultAllDayReminderMinutes = defaults.allDayReminder,
@@ -148,6 +154,14 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { prefs.setAgendaWidgetRange(range) } viewModelScope.launch { prefs.setAgendaWidgetRange(range) }
} }
fun setAgendaShowRangeBanner(enabled: Boolean) {
viewModelScope.launch { prefs.setAgendaShowRangeBanner(enabled) }
}
fun setAgendaShowRangePill(enabled: Boolean) {
viewModelScope.launch { prefs.setAgendaShowRangePill(enabled) }
}
fun setTimeFormat(pref: TimeFormatPref) { fun setTimeFormat(pref: TimeFormatPref) {
viewModelScope.launch { prefs.setTimeFormat(pref) } viewModelScope.launch { prefs.setTimeFormat(pref) }
} }

View File

@@ -286,6 +286,10 @@
<string name="settings_agenda_range_hint">Wie weit im Voraus die Agenda-Ansicht Termine anzeigt.</string> <string name="settings_agenda_range_hint">Wie weit im Voraus die Agenda-Ansicht Termine anzeigt.</string>
<string name="settings_agenda_widget_range">Agenda-Widget-Zeitraum</string> <string name="settings_agenda_widget_range">Agenda-Widget-Zeitraum</string>
<string name="settings_agenda_widget_range_hint">Wie weit im Voraus das Agenda-Widget auf dem Startbildschirm Termine anzeigt.</string> <string name="settings_agenda_widget_range_hint">Wie weit im Voraus das Agenda-Widget auf dem Startbildschirm Termine anzeigt.</string>
<string name="settings_agenda_range_title">Zeitraum-Titel</string>
<string name="settings_agenda_range_title_hint">Eine Überschrift anzeigen, die den dargestellten Zeitraum benennt</string>
<string name="settings_agenda_range_button">Zeitraum-Schaltfläche</string>
<string name="settings_agenda_range_button_hint">Eine Schaltfläche in der Agenda anzeigen, um den Zeitraum für die Sitzung zu wechseln</string>
<string name="agenda_range_day">Heute</string> <string name="agenda_range_day">Heute</string>
<string name="agenda_range_this_week">Diese Woche</string> <string name="agenda_range_this_week">Diese Woche</string>
<string name="agenda_range_this_month">Dieser Monat</string> <string name="agenda_range_this_month">Dieser Monat</string>

View File

@@ -279,6 +279,10 @@
<string name="settings_agenda_range_hint">How far ahead the Agenda screen lists events.</string> <string name="settings_agenda_range_hint">How far ahead the Agenda screen lists events.</string>
<string name="settings_agenda_widget_range">Agenda widget range</string> <string name="settings_agenda_widget_range">Agenda widget range</string>
<string name="settings_agenda_widget_range_hint">How far ahead the agenda home-screen widget lists events.</string> <string name="settings_agenda_widget_range_hint">How far ahead the agenda home-screen widget lists events.</string>
<string name="settings_agenda_range_title">Range title</string>
<string name="settings_agenda_range_title_hint">Show a header naming the dates the agenda is showing</string>
<string name="settings_agenda_range_button">Range button</string>
<string name="settings_agenda_range_button_hint">Show a button in the agenda to switch the range for the session</string>
<string name="agenda_range_day">Today</string> <string name="agenda_range_day">Today</string>
<string name="agenda_range_this_week">This week</string> <string name="agenda_range_this_week">This week</string>
<string name="agenda_range_this_month">This month</string> <string name="agenda_range_this_month">This month</string>