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.toJavaDayOfWeek
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toKotlinDayOfWeek
import kotlinx.datetime.toJavaLocalTime
import kotlinx.datetime.toLocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.time.format.TextStyle as JavaTextStyle
import java.time.temporal.WeekFields
import java.util.Locale
import kotlin.time.Clock
@@ -502,6 +500,7 @@ private fun EventEditContent(
) {
val form = state.form
val locale = currentLocale()
val firstDayOfWeek by viewModel.firstDayOfWeek.collectAsStateWithLifecycle()
val dark = isSystemInDarkTheme()
// The title, date and recurrence are managed by the special-dates sync, so
// they're locked here; everything else (reminders, location, notes) is the
@@ -1122,6 +1121,7 @@ private fun EventEditContent(
RecurrencePickerDialog(
current = form.rrule,
startDay = form.start.date.dayOfWeek,
firstDayOfWeek = firstDayOfWeek,
onSelect = { rrule ->
viewModel.setRecurrence(rrule)
showRecurrencePicker = false
@@ -1293,6 +1293,7 @@ private enum class RecurrenceEndMode { Never, Until, Count }
private fun RecurrencePickerDialog(
current: String?,
startDay: DayOfWeek,
firstDayOfWeek: DayOfWeek,
onSelect: (String?) -> Unit,
onDismiss: () -> Unit,
) {
@@ -1381,10 +1382,10 @@ private fun RecurrencePickerDialog(
onClick = { customMode = true },
)
} else {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Section gaps live on each block (not a parent spacedBy) so the
// weekday card's gap collapses *with* it under AnimatedVisibility —
// 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
// effect of the controls below is never a guess.
Text(
@@ -1414,6 +1415,7 @@ private fun RecurrencePickerDialog(
shape = groupedShape(Position.Alone),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
) {
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) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = groupedShape(Position.Alone),
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.padding(horizontal = 16.dp),
) {
WeekdayToggleRow(
selected = daysMask.toDaySet(),
onToggle = { day -> daysMask = daysMask xor day.toMaskBit() },
locale = locale,
firstDay = firstDayOfWeek,
modifier = Modifier.padding(16.dp),
)
}
@@ -1467,7 +1472,7 @@ private fun RecurrencePickerDialog(
// Ends: a connected never / on-a-date / after-N group, the count
// field folding into the bottom of the run when chosen.
Column {
Column(modifier = Modifier.padding(top = 16.dp)) {
Text(
text = stringResource(R.string.event_edit_recurrence_ends),
style = MaterialTheme.typography.labelLarge,
@@ -1559,11 +1564,11 @@ private fun WeekdayToggleRow(
selected: Set<DayOfWeek>,
onToggle: (DayOfWeek) -> Unit,
locale: Locale,
firstDay: DayOfWeek,
modifier: Modifier = Modifier,
) {
val days = remember(locale) {
val first = WeekFields.of(locale).firstDayOfWeek.toKotlinDayOfWeek()
(0 until 7).map { DayOfWeek(((first.isoDayNumber - 1 + it) % 7) + 1) }
val days = remember(firstDay) {
(0 until 7).map { DayOfWeek(((firstDay.isoDayNumber - 1 + it) % 7) + 1) }
}
Row(
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.prefs.CalendarPrefs
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.resolveFirstDay
import de.jeanlucmakiola.calendula.domain.AccessLevel
import de.jeanlucmakiola.calendula.domain.Availability
import de.jeanlucmakiola.calendula.domain.CalendarSource
@@ -40,12 +42,14 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Clock
import kotlin.time.Duration.Companion.hours
@@ -267,6 +271,19 @@ class EventEditViewModel @Inject constructor(
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
* from midnight) anchors the start when the form is opened by tapping a slot