fix(edit): honour week-start pref in recurrence picker + smooth weekday collapse

- The weekday toggles now order by the app's "Week starts on" setting
  (via WeekStartPref.resolveFirstDay), matching the month/week/agenda
  views, instead of always following the device locale. Threaded through
  a new EventEditViewModel.firstDayOfWeek flow.
- Carry the 16dp section gaps on each block rather than a parent
  spacedBy, so the weekday card's gap collapses together with the card
  under AnimatedVisibility — removing the end-of-animation jump in the
  ends section when switching away from Weekly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-20 13:04:57 +02:00
parent ccd433fee1
commit 722fc87259
2 changed files with 33 additions and 11 deletions

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