feat(edit): form redesign, optional fields, OptionCard dialogs, expressive motion
All checks were successful
CI / ci (push) Successful in 5m56s
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:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user