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.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) {

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.
* 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