feat(agenda): range header banner + toggles for banner and pill
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:
@@ -137,6 +137,24 @@ class SettingsPrefs @Inject constructor(
|
||||
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 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 AGENDA_SCREEN_RANGE_KEY = stringPreferencesKey("agenda_screen_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 SHOW_HOUR_LINES_KEY = booleanPreferencesKey("show_hour_lines")
|
||||
internal val DEFAULT_VIEW_KEY = stringPreferencesKey("default_view")
|
||||
|
||||
@@ -150,17 +150,27 @@ fun AgendaScreen(
|
||||
},
|
||||
) { innerPadding ->
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AgendaContent(
|
||||
state = state,
|
||||
onRetry = viewModel::goToToday,
|
||||
onEventClick = onEventClick,
|
||||
Column(
|
||||
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
|
||||
// session, mirroring the FAB column on the right.
|
||||
successState?.let { s ->
|
||||
successState?.takeIf { it.showRangePill }?.let { s ->
|
||||
AgendaRangePill(
|
||||
range = s.range,
|
||||
isOverride = s.rangeIsOverride,
|
||||
@@ -176,16 +186,12 @@ 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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
private fun AgendaContent(
|
||||
state: AgendaUiState,
|
||||
|
||||
@@ -59,5 +59,9 @@ sealed interface AgendaUiState {
|
||||
val rangeIsOverride: Boolean,
|
||||
/** Last day the current [range] covers (inclusive), for the range header. */
|
||||
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
|
||||
}
|
||||
|
||||
@@ -43,7 +43,12 @@ class AgendaViewModel @Inject constructor(
|
||||
@IoDispatcher private val io: CoroutineDispatcher,
|
||||
) : 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.
|
||||
private val weekStartDay = settingsPrefs.weekStart
|
||||
@@ -63,12 +68,14 @@ class AgendaViewModel @Inject constructor(
|
||||
private val _rangeOverride = MutableStateFlow<AgendaRange?>(null)
|
||||
|
||||
val state: StateFlow<AgendaUiState> =
|
||||
combine(_anchor, screenRange, _rangeOverride, weekStartDay) { anchor, default, override, weekStart ->
|
||||
combine(_anchor, agendaSettings, _rangeOverride, weekStartDay) { anchor, settings, override, weekStart ->
|
||||
AgendaParams(
|
||||
anchor = anchor,
|
||||
range = override ?: default,
|
||||
rangeIsOverride = override != null && override != default,
|
||||
range = override ?: settings.range,
|
||||
rangeIsOverride = override != null && override != settings.range,
|
||||
weekStart = weekStart,
|
||||
showRangeBanner = settings.showBanner,
|
||||
showRangePill = settings.showPill,
|
||||
)
|
||||
}
|
||||
.flatMapLatest { params ->
|
||||
@@ -106,11 +113,19 @@ class AgendaViewModel @Inject constructor(
|
||||
_rangeOverride.value = range
|
||||
}
|
||||
|
||||
private data class AgendaSettings(
|
||||
val range: AgendaRange,
|
||||
val showBanner: Boolean,
|
||||
val showPill: Boolean,
|
||||
)
|
||||
|
||||
private data class AgendaParams(
|
||||
val anchor: LocalDate,
|
||||
val range: AgendaRange,
|
||||
val rangeIsOverride: Boolean,
|
||||
val weekStart: DayOfWeek,
|
||||
val showRangeBanner: Boolean,
|
||||
val showRangePill: Boolean,
|
||||
)
|
||||
|
||||
private fun buildState(
|
||||
@@ -134,6 +149,8 @@ class AgendaViewModel @Inject constructor(
|
||||
range = params.range,
|
||||
rangeIsOverride = params.rangeIsOverride,
|
||||
rangeEnd = rangeEnd,
|
||||
showRangeBanner = params.showRangeBanner,
|
||||
showRangePill = params.showRangePill,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,6 @@ private fun SelectedCheck() {
|
||||
* 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
|
||||
@@ -297,7 +296,6 @@ fun AgendaRangePicker(
|
||||
selected: AgendaRange,
|
||||
onSelect: (AgendaRange) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
currentWindow: String? = null,
|
||||
) {
|
||||
val calendarAligned = listOf(AgendaRange.Day, AgendaRange.ThisWeek, AgendaRange.ThisMonth)
|
||||
val rolling = listOf(AgendaRange.Week, AgendaRange.Month)
|
||||
@@ -328,7 +326,6 @@ fun AgendaRangePicker(
|
||||
}
|
||||
|
||||
FullScreenPicker(title = title, onDismiss = onDismiss) {
|
||||
if (currentWindow != null) AgendaRangeHeader(currentWindow)
|
||||
PickerDescription(description)
|
||||
|
||||
// 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 (1–365) and Set. */
|
||||
@Composable
|
||||
private fun CustomDaysEditor(
|
||||
|
||||
@@ -514,9 +514,33 @@ private fun AppearanceScreen(
|
||||
GroupedRow(
|
||||
title = stringResource(R.string.settings_agenda_widget_range),
|
||||
summary = agendaRangeLabel(state.agendaWidgetRange),
|
||||
position = Position.Bottom,
|
||||
position = Position.Middle,
|
||||
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) {
|
||||
|
||||
@@ -28,6 +28,10 @@ data class SettingsUiState(
|
||||
val agendaScreenRange: AgendaRange = AgendaRange.Month,
|
||||
/** How far ahead the agenda widget shows events (v2.11). */
|
||||
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). */
|
||||
val defaultView: CalendarView = CalendarView.Week,
|
||||
/** Optional event-form fields shown by default (rest behind "more fields"). */
|
||||
|
||||
@@ -84,13 +84,19 @@ class SettingsViewModel @Inject constructor(
|
||||
) { 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(
|
||||
defaultView = views.defaultView,
|
||||
agendaScreenRange = views.agendaScreenRange,
|
||||
agendaWidgetRange = views.agendaWidgetRange,
|
||||
timeFormat = views.timeFormat,
|
||||
showHourLines = views.showHourLines,
|
||||
agendaShowRangeBanner = agendaToggles.first,
|
||||
agendaShowRangePill = agendaToggles.second,
|
||||
allowColorOnUnsupportedCalendars = defaults.allowColor,
|
||||
defaultReminderMinutes = defaults.defaultReminder,
|
||||
defaultAllDayReminderMinutes = defaults.allDayReminder,
|
||||
@@ -148,6 +154,14 @@ class SettingsViewModel @Inject constructor(
|
||||
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) {
|
||||
viewModelScope.launch { prefs.setTimeFormat(pref) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user