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:
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user