feat(edit): form redesign, optional fields, OptionCard dialogs, expressive motion
All checks were successful
CI / ci (push) Successful in 5m56s

Post-v1.2.0 design iteration on the event form, reviewed slice by slice
on-device:

- Form rebuilt on the detail screen's card system: tonal EditCards with
  gutter icons (centred on the first row, top-aligned for multiline),
  borderless inline fields (placeholders at half opacity), calendar-coloured
  title accent, no dividers, bare top bar
- Optional sections (location, description, reminders, availability,
  visibility) with per-user defaults in Settings ("New event form" toggles);
  hidden ones unfold via a "More fields" picker dialog
- Reminders: stacked rows + full-width borderless add; two-step picker
  (one-tap presets, then custom amount + minutes/hours/days/weeks dropdown);
  written as METHOD_ALERT Reminders rows. Availability busy/free segmented
  toggle; visibility selector with per-level icons
- OptionCard (ui/common) is now the app-wide selection-dialog standard;
  calendar picker, visibility, more-fields, reminder presets and the
  recurring-delete chooser all use it — radio-row dialogs removed
- MaterialExpressiveTheme with MotionScheme.standard() (expressive bounce
  felt overdone); FAB stack + field reveals animate on theme springs;
  jump-to-today slides toward today's actual direction
- IME: adjustResize + imePadding so the keyboard never pans the form
- Tests: form-field prefs round-trips, availability/access provider
  mappings; DE+EN strings throughout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 15:14:30 +02:00
parent 779fa1d480
commit a69be3da43
23 changed files with 1236 additions and 227 deletions

View File

@@ -1,6 +1,9 @@
package de.jeanlucmakiola.calendula.data.calendar
import android.provider.CalendarContract
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.domain.AccessLevel
import de.jeanlucmakiola.calendula.domain.Availability
import de.jeanlucmakiola.calendula.domain.EventForm
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
@@ -36,6 +39,28 @@ class EventWriteMapperTest {
assertThat(times.dtEndMillis - times.dtStartMillis).isEqualTo(86_400_000L)
}
@Test
fun `availability maps to the provider constants`() {
assertThat(Availability.Busy.toProviderValue())
.isEqualTo(CalendarContract.Events.AVAILABILITY_BUSY)
assertThat(Availability.Free.toProviderValue())
.isEqualTo(CalendarContract.Events.AVAILABILITY_FREE)
assertThat(Availability.Tentative.toProviderValue())
.isEqualTo(CalendarContract.Events.AVAILABILITY_TENTATIVE)
}
@Test
fun `access level maps to the provider constants`() {
assertThat(AccessLevel.Default.toProviderValue())
.isEqualTo(CalendarContract.Events.ACCESS_DEFAULT)
assertThat(AccessLevel.Private.toProviderValue())
.isEqualTo(CalendarContract.Events.ACCESS_PRIVATE)
assertThat(AccessLevel.Confidential.toProviderValue())
.isEqualTo(CalendarContract.Events.ACCESS_CONFIDENTIAL)
assertThat(AccessLevel.Public.toProviderValue())
.isEqualTo(CalendarContract.Events.ACCESS_PUBLIC)
}
@Test
fun `multi-day all-day event spans every covered day`() {
val times = form(

View File

@@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.domain.EventFormField
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.DayOfWeek
@@ -60,6 +61,45 @@ class SettingsPrefsTest {
assertThat(prefs.themeMode.first()).isEqualTo(ThemeMode.SYSTEM)
}
@Test
fun `form fields default to location and description`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.defaultFormFields.first()).containsExactly(
EventFormField.Location,
EventFormField.Description,
)
}
@Test
fun `form field toggle round-trips`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
prefs.setFormFieldDefault(EventFormField.Reminders, enabled = true)
prefs.setFormFieldDefault(EventFormField.Location, enabled = false)
assertThat(prefs.defaultFormFields.first()).containsExactly(
EventFormField.Description,
EventFormField.Reminders,
)
}
@Test
fun `disabling every form field persists as none, not factory default`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
EventFormField.entries.forEach { prefs.setFormFieldDefault(it, enabled = false) }
assertThat(prefs.defaultFormFields.first()).isEmpty()
}
@Test
fun `unknown stored form-field names are dropped`(@TempDir tempDir: Path) = runTest {
val store = newDataStore(tempDir)
val prefs = SettingsPrefs(store)
store.updateData { p ->
val m = p.toMutablePreferences()
m[SettingsPrefs.FORM_FIELDS_KEY] = "Location,Hologram"
m
}
assertThat(prefs.defaultFormFields.first()).containsExactly(EventFormField.Location)
}
@Test
fun `explicit week-start prefs resolve regardless of locale`() {
assertThat(WeekStartPref.MONDAY.resolveFirstDay(Locale.US)).isEqualTo(DayOfWeek.MONDAY)