From 71a652ce3b53d201e2bf6b4d3a8c3271736e92e1 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Tue, 7 Jul 2026 14:23:23 +0200 Subject: [PATCH] feat: full-screen selection pickers (retire OptionCard modals, bar actions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unify the app's "choose one" surfaces on the full-screen picker style (floret-kit FullScreenPicker/OptionPicker) instead of the OptionCard modal dialogs, for consistency across the app: - Event editor: visibility + add-field -> OptionPicker; reminder, recurrence rule, and colour -> FullScreenPicker, with the custom-value Add / OK and the colour Reset carried in the app-bar via the picker's new `actions` slot; the save-conflict chooser -> full-screen. - The recurring scope choosers stay compact OptionCard popups — saving an edit to, or deleting, a recurring event — since a quick 2-3 option decision reads better as a popup than a near-empty full screen. Bumps the floret-kit pin (55ad536 -> e1919ca) for the FullScreenPicker `actions` passthrough. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calendula/ui/edit/EventEditScreen.kt | 477 +++++++++--------- .../calendula/ui/settings/SettingsScreen.kt | 2 +- floret-kit | 2 +- 3 files changed, 234 insertions(+), 247 deletions(-) diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt index ae1f1e1..66944f1 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/edit/EventEditScreen.kt @@ -134,6 +134,7 @@ import de.jeanlucmakiola.floret.components.GroupedRow import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY import de.jeanlucmakiola.floret.components.InlineTextField import de.jeanlucmakiola.floret.components.OptionCard +import de.jeanlucmakiola.floret.components.OptionPicker import de.jeanlucmakiola.floret.components.Position import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS @@ -345,29 +346,29 @@ private fun SaveConflictDialog( onDiscard: () -> Unit, onDismiss: () -> Unit, ) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_edit_conflict_title)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text(stringResource(R.string.event_edit_conflict_body)) - Spacer(Modifier.height(4.dp)) - OptionCard( - label = stringResource(R.string.event_edit_conflict_overwrite), - supportingText = stringResource(R.string.event_edit_conflict_overwrite_hint), - onClick = onOverwrite, - ) - OptionCard( - label = stringResource(R.string.event_edit_conflict_discard), - supportingText = stringResource(R.string.event_edit_conflict_discard_hint), - onClick = onDiscard, - ) - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - ) + FullScreenPicker( + title = stringResource(R.string.event_edit_conflict_title), + onDismiss = onDismiss, + ) { + Text( + text = stringResource(R.string.event_edit_conflict_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + ) + GroupedRow( + title = stringResource(R.string.event_edit_conflict_overwrite), + summary = stringResource(R.string.event_edit_conflict_overwrite_hint), + position = positionOf(0, 2), + onClick = onOverwrite, + ) + GroupedRow( + title = stringResource(R.string.event_edit_conflict_discard), + summary = stringResource(R.string.event_edit_conflict_discard_hint), + position = positionOf(1, 2), + onClick = onDiscard, + ) + } } /** @@ -1064,24 +1065,19 @@ private fun FieldPickerDialog( onSelect: (EventFormField) -> Unit, onDismiss: () -> Unit, ) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_edit_more_fields)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - hiddenFields.forEach { field -> - OptionCard( - label = stringResource(eventFormFieldLabel(field)), - onClick = { onSelect(field) }, - icon = eventFormFieldIcon(field), - ) - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - ) + FullScreenPicker( + title = stringResource(R.string.event_edit_more_fields), + onDismiss = onDismiss, + ) { + hiddenFields.forEachIndexed { index, field -> + GroupedRow( + title = stringResource(eventFormFieldLabel(field)), + position = positionOf(index, hiddenFields.size), + leading = { Icon(imageVector = eventFormFieldIcon(field), contentDescription = null) }, + onClick = { onSelect(field) }, + ) + } + } } /** Quick-pick lead times offered as chips in the reminder dialog. */ @@ -1106,41 +1102,10 @@ private fun ReminderPickerDialog( ?.takeIf { it in 1..999 } ?.let { it * unit.minutesFactor } - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_edit_add_reminder)) }, - text = { - if (!customMode) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }.forEach { minutes -> - OptionCard( - label = reminderLabel(minutes), - onClick = { onSelect(minutes) }, - ) - } - OptionCard( - label = stringResource(R.string.event_edit_reminder_custom), - onClick = { customMode = true }, - labelColor = MaterialTheme.colorScheme.primary, - ) - } - } else { - Row(verticalAlignment = Alignment.CenterVertically) { - DialogAmountField( - value = amountText, - onValueChange = { amountText = it }, - placeholder = "10", - ) - Spacer(Modifier.width(12.dp)) - DialogUnitDropdown( - label = stringResource(reminderUnitLabel(unit)), - entries = ReminderUnit.entries.map { stringResource(reminderUnitLabel(it)) }, - onPick = { unit = ReminderUnit.entries[it] }, - ) - } - } - }, - confirmButton = { + FullScreenPicker( + title = stringResource(R.string.event_edit_add_reminder), + onDismiss = onDismiss, + actions = { // The quick-pick list adds on tap; only the custom step needs Add. if (customMode) { TextButton( @@ -1149,10 +1114,41 @@ private fun ReminderPickerDialog( ) { Text(stringResource(R.string.event_edit_add)) } } }, - dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - ) + ) { + if (!customMode) { + val presets = REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen } + val rowCount = presets.size + 1 + presets.forEachIndexed { index, minutes -> + GroupedRow( + title = reminderLabel(minutes), + position = positionOf(index, rowCount), + onClick = { onSelect(minutes) }, + ) + } + GroupedRow( + title = stringResource(R.string.event_edit_reminder_custom), + position = positionOf(rowCount - 1, rowCount), + onClick = { customMode = true }, + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + ) { + DialogAmountField( + value = amountText, + onValueChange = { amountText = it }, + placeholder = "10", + ) + Spacer(Modifier.width(12.dp)) + DialogUnitDropdown( + label = stringResource(reminderUnitLabel(unit)), + entries = ReminderUnit.entries.map { stringResource(reminderUnitLabel(it)) }, + onPick = { unit = ReminderUnit.entries[it] }, + ) + } + } + } } /** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */ @@ -1222,109 +1218,10 @@ private fun RecurrencePickerDialog( null } - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_detail_recurrence)) }, - text = { - if (!customMode) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - OptionCard( - label = stringResource(R.string.event_edit_recurrence_none), - onClick = { onSelect(null) }, - selected = current == null, - ) - RecurrenceFreq.entries.forEach { entry -> - OptionCard( - label = stringResource(recurrencePresetLabel(entry)), - onClick = { onSelect(SimpleRecurrence(entry).toRRule()) }, - selected = isPlainPreset && parsed?.freq == entry, - ) - } - OptionCard( - label = stringResource(R.string.event_edit_recurrence_custom), - onClick = { customMode = true }, - selected = current != null && !isPlainPreset, - labelColor = MaterialTheme.colorScheme.primary, - ) - } - } else { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(R.string.event_edit_recurrence_every), - style = MaterialTheme.typography.titleMedium, - ) - Spacer(Modifier.width(12.dp)) - DialogAmountField( - value = intervalText, - onValueChange = { intervalText = it }, - placeholder = "1", - ) - Spacer(Modifier.width(12.dp)) - DialogUnitDropdown( - label = stringResource(recurrenceUnitLabel(freq)), - entries = RecurrenceFreq.entries.map { - stringResource(recurrenceUnitLabel(it)) - }, - onPick = { freq = RecurrenceFreq.entries[it] }, - ) - } - if (freq == RecurrenceFreq.Weekly) { - Spacer(Modifier.height(4.dp)) - WeekdayToggleRow( - selected = daysMask.toDaySet(), - onToggle = { day -> daysMask = daysMask xor day.toMaskBit() }, - locale = locale, - ) - } - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.event_edit_recurrence_ends), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - OptionCard( - label = stringResource(R.string.event_edit_recurrence_end_never), - onClick = { endMode = RecurrenceEndMode.Never }, - selected = endMode == RecurrenceEndMode.Never, - ) - OptionCard( - label = stringResource(R.string.event_edit_recurrence_end_until), - onClick = { - endMode = RecurrenceEndMode.Until - showUntilPicker = true - }, - supportingText = untilDate?.let { - remember(it, locale) { - DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) - .withLocale(locale).format(it.toJavaLocalDate()) - } - }, - selected = endMode == RecurrenceEndMode.Until, - ) - OptionCard( - label = stringResource(R.string.event_edit_recurrence_end_count), - onClick = { endMode = RecurrenceEndMode.Count }, - selected = endMode == RecurrenceEndMode.Count, - ) - if (endMode == RecurrenceEndMode.Count) { - Row(verticalAlignment = Alignment.CenterVertically) { - DialogAmountField( - value = countText, - onValueChange = { countText = it }, - placeholder = "10", - ) - Spacer(Modifier.width(12.dp)) - Text( - text = stringResource(R.string.event_edit_recurrence_times), - style = MaterialTheme.typography.titleMedium, - ) - } - } - } - } - }, - confirmButton = { + FullScreenPicker( + title = stringResource(R.string.event_detail_recurrence), + onDismiss = onDismiss, + actions = { // The preset list applies on tap; only the custom step needs OK. if (customMode) { TextButton( @@ -1333,10 +1230,113 @@ private fun RecurrencePickerDialog( ) { Text(stringResource(R.string.dialog_ok)) } } }, - dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - ) + ) { + if (!customMode) { + val rowCount = RecurrenceFreq.entries.size + 2 + GroupedRow( + title = stringResource(R.string.event_edit_recurrence_none), + position = positionOf(0, rowCount), + selected = current == null, + onClick = { onSelect(null) }, + ) + RecurrenceFreq.entries.forEachIndexed { index, entry -> + GroupedRow( + title = stringResource(recurrencePresetLabel(entry)), + position = positionOf(index + 1, rowCount), + selected = isPlainPreset && parsed?.freq == entry, + onClick = { onSelect(SimpleRecurrence(entry).toRRule()) }, + ) + } + GroupedRow( + title = stringResource(R.string.event_edit_recurrence_custom), + position = positionOf(rowCount - 1, rowCount), + selected = current != null && !isPlainPreset, + onClick = { customMode = true }, + ) + } else { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 24.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.event_edit_recurrence_every), + style = MaterialTheme.typography.titleMedium, + ) + Spacer(Modifier.width(12.dp)) + DialogAmountField( + value = intervalText, + onValueChange = { intervalText = it }, + placeholder = "1", + ) + Spacer(Modifier.width(12.dp)) + DialogUnitDropdown( + label = stringResource(recurrenceUnitLabel(freq)), + entries = RecurrenceFreq.entries.map { + stringResource(recurrenceUnitLabel(it)) + }, + onPick = { freq = RecurrenceFreq.entries[it] }, + ) + } + if (freq == RecurrenceFreq.Weekly) { + WeekdayToggleRow( + selected = daysMask.toDaySet(), + onToggle = { day -> daysMask = daysMask xor day.toMaskBit() }, + locale = locale, + ) + } + Text( + text = stringResource(R.string.event_edit_recurrence_ends), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + GroupedRow( + title = stringResource(R.string.event_edit_recurrence_end_never), + position = positionOf(0, 3), + selected = endMode == RecurrenceEndMode.Never, + onClick = { endMode = RecurrenceEndMode.Never }, + ) + GroupedRow( + title = stringResource(R.string.event_edit_recurrence_end_until), + summary = untilDate?.let { + remember(it, locale) { + DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) + .withLocale(locale).format(it.toJavaLocalDate()) + } + }, + position = positionOf(1, 3), + selected = endMode == RecurrenceEndMode.Until, + onClick = { + endMode = RecurrenceEndMode.Until + showUntilPicker = true + }, + ) + GroupedRow( + title = stringResource(R.string.event_edit_recurrence_end_count), + position = positionOf(2, 3), + selected = endMode == RecurrenceEndMode.Count, + onClick = { endMode = RecurrenceEndMode.Count }, + ) + if (endMode == RecurrenceEndMode.Count) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp), + ) { + DialogAmountField( + value = countText, + onValueChange = { countText = it }, + placeholder = "10", + ) + Spacer(Modifier.width(12.dp)) + Text( + text = stringResource(R.string.event_edit_recurrence_times), + style = MaterialTheme.typography.titleMedium, + ) + } + } + } + } if (showUntilPicker) { CalendarDatePickerDialog( @@ -1701,24 +1701,14 @@ private fun VisibilityPickerDialog( onSelect: (AccessLevel) -> Unit, onDismiss: () -> Unit, ) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_edit_visibility)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - AccessLevel.entries.forEach { level -> - OptionCard( - label = stringResource(accessLevelLabel(level)), - onClick = { onSelect(level) }, - icon = accessLevelIcon(level), - selected = level == selected, - ) - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, + OptionPicker( + title = stringResource(R.string.event_edit_visibility), + options = AccessLevel.entries.toList(), + selected = selected, + label = { stringResource(accessLevelLabel(it)) }, + onSelect = onSelect, + onDismiss = onDismiss, + leading = { Icon(imageVector = accessLevelIcon(it), contentDescription = null) }, ) } @@ -1741,51 +1731,48 @@ private fun ColorPickerDialog( onDismiss: () -> Unit, ) { val dark = isSystemInDarkTheme() - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.event_edit_color)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - if (palette.isNotEmpty()) { - ColorSwatchRow( - colors = palette.map { it.argb }, - selected = selected, - onSelect = { argb -> - palette.firstOrNull { it.argb == argb } - ?.let { onPickKey(it.key, it.argb) } - }, - dark = dark, - ) - } else { - ColorSwatchRow( - colors = CalendarColorPalette.all, - selected = selected, - onSelect = onPickRaw, - dark = dark, - ) - if (syncWarning) { - Text( - text = stringResource(R.string.event_edit_color_sync_warning), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) } - }, - dismissButton = if (hasExplicitColor) { - { + FullScreenPicker( + title = stringResource(R.string.event_edit_color), + onDismiss = onDismiss, + actions = { + if (hasExplicitColor) { TextButton(onClick = onClear) { Text(stringResource(R.string.event_edit_color_reset)) } } - } else { - null }, - ) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + ) { + if (palette.isNotEmpty()) { + ColorSwatchRow( + colors = palette.map { it.argb }, + selected = selected, + onSelect = { argb -> + palette.firstOrNull { it.argb == argb } + ?.let { onPickKey(it.key, it.argb) } + }, + dark = dark, + ) + } else { + ColorSwatchRow( + colors = CalendarColorPalette.all, + selected = selected, + onSelect = onPickRaw, + dark = dark, + ) + if (syncWarning) { + Text( + text = stringResource(R.string.event_edit_color_sync_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + } + } } private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) { diff --git a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt index b2737a8..715f81d 100644 --- a/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/de/jeanlucmakiola/calendula/ui/settings/SettingsScreen.kt @@ -160,7 +160,7 @@ private enum class ChipAccent { Neutral, Primary, Tertiary } * Settings (M4), restructured in v2.3 into a category hub with sub-screens. * Both the hub and the sub-screens use a collapsing [LargeTopAppBar] and the * grouped-row card system. Calendars opens the separate manager hoisted in - * [CalendarHost]; Language opens an inline OptionCard dialog; About is a card + * [CalendarHost]; Language opens a full-screen picker; About is a card * at the top. A full-screen destination; [onBack] pops it. */ @Composable diff --git a/floret-kit b/floret-kit index 55ad536..e1919ca 160000 --- a/floret-kit +++ b/floret-kit @@ -1 +1 @@ -Subproject commit 55ad536513bb8903a097a2aff96c91aafbec9727 +Subproject commit e1919cab83649d1c30a6bd8c7134e8b093156f35