chore: draw shared code from floret-kit (fresh re-migration onto current main)

Supersedes the stale chore/floret-kit-core-time branch: re-applies the
floret-kit migration on top of current main (122 commits ahead of the old
branch's base), pinning the kit at the multi-value-reminders + pinned-picker HEAD.

- Submodule + composite build (includeBuild), 6 module deps, CI submodules:
  recursive, reproducible-release scan extended to the kit, F-Droid recipe.
- Deletes the inline copies now owned by the kit (GroupedList, Picker scaffolds,
  InlineTextField, OptionCard, DialogControls, CrashReporter + dialog/submit,
  OnboardingScaffold, AppLanguage, TimeBridge, ReorderableColumn, DebugRibbon)
  and redraws them from components/identity/core-crash/core-locale/core-time.
- Reminder overrides drawn from core-reminders (multi-value ReminderOverride +
  codec); Calendula keeps its app-specific bits (all-day resolution, labels,
  presets, the multi-select ReminderDefaultPicker, its own CrashReportActivity).
- Theme draws FloretExpressiveTheme while keeping the user-typography param.

Build pending (deferred): run ./gradlew :app:compileDebugKotlin with ANDROID_HOME
(or floret-kit/local.properties) set.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 22:01:14 +02:00
parent bb7954d026
commit 955d47ef12
64 changed files with 397 additions and 2002 deletions

View File

@@ -1,26 +0,0 @@
package de.jeanlucmakiola.calendula.data.calendar
import com.google.common.truth.Truth.assertThat
import kotlin.time.Instant
import org.junit.jupiter.api.Test
class TimeBridgeTest {
@Test
fun `epoch millis round-trips through Instant`() {
val original = 1_717_840_800_000L // 2024-06-08T10:00:00Z
val instant = original.toKotlinInstantFromEpochMillis()
assertThat(instant.toEpochMillis()).isEqualTo(original)
}
@Test
fun `zero millis maps to Instant epoch`() {
assertThat(0L.toKotlinInstantFromEpochMillis()).isEqualTo(Instant.fromEpochMilliseconds(0L))
}
@Test
fun `negative epoch millis is supported`() {
val original = -1_000_000L
assertThat(original.toKotlinInstantFromEpochMillis().toEpochMillis()).isEqualTo(original)
}
}

View File

@@ -7,7 +7,7 @@ import androidx.datastore.preferences.core.Preferences
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.data.calendar.FakeCalendarDataSource
import de.jeanlucmakiola.calendula.data.calendar.ManagedEventRow
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
import de.jeanlucmakiola.floret.reminders.ReminderOverride
import de.jeanlucmakiola.calendula.data.prefs.SettingsPrefs
import de.jeanlucmakiola.calendula.domain.contacts.ContactSpecialDate
import de.jeanlucmakiola.calendula.domain.contacts.SpecialDateType
@@ -169,7 +169,7 @@ class SpecialDatesSyncEngineTest {
engine.sync()
val birthdayCal = settings.specialDatesCalendars.first().getValue(SpecialDateType.Birthday)
engine.applyReminders(SpecialDateType.Birthday, CalendarReminderOverride.Minutes(listOf(0, 1440)))
engine.applyReminders(SpecialDateType.Birthday, ReminderOverride.Minutes(listOf(0, 1440)))
// Persisted as the per-calendar all-day override (so new events match)...
assertThat(settings.perCalendarAllDayReminderOverride.first()[birthdayCal])
@@ -187,7 +187,7 @@ class SpecialDatesSyncEngineTest {
engine.sync()
val birthdayCal = settings.specialDatesCalendars.first().getValue(SpecialDateType.Birthday)
engine.applyReminders(SpecialDateType.Birthday, CalendarReminderOverride.None)
engine.applyReminders(SpecialDateType.Birthday, ReminderOverride.None)
assertThat(settings.perCalendarAllDayReminderOverride.first()[birthdayCal]).isEmpty()
assertThat(calendars.appliedManagedReminders.last().third).isEmpty()

View File

@@ -1,8 +1,17 @@
package de.jeanlucmakiola.calendula.data.crash
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.floret.crash.CrashContext
import de.jeanlucmakiola.floret.crash.buildCrashReport
import org.junit.jupiter.api.Test
/**
* Guards the privacy contract of the report Calendula ships: the report builder
* itself now lives in floret-kit's core-crash, but the allowlist — exactly the
* app/Android/device/locale/time header lines plus the stack trace, and nothing
* else — is what Calendula relies on, so it stays pinned here with Calendula's
* own app label.
*/
class CrashReportBuilderTest {
private val context = CrashContext(
@@ -17,7 +26,7 @@ class CrashReportBuilderTest {
@Test
fun `report carries the allowlisted facts and the stack trace`() {
val report = buildCrashReport(context, IllegalStateException("boom"), nowMillis = 0L)
val report = buildCrashReport(context, IllegalStateException("boom"), nowMillis = 0L, appLabel = "Calendula")
assertThat(report).startsWith("Calendula crash report")
assertThat(report).contains("App version: 2.7.0 (20700)")
@@ -33,7 +42,7 @@ class CrashReportBuilderTest {
@Test
fun `nested causes are included`() {
val cause = NullPointerException("inner")
val report = buildCrashReport(context, RuntimeException("outer", cause), nowMillis = 0L)
val report = buildCrashReport(context, RuntimeException("outer", cause), nowMillis = 0L, appLabel = "Calendula")
assertThat(report).contains("outer")
assertThat(report).contains("Caused by")
@@ -42,7 +51,7 @@ class CrashReportBuilderTest {
@Test
fun `report holds only the allowlisted lines before the stack trace`() {
val report = buildCrashReport(context, Exception("x"), nowMillis = 0L)
val report = buildCrashReport(context, Exception("x"), nowMillis = 0L, appLabel = "Calendula")
val header = report.substringBefore("Stack trace:").trim().lines()
// No identifiers, accounts, or extra fields ever creep into the header:

View File

@@ -1,5 +1,7 @@
package de.jeanlucmakiola.calendula.data.prefs
import de.jeanlucmakiola.floret.reminders.ReminderOverride
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
@@ -323,14 +325,14 @@ class SettingsPrefsTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.perCalendarReminderOverride.first()).isEmpty()
prefs.setCalendarReminderOverride(7L, CalendarReminderOverride.Minutes(listOf(15, 10_080)))
prefs.setCalendarReminderOverride(9L, CalendarReminderOverride.None)
prefs.setCalendarReminderOverride(7L, ReminderOverride.Minutes(listOf(15, 10_080)))
prefs.setCalendarReminderOverride(9L, ReminderOverride.None)
prefs.perCalendarReminderOverride.first().let { map ->
assertThat(map).containsExactly(7L, listOf(15, 10_080), 9L, emptyList<Int>())
}
// Inherit drops the override entirely (absent != an empty-list value).
prefs.setCalendarReminderOverride(9L, CalendarReminderOverride.Inherit)
prefs.setCalendarReminderOverride(9L, ReminderOverride.Inherit)
prefs.perCalendarReminderOverride.first().let { map ->
assertThat(map).containsExactly(7L, listOf(15, 10_080))
assertThat(map.containsKey(9L)).isFalse()
@@ -370,12 +372,12 @@ class SettingsPrefsTest {
@Test
fun `per-calendar all-day override round-trips independently of the timed one`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
prefs.setCalendarReminderOverride(7L, CalendarReminderOverride.Minutes(listOf(15)))
prefs.setCalendarAllDayReminderOverride(7L, CalendarReminderOverride.Minutes(listOf(1_440)))
prefs.setCalendarReminderOverride(7L, ReminderOverride.Minutes(listOf(15)))
prefs.setCalendarAllDayReminderOverride(7L, ReminderOverride.Minutes(listOf(1_440)))
assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(7L, listOf(15))
assertThat(prefs.perCalendarAllDayReminderOverride.first()).containsExactly(7L, listOf(1_440))
// Clearing the all-day override leaves the timed one untouched.
prefs.setCalendarAllDayReminderOverride(7L, CalendarReminderOverride.Inherit)
prefs.setCalendarAllDayReminderOverride(7L, ReminderOverride.Inherit)
assertThat(prefs.perCalendarAllDayReminderOverride.first()).isEmpty()
assertThat(prefs.perCalendarReminderOverride.first()).containsExactly(7L, listOf(15))
}

View File

@@ -1,35 +0,0 @@
package de.jeanlucmakiola.calendula.ui.common
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.calendula.data.prefs.CalendarReminderOverride
import org.junit.jupiter.api.Test
/**
* The pure emit rule behind [ReminderDefaultPicker]: a non-empty selection is
* [CalendarReminderOverride.Minutes] (normalised), while clearing the last time
* reverts to [CalendarReminderOverride.Inherit] on a per-calendar picker (so an
* accidental toggle-undo can't silently wipe the calendar's default) and to
* explicit [CalendarReminderOverride.None] on the global default.
*/
class ReminderOverrideForMinutesTest {
@Test
fun `empty selection on a per-calendar picker reverts to inherit`() {
assertThat(reminderOverrideForMinutes(emptyList(), allowInherit = true))
.isEqualTo(CalendarReminderOverride.Inherit)
}
@Test
fun `empty selection on the global default is explicit none`() {
assertThat(reminderOverrideForMinutes(emptyList(), allowInherit = false))
.isEqualTo(CalendarReminderOverride.None)
}
@Test
fun `a non-empty selection is normalised minutes regardless of scope`() {
assertThat(reminderOverrideForMinutes(listOf(30, 10, 10, 60), allowInherit = true))
.isEqualTo(CalendarReminderOverride.Minutes(listOf(10, 30, 60)))
assertThat(reminderOverrideForMinutes(listOf(30, 10, 10, 60), allowInherit = false))
.isEqualTo(CalendarReminderOverride.Minutes(listOf(10, 30, 60)))
}
}