fix(edit): recurrence picker review fixes + picker unification (#42) #89

Merged
makiolaj merged 6 commits from feat/recurrence-exdate into release/v2.16.0 2026-07-20 13:29:16 +00:00
2 changed files with 33 additions and 11 deletions
Showing only changes of commit 722fc87259 - Show all commits

View File

@@ -163,14 +163,12 @@ import kotlinx.datetime.TimeZone
import kotlinx.datetime.isoDayNumber import kotlinx.datetime.isoDayNumber
import kotlinx.datetime.toJavaDayOfWeek import kotlinx.datetime.toJavaDayOfWeek
import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toKotlinDayOfWeek
import kotlinx.datetime.toJavaLocalTime import kotlinx.datetime.toJavaLocalTime
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.time.format.TextStyle as JavaTextStyle import java.time.format.TextStyle as JavaTextStyle
import java.time.temporal.WeekFields
import java.util.Locale import java.util.Locale
import kotlin.time.Clock import kotlin.time.Clock
@@ -502,6 +500,7 @@ private fun EventEditContent(
) { ) {
val form = state.form val form = state.form
val locale = currentLocale() val locale = currentLocale()
val firstDayOfWeek by viewModel.firstDayOfWeek.collectAsStateWithLifecycle()
val dark = isSystemInDarkTheme() val dark = isSystemInDarkTheme()
// The title, date and recurrence are managed by the special-dates sync, so // The title, date and recurrence are managed by the special-dates sync, so
// they're locked here; everything else (reminders, location, notes) is the // they're locked here; everything else (reminders, location, notes) is the
@@ -1122,6 +1121,7 @@ private fun EventEditContent(
RecurrencePickerDialog( RecurrencePickerDialog(
current = form.rrule, current = form.rrule,
startDay = form.start.date.dayOfWeek, startDay = form.start.date.dayOfWeek,
firstDayOfWeek = firstDayOfWeek,
onSelect = { rrule -> onSelect = { rrule ->
viewModel.setRecurrence(rrule) viewModel.setRecurrence(rrule)
showRecurrencePicker = false showRecurrencePicker = false
@@ -1293,6 +1293,7 @@ private enum class RecurrenceEndMode { Never, Until, Count }
private fun RecurrencePickerDialog( private fun RecurrencePickerDialog(
current: String?, current: String?,
startDay: DayOfWeek, startDay: DayOfWeek,
firstDayOfWeek: DayOfWeek,
onSelect: (String?) -> Unit, onSelect: (String?) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
@@ -1381,10 +1382,10 @@ private fun RecurrencePickerDialog(
onClick = { customMode = true }, onClick = { customMode = true },
) )
} else { } else {
Column( // Section gaps live on each block (not a parent spacedBy) so the
modifier = Modifier.fillMaxWidth(), // weekday card's gap collapses *with* it under AnimatedVisibility —
verticalArrangement = Arrangement.spacedBy(16.dp), // a parent arrangement would leave a residual gap that snaps shut.
) { Column(modifier = Modifier.fillMaxWidth()) {
// A live, human-readable read-out of the rule being built, so the // A live, human-readable read-out of the rule being built, so the
// effect of the controls below is never a guess. // effect of the controls below is never a guess.
Text( Text(
@@ -1414,6 +1415,7 @@ private fun RecurrencePickerDialog(
shape = groupedShape(Position.Alone), shape = groupedShape(Position.Alone),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
) { ) {
Column( Column(
@@ -1447,19 +1449,22 @@ private fun RecurrencePickerDialog(
} }
} }
// Weekday picks — only meaningful on a weekly rule. // Weekday picks — only meaningful on a weekly rule. The 16dp gap
// sits inside the animated block so it grows/shrinks with the card.
AnimatedVisibility(visible = freq == RecurrenceFreq.Weekly) { AnimatedVisibility(visible = freq == RecurrenceFreq.Weekly) {
Surface( Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = groupedShape(Position.Alone), shape = groupedShape(Position.Alone),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 16.dp)
.padding(horizontal = 16.dp), .padding(horizontal = 16.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,
firstDay = firstDayOfWeek,
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
) )
} }
@@ -1467,7 +1472,7 @@ private fun RecurrencePickerDialog(
// Ends: a connected never / on-a-date / after-N group, the count // Ends: a connected never / on-a-date / after-N group, the count
// field folding into the bottom of the run when chosen. // field folding into the bottom of the run when chosen.
Column { Column(modifier = Modifier.padding(top = 16.dp)) {
Text( Text(
text = stringResource(R.string.event_edit_recurrence_ends), text = stringResource(R.string.event_edit_recurrence_ends),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
@@ -1559,11 +1564,11 @@ private fun WeekdayToggleRow(
selected: Set<DayOfWeek>, selected: Set<DayOfWeek>,
onToggle: (DayOfWeek) -> Unit, onToggle: (DayOfWeek) -> Unit,
locale: Locale, locale: Locale,
firstDay: DayOfWeek,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val days = remember(locale) { val days = remember(firstDay) {
val first = WeekFields.of(locale).firstDayOfWeek.toKotlinDayOfWeek() (0 until 7).map { DayOfWeek(((firstDay.isoDayNumber - 1 + it) % 7) + 1) }
(0 until 7).map { DayOfWeek(((first.isoDayNumber - 1 + it) % 7) + 1) }
} }
Row( Row(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,

View File

@@ -8,7 +8,9 @@ import de.jeanlucmakiola.calendula.data.calendar.NoSuchEventException
import de.jeanlucmakiola.calendula.data.di.IoDispatcher import de.jeanlucmakiola.calendula.data.di.IoDispatcher
import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs import de.jeanlucmakiola.calendula.data.prefs.CalendarPrefs
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.data.prefs.WeekStartPref
import de.jeanlucmakiola.calendula.data.prefs.resolveDefaultReminder import de.jeanlucmakiola.calendula.data.prefs.resolveDefaultReminder
import de.jeanlucmakiola.calendula.data.prefs.resolveFirstDay
import de.jeanlucmakiola.calendula.domain.AccessLevel import de.jeanlucmakiola.calendula.domain.AccessLevel
import de.jeanlucmakiola.calendula.domain.Availability import de.jeanlucmakiola.calendula.domain.Availability
import de.jeanlucmakiola.calendula.domain.CalendarSource import de.jeanlucmakiola.calendula.domain.CalendarSource
@@ -40,12 +42,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
@@ -267,6 +271,19 @@ class EventEditViewModel @Inject constructor(
initialValue = null, initialValue = null,
) )
/**
* First day of the week for ordering the recurrence weekday toggles — the
* app's "week starts on" preference, matching the calendar views (Auto
* falls back to the locale convention).
*/
val firstDayOfWeek: StateFlow<DayOfWeek> = settingsPrefs.weekStart
.map { it.resolveFirstDay(Locale.getDefault()) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = WeekStartPref.Auto.resolveFirstDay(Locale.getDefault()),
)
/** /**
* Initialise a fresh form for a new event on [date]. [startMinutes] (minutes * Initialise a fresh form for a new event on [date]. [startMinutes] (minutes
* from midnight) anchors the start when the form is opened by tapping a slot * from midnight) anchors the start when the form is opened by tapping a slot