release: v2.14.0 — Day view on date-header tap #61

Merged
makiolaj merged 36 commits from release/v2.14.0 into main 2026-07-07 14:42:30 +00:00
3 changed files with 234 additions and 247 deletions
Showing only changes of commit 71a652ce3b - Show all commits

View File

@@ -134,6 +134,7 @@ import de.jeanlucmakiola.floret.components.GroupedRow
import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY import de.jeanlucmakiola.calendula.ui.common.MILLIS_PER_DAY
import de.jeanlucmakiola.floret.components.InlineTextField import de.jeanlucmakiola.floret.components.InlineTextField
import de.jeanlucmakiola.floret.components.OptionCard import de.jeanlucmakiola.floret.components.OptionCard
import de.jeanlucmakiola.floret.components.OptionPicker
import de.jeanlucmakiola.floret.components.Position import de.jeanlucmakiola.floret.components.Position
import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.floret.components.positionOf
import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS import de.jeanlucmakiola.calendula.ui.common.REMINDER_PRESETS
@@ -345,29 +346,29 @@ private fun SaveConflictDialog(
onDiscard: () -> Unit, onDiscard: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
AlertDialog( FullScreenPicker(
onDismissRequest = onDismiss, title = stringResource(R.string.event_edit_conflict_title),
title = { Text(stringResource(R.string.event_edit_conflict_title)) }, onDismiss = onDismiss,
text = { ) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text(
Text(stringResource(R.string.event_edit_conflict_body)) text = stringResource(R.string.event_edit_conflict_body),
Spacer(Modifier.height(4.dp)) style = MaterialTheme.typography.bodyMedium,
OptionCard( color = MaterialTheme.colorScheme.onSurfaceVariant,
label = stringResource(R.string.event_edit_conflict_overwrite), modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
supportingText = stringResource(R.string.event_edit_conflict_overwrite_hint), )
GroupedRow(
title = stringResource(R.string.event_edit_conflict_overwrite),
summary = stringResource(R.string.event_edit_conflict_overwrite_hint),
position = positionOf(0, 2),
onClick = onOverwrite, onClick = onOverwrite,
) )
OptionCard( GroupedRow(
label = stringResource(R.string.event_edit_conflict_discard), title = stringResource(R.string.event_edit_conflict_discard),
supportingText = stringResource(R.string.event_edit_conflict_discard_hint), summary = stringResource(R.string.event_edit_conflict_discard_hint),
position = positionOf(1, 2),
onClick = onDiscard, onClick = onDiscard,
) )
} }
},
confirmButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
},
)
} }
/** /**
@@ -1064,24 +1065,19 @@ private fun FieldPickerDialog(
onSelect: (EventFormField) -> Unit, onSelect: (EventFormField) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
AlertDialog( FullScreenPicker(
onDismissRequest = onDismiss, title = stringResource(R.string.event_edit_more_fields),
title = { Text(stringResource(R.string.event_edit_more_fields)) }, onDismiss = onDismiss,
text = { ) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { hiddenFields.forEachIndexed { index, field ->
hiddenFields.forEach { field -> GroupedRow(
OptionCard( title = stringResource(eventFormFieldLabel(field)),
label = stringResource(eventFormFieldLabel(field)), position = positionOf(index, hiddenFields.size),
leading = { Icon(imageVector = eventFormFieldIcon(field), contentDescription = null) },
onClick = { onSelect(field) }, onClick = { onSelect(field) },
icon = eventFormFieldIcon(field),
) )
} }
} }
},
confirmButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
},
)
} }
/** Quick-pick lead times offered as chips in the reminder dialog. */ /** Quick-pick lead times offered as chips in the reminder dialog. */
@@ -1106,26 +1102,39 @@ private fun ReminderPickerDialog(
?.takeIf { it in 1..999 } ?.takeIf { it in 1..999 }
?.let { it * unit.minutesFactor } ?.let { it * unit.minutesFactor }
AlertDialog( FullScreenPicker(
onDismissRequest = onDismiss, title = stringResource(R.string.event_edit_add_reminder),
title = { Text(stringResource(R.string.event_edit_add_reminder)) }, onDismiss = onDismiss,
text = { actions = {
// The quick-pick list adds on tap; only the custom step needs Add.
if (customMode) {
TextButton(
enabled = customMinutes != null && customMinutes !in alreadyChosen,
onClick = { customMinutes?.let(onSelect) },
) { Text(stringResource(R.string.event_edit_add)) }
}
},
) {
if (!customMode) { if (!customMode) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { val presets = REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }
REMINDER_QUICK_PICKS.filterNot { it in alreadyChosen }.forEach { minutes -> val rowCount = presets.size + 1
OptionCard( presets.forEachIndexed { index, minutes ->
label = reminderLabel(minutes), GroupedRow(
title = reminderLabel(minutes),
position = positionOf(index, rowCount),
onClick = { onSelect(minutes) }, onClick = { onSelect(minutes) },
) )
} }
OptionCard( GroupedRow(
label = stringResource(R.string.event_edit_reminder_custom), title = stringResource(R.string.event_edit_reminder_custom),
position = positionOf(rowCount - 1, rowCount),
onClick = { customMode = true }, onClick = { customMode = true },
labelColor = MaterialTheme.colorScheme.primary,
) )
}
} else { } else {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
) {
DialogAmountField( DialogAmountField(
value = amountText, value = amountText,
onValueChange = { amountText = it }, onValueChange = { amountText = it },
@@ -1139,20 +1148,7 @@ private fun ReminderPickerDialog(
) )
} }
} }
},
confirmButton = {
// The quick-pick list adds on tap; only the custom step needs Add.
if (customMode) {
TextButton(
enabled = customMinutes != null && customMinutes !in alreadyChosen,
onClick = { customMinutes?.let(onSelect) },
) { Text(stringResource(R.string.event_edit_add)) }
} }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
},
)
} }
/** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */ /** How a custom recurrence ends; mirrors [RecurrenceEnd] in saveable form. */
@@ -1222,33 +1218,46 @@ private fun RecurrencePickerDialog(
null null
} }
AlertDialog( FullScreenPicker(
onDismissRequest = onDismiss, title = stringResource(R.string.event_detail_recurrence),
title = { Text(stringResource(R.string.event_detail_recurrence)) }, onDismiss = onDismiss,
text = { actions = {
// The preset list applies on tap; only the custom step needs OK.
if (customMode) {
TextButton(
enabled = customResult != null,
onClick = { customResult?.let(onSelect) },
) { Text(stringResource(R.string.dialog_ok)) }
}
},
) {
if (!customMode) { if (!customMode) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { val rowCount = RecurrenceFreq.entries.size + 2
OptionCard( GroupedRow(
label = stringResource(R.string.event_edit_recurrence_none), title = stringResource(R.string.event_edit_recurrence_none),
onClick = { onSelect(null) }, position = positionOf(0, rowCount),
selected = current == null, selected = current == null,
onClick = { onSelect(null) },
) )
RecurrenceFreq.entries.forEach { entry -> RecurrenceFreq.entries.forEachIndexed { index, entry ->
OptionCard( GroupedRow(
label = stringResource(recurrencePresetLabel(entry)), title = stringResource(recurrencePresetLabel(entry)),
onClick = { onSelect(SimpleRecurrence(entry).toRRule()) }, position = positionOf(index + 1, rowCount),
selected = isPlainPreset && parsed?.freq == entry, selected = isPlainPreset && parsed?.freq == entry,
onClick = { onSelect(SimpleRecurrence(entry).toRRule()) },
) )
} }
OptionCard( GroupedRow(
label = stringResource(R.string.event_edit_recurrence_custom), title = stringResource(R.string.event_edit_recurrence_custom),
onClick = { customMode = true }, position = positionOf(rowCount - 1, rowCount),
selected = current != null && !isPlainPreset, selected = current != null && !isPlainPreset,
labelColor = MaterialTheme.colorScheme.primary, onClick = { customMode = true },
) )
}
} else { } else {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 24.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = stringResource(R.string.event_edit_recurrence_every), text = stringResource(R.string.event_edit_recurrence_every),
@@ -1270,45 +1279,50 @@ private fun RecurrencePickerDialog(
) )
} }
if (freq == RecurrenceFreq.Weekly) { if (freq == RecurrenceFreq.Weekly) {
Spacer(Modifier.height(4.dp))
WeekdayToggleRow( WeekdayToggleRow(
selected = daysMask.toDaySet(), selected = daysMask.toDaySet(),
onToggle = { day -> daysMask = daysMask xor day.toMaskBit() }, onToggle = { day -> daysMask = daysMask xor day.toMaskBit() },
locale = locale, locale = locale,
) )
} }
Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.event_edit_recurrence_ends), text = stringResource(R.string.event_edit_recurrence_ends),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
OptionCard( }
label = stringResource(R.string.event_edit_recurrence_end_never), GroupedRow(
onClick = { endMode = RecurrenceEndMode.Never }, title = stringResource(R.string.event_edit_recurrence_end_never),
position = positionOf(0, 3),
selected = endMode == RecurrenceEndMode.Never, selected = endMode == RecurrenceEndMode.Never,
onClick = { endMode = RecurrenceEndMode.Never },
) )
OptionCard( GroupedRow(
label = stringResource(R.string.event_edit_recurrence_end_until), title = stringResource(R.string.event_edit_recurrence_end_until),
onClick = { summary = untilDate?.let {
endMode = RecurrenceEndMode.Until
showUntilPicker = true
},
supportingText = untilDate?.let {
remember(it, locale) { remember(it, locale) {
DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM) DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(locale).format(it.toJavaLocalDate()) .withLocale(locale).format(it.toJavaLocalDate())
} }
}, },
position = positionOf(1, 3),
selected = endMode == RecurrenceEndMode.Until, selected = endMode == RecurrenceEndMode.Until,
onClick = {
endMode = RecurrenceEndMode.Until
showUntilPicker = true
},
) )
OptionCard( GroupedRow(
label = stringResource(R.string.event_edit_recurrence_end_count), title = stringResource(R.string.event_edit_recurrence_end_count),
onClick = { endMode = RecurrenceEndMode.Count }, position = positionOf(2, 3),
selected = endMode == RecurrenceEndMode.Count, selected = endMode == RecurrenceEndMode.Count,
onClick = { endMode = RecurrenceEndMode.Count },
) )
if (endMode == RecurrenceEndMode.Count) { if (endMode == RecurrenceEndMode.Count) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp),
) {
DialogAmountField( DialogAmountField(
value = countText, value = countText,
onValueChange = { countText = it }, onValueChange = { countText = it },
@@ -1323,20 +1337,6 @@ private fun RecurrencePickerDialog(
} }
} }
} }
},
confirmButton = {
// The preset list applies on tap; only the custom step needs OK.
if (customMode) {
TextButton(
enabled = customResult != null,
onClick = { customResult?.let(onSelect) },
) { Text(stringResource(R.string.dialog_ok)) }
}
},
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
},
)
if (showUntilPicker) { if (showUntilPicker) {
CalendarDatePickerDialog( CalendarDatePickerDialog(
@@ -1701,24 +1701,14 @@ private fun VisibilityPickerDialog(
onSelect: (AccessLevel) -> Unit, onSelect: (AccessLevel) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
AlertDialog( OptionPicker(
onDismissRequest = onDismiss, title = stringResource(R.string.event_edit_visibility),
title = { Text(stringResource(R.string.event_edit_visibility)) }, options = AccessLevel.entries.toList(),
text = { selected = selected,
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { label = { stringResource(accessLevelLabel(it)) },
AccessLevel.entries.forEach { level -> onSelect = onSelect,
OptionCard( onDismiss = onDismiss,
label = stringResource(accessLevelLabel(level)), leading = { Icon(imageVector = accessLevelIcon(it), contentDescription = null) },
onClick = { onSelect(level) },
icon = accessLevelIcon(level),
selected = level == selected,
)
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
},
) )
} }
@@ -1741,11 +1731,21 @@ private fun ColorPickerDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
val dark = isSystemInDarkTheme() val dark = isSystemInDarkTheme()
AlertDialog( FullScreenPicker(
onDismissRequest = onDismiss, title = stringResource(R.string.event_edit_color),
title = { Text(stringResource(R.string.event_edit_color)) }, onDismiss = onDismiss,
text = { actions = {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { if (hasExplicitColor) {
TextButton(onClick = onClear) {
Text(stringResource(R.string.event_edit_color_reset))
}
}
},
) {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
) {
if (palette.isNotEmpty()) { if (palette.isNotEmpty()) {
ColorSwatchRow( ColorSwatchRow(
colors = palette.map { it.argb }, colors = palette.map { it.argb },
@@ -1772,20 +1772,7 @@ private fun ColorPickerDialog(
} }
} }
} }
},
confirmButton = {
TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
},
dismissButton = if (hasExplicitColor) {
{
TextButton(onClick = onClear) {
Text(stringResource(R.string.event_edit_color_reset))
} }
}
} else {
null
},
)
} }
private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) { private fun accessLevelIcon(level: AccessLevel): ImageVector = when (level) {

View File

@@ -160,7 +160,7 @@ private enum class ChipAccent { Neutral, Primary, Tertiary }
* Settings (M4), restructured in v2.3 into a category hub with sub-screens. * 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 * Both the hub and the sub-screens use a collapsing [LargeTopAppBar] and the
* grouped-row card system. Calendars opens the separate manager hoisted in * 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. * at the top. A full-screen destination; [onBack] pops it.
*/ */
@Composable @Composable