feat(agenda): limit how far ahead the agenda screen and widget show

Add independent agendaScreenRange and agendaWidgetRange preferences, each a
rolling window: 1 day / 1 week / 1 month / custom 1–365 days (default Month).
Rolling (not calendar-aligned) so the span never degenerates near a period
boundary. The in-app Agenda screen and the agenda widget each read their own
setting, configured via a new AgendaRangePicker with an inline custom-days
editor.

Closes #4 (Codeberg)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 14:15:46 +02:00
parent 07a8faf2df
commit add88fbadf
12 changed files with 368 additions and 16 deletions

View File

@@ -5,6 +5,7 @@ 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 de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import kotlinx.datetime.DayOfWeek
@@ -49,6 +50,18 @@ class SettingsPrefsTest {
assertThat(prefs.weekStart.first()).isEqualTo(WeekStartPref.SUNDAY)
}
@Test
fun `agenda ranges default to month and round-trip independently`(@TempDir tempDir: Path) = runTest {
val prefs = SettingsPrefs(newDataStore(tempDir))
assertThat(prefs.agendaScreenRange.first()).isEqualTo(AgendaRange.Month)
assertThat(prefs.agendaWidgetRange.first()).isEqualTo(AgendaRange.Month)
prefs.setAgendaScreenRange(AgendaRange.Custom(14))
prefs.setAgendaWidgetRange(AgendaRange.Week)
assertThat(prefs.agendaScreenRange.first()).isEqualTo(AgendaRange.Custom(14))
assertThat(prefs.agendaWidgetRange.first()).isEqualTo(AgendaRange.Week)
}
@Test
fun `garbage stored enum falls back to default`(@TempDir tempDir: Path) = runTest {
val store = newDataStore(tempDir)

View File

@@ -0,0 +1,50 @@
package de.jeanlucmakiola.calendula.ui.agenda
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
class AgendaRangeTest {
@Test
fun `fixed ranges have 1, 7, 30 day counts`() {
assertThat(AgendaRange.Day.dayCount()).isEqualTo(1)
assertThat(AgendaRange.Week.dayCount()).isEqualTo(7)
assertThat(AgendaRange.Month.dayCount()).isEqualTo(30)
}
@Test
fun `custom day count is clamped to bounds`() {
assertThat(AgendaRange.Custom(45).dayCount()).isEqualTo(45)
assertThat(AgendaRange.Custom(0).dayCount()).isEqualTo(AgendaRange.MIN_CUSTOM_DAYS)
assertThat(AgendaRange.Custom(9_999).dayCount()).isEqualTo(AgendaRange.MAX_CUSTOM_DAYS)
}
@Test
fun `fixed ranges round-trip through storage`() {
listOf(AgendaRange.Day, AgendaRange.Week, AgendaRange.Month).forEach { range ->
assertThat(parseAgendaRange(range.storageValue(), default = AgendaRange.Day))
.isEqualTo(range)
}
}
@Test
fun `custom range round-trips through storage`() {
val range = AgendaRange.Custom(14)
assertThat(range.storageValue()).isEqualTo("CUSTOM:14")
assertThat(parseAgendaRange("CUSTOM:14", default = AgendaRange.Day))
.isEqualTo(AgendaRange.Custom(14))
}
@Test
fun `garbage and null fall back to the default`() {
assertThat(parseAgendaRange(null, default = AgendaRange.Month)).isEqualTo(AgendaRange.Month)
assertThat(parseAgendaRange("YEAR", default = AgendaRange.Week)).isEqualTo(AgendaRange.Week)
assertThat(parseAgendaRange("CUSTOM:abc", default = AgendaRange.Day)).isEqualTo(AgendaRange.Day)
}
@Test
fun `out-of-range custom value clamps on parse`() {
assertThat(parseAgendaRange("CUSTOM:9999", default = AgendaRange.Day))
.isEqualTo(AgendaRange.Custom(AgendaRange.MAX_CUSTOM_DAYS))
}
}