Put the day view on the paged timeline (#184)

This commit is contained in:
2026-09-27 19:13:47 +02:00
parent 39ca7d869a
commit f9738b0c23
13 changed files with 150 additions and 1325 deletions
@@ -50,7 +50,7 @@ import de.jeanlucmakiola.calendula.ui.common.drillToDay
import de.jeanlucmakiola.calendula.ui.common.rememberCalendarSlideSpec
import de.jeanlucmakiola.calendula.ui.common.selectView
import de.jeanlucmakiola.calendula.ui.common.viewBaseStack
import de.jeanlucmakiola.calendula.ui.day.DayScreen
import de.jeanlucmakiola.calendula.ui.week.DayScreen
import de.jeanlucmakiola.calendula.ui.detail.EventDetailScreen
import de.jeanlucmakiola.calendula.ui.edit.EventEditScreen
import de.jeanlucmakiola.calendula.ui.edit.ImportSource
@@ -1,22 +0,0 @@
package de.jeanlucmakiola.calendula.ui.day
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.LocalDate
import kotlinx.datetime.daysUntil
import kotlinx.datetime.plus
/** Pages the day pager spans — a little under four centuries, centred on [PAGE_EPOCH]. */
internal const val DAY_PAGE_COUNT: Int = 140_000
private const val EPOCH_PAGE = DAY_PAGE_COUNT / 2
/** The day at [EPOCH_PAGE]; any date works, it only has to stay fixed. */
private val PAGE_EPOCH = LocalDate(2000, 1, 1)
/** The day shown on pager [page]. */
internal fun dayForPage(page: Int): LocalDate =
PAGE_EPOCH.plus(page - EPOCH_PAGE, DateTimeUnit.DAY)
/** The pager page showing [date]. */
internal fun dayPageFor(date: LocalDate): Int =
(EPOCH_PAGE + PAGE_EPOCH.daysUntil(date)).coerceIn(0, DAY_PAGE_COUNT - 1)
File diff suppressed because it is too large Load Diff
@@ -1,25 +0,0 @@
package de.jeanlucmakiola.calendula.ui.day
import de.jeanlucmakiola.calendula.domain.FailureReason
import de.jeanlucmakiola.calendula.ui.week.AllDaySpan
import de.jeanlucmakiola.calendula.ui.week.TimedBlock
import kotlinx.datetime.LocalDate
/**
* The day view is a single-column slice of the week view (spec S3). It reuses the
* week's [TimedBlock] and [AllDaySpan] layout primitives — for one day, all-day
* spans collapse to a single column ([AllDaySpan.startCol] == [AllDaySpan.endCol]
* == 0) and only their [AllDaySpan.lane] (vertical stacking) matters.
*/
sealed interface DayUiState {
data object Loading : DayUiState
data class Failure(val reason: FailureReason) : DayUiState
data class Success(
val date: LocalDate,
val today: LocalDate,
/** All-day/multi-day events covering this day, stacked by lane. */
val allDay: List<AllDaySpan>,
/** Timed events clipped to this day with overlap lanes resolved. */
val timed: List<TimedBlock>,
) : DayUiState
}
@@ -1,136 +0,0 @@
package de.jeanlucmakiola.calendula.ui.day
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import de.jeanlucmakiola.calendula.data.calendar.CalendarRepository
import de.jeanlucmakiola.calendula.data.di.IoDispatcher
import de.jeanlucmakiola.calendula.domain.CalendarSource
import de.jeanlucmakiola.calendula.domain.EventInstance
import de.jeanlucmakiola.calendula.domain.FailureReason
import de.jeanlucmakiola.calendula.domain.calendarListFailure
import de.jeanlucmakiola.calendula.ui.common.PageStateCache
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
import de.jeanlucmakiola.calendula.ui.week.layoutDay
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.atTime
import kotlinx.datetime.daysUntil
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
import kotlin.time.Instant
import javax.inject.Inject
import kotlin.math.abs
/** How far from the requested day [DayViewModel.day] keeps other days cached. */
private const val DAY_CACHE_DAYS = 7
@OptIn(ExperimentalCoroutinesApi::class)
@HiltViewModel
class DayViewModel @Inject constructor(
private val repository: CalendarRepository,
@IoDispatcher private val io: CoroutineDispatcher,
) : ViewModel() {
private val zone = TimeZone.currentSystemDefault()
private val todayDate: LocalDate
get() = Clock.System.now().toLocalDateTime(zone).date
private val _date = MutableStateFlow(todayDate)
/** The pager page the anchor day sits on. */
val anchorPage: StateFlow<Int> = _date
.map { dayPageFor(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = dayPageFor(todayDate),
)
/**
* The anchor day's state. Once loaded it never falls back to [DayUiState.Loading]:
* moving the anchor keeps the last result until the new day arrives, since
* the pages draw their own days and this only gates the failure screen.
*/
val state: StateFlow<DayUiState> = _date
.flatMapLatest { date -> day(date).filterNot { it is DayUiState.Loading } }
.catch { emit(DayUiState.Failure(FailureReason.ProviderUnavailable)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000L),
initialValue = DayUiState.Loading,
)
private val days = PageStateCache<LocalDate, DayUiState>(
scope = viewModelScope,
initial = DayUiState.Loading,
// Far enough to cover the pages either side and a swipe back, so paging
// through a month doesn't keep a month of queries around.
keep = { cached, requested -> abs(cached.daysUntil(requested)) <= DAY_CACHE_DAYS },
) { date ->
combine(
repository.calendars(),
repository.instances(dayRange(date, zone)),
) { calendars, instances ->
buildState(date, calendars, instances)
}
.catch { emit(DayUiState.Failure(FailureReason.ProviderUnavailable)) }
.flowOn(io)
}
/** The day [date], shared between its pager page and [state]. Main thread only. */
fun day(date: LocalDate): StateFlow<DayUiState> = days.get(date)
/** The pager came to rest on [page]; follow it unless it is already the anchor's. */
fun onPageSettled(page: Int) {
if (dayPageFor(_date.value) != page) _date.value = dayForPage(page)
}
fun goToToday() {
_date.value = todayDate
}
/** Jump to a specific date (e.g. when opened from the month grid). */
fun goToDate(date: LocalDate) {
_date.value = date
}
private fun buildState(
day: LocalDate,
calendars: List<CalendarSource>,
instances: List<EventInstance>,
): DayUiState {
calendarListFailure(calendars)?.let { return DayUiState.Failure(it) }
val days = listOf(day)
val allDay = instances.filter { it.isAllDay }
val timed = instances.filterNot { it.isAllDay }
return DayUiState.Success(
date = day,
today = todayDate,
allDay = layoutAllDay(allDay, days, zone),
timed = layoutDay(timed, day, zone),
)
}
}
/** Half-open instant range covering the single calendar [date]. */
internal fun dayRange(date: LocalDate, zone: TimeZone): ClosedRange<Instant> {
val from = date.atStartOfDayIn(zone)
val to = date.atTime(23, 59, 59).toInstant(zone)
return from..to
}
@@ -1,63 +0,0 @@
package de.jeanlucmakiola.calendula.ui.day
import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import de.jeanlucmakiola.calendula.ui.common.ScaledViewPreview
import de.jeanlucmakiola.calendula.ui.common.rememberTimelineDragController
import de.jeanlucmakiola.calendula.ui.common.sampleTimelineEvents
import de.jeanlucmakiola.calendula.ui.week.layoutAllDay
import de.jeanlucmakiola.calendula.ui.week.layoutDay
import kotlinx.coroutines.flow.first
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Clock
/**
* A live, scaled-down Day view for the onboarding view chooser. Today's date is
* real; only the events are stand-ins.
*/
@Composable
internal fun DayViewPreview(
height: Dp,
modifier: Modifier = Modifier,
) {
val zone = remember { TimeZone.currentSystemDefault() }
val today = remember(zone) { Clock.System.now().toLocalDateTime(zone).date }
val state = remember(today, zone) { sampleDayState(today, zone) }
val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
snapshotFlow { scrollState.maxValue }.first { it > 0 }
// Half the scroll range is noon — the same centring the live view does.
scrollState.scrollTo(scrollState.maxValue / 2)
}
ScaledViewPreview(height = height, modifier = modifier) {
DaySuccess(
state = state,
scrollState = scrollState,
allDayHeight = state.allDayStripHeight(),
dragController = rememberTimelineDragController(),
onEventClick = {},
onCreateAt = { _, _ -> },
onDrop = {},
)
}
}
/** Sample day state, laid out through the same helpers the live view uses. */
private fun sampleDayState(today: LocalDate, zone: TimeZone): DayUiState.Success {
val days = listOf(today)
val events = sampleTimelineEvents(days, today, zone)
return DayUiState.Success(
date = today,
today = today,
allDay = layoutAllDay(events.filter { it.isAllDay }, days, zone),
timed = layoutDay(events, today, zone),
)
}
@@ -23,7 +23,6 @@ import de.jeanlucmakiola.calendula.ui.common.PickerDescription
import de.jeanlucmakiola.calendula.ui.common.ViewPreviewFrame
import de.jeanlucmakiola.calendula.ui.common.icon
import de.jeanlucmakiola.calendula.ui.common.label
import de.jeanlucmakiola.calendula.ui.day.DayViewPreview
import de.jeanlucmakiola.calendula.ui.month.MonthStylePreview
import de.jeanlucmakiola.calendula.ui.month.MonthViewStyle
import de.jeanlucmakiola.calendula.ui.month.descriptionRes
@@ -73,7 +72,7 @@ internal fun ViewStep(
height = PREVIEW_HEIGHT,
dayCount = LocalMultiDayCount.current,
)
CalendarView.Day -> DayViewPreview(height = PREVIEW_HEIGHT)
CalendarView.Day -> WeekViewPreview(weekStart = weekStart, height = PREVIEW_HEIGHT, dayCount = 1)
CalendarView.Agenda -> AgendaViewPreview(height = PREVIEW_HEIGHT)
}
}
@@ -73,3 +73,23 @@ data class DaysGrid(override val dayCount: Int, val phase: Int) : PageGrid {
DaysGrid(dayCount, Math.floorMod(DAYS_EPOCH.daysUntil(date), dayCount))
}
}
/** Pages the day pager spans — a little under four centuries, centred on [DAY_EPOCH]. */
internal const val DAY_PAGE_COUNT: Int = 140_000
private const val DAY_EPOCH_PAGE = DAY_PAGE_COUNT / 2
/** The day at [DAY_EPOCH_PAGE]; any date works, it only has to stay fixed. */
private val DAY_EPOCH = LocalDate(2000, 1, 1)
/** Single days: every date starts a page of its own. */
data object DayGrid : PageGrid {
override val dayCount: Int get() = 1
override val pageCount: Int get() = DAY_PAGE_COUNT
override fun startForPage(page: Int): LocalDate =
DAY_EPOCH.plus(page - DAY_EPOCH_PAGE, DateTimeUnit.DAY)
override fun pageFor(date: LocalDate): Int =
(DAY_EPOCH_PAGE + DAY_EPOCH.daysUntil(date)).coerceIn(0, DAY_PAGE_COUNT - 1)
}
@@ -66,6 +66,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
@@ -184,6 +185,9 @@ private val HEADER_BOTTOM_PADDING = 8.dp
/** The header's date slot, reserved whether or not it holds today's circle. */
private val DATE_SLOT_SIZE = 28.dp
/** A start-aligned header cell's inset, lining its date up with the event titles below. */
private val HEADER_START_INSET = BLOCK_OUTER_INSET + BLOCK_TEXT_PADDING
/** The weekday header's height, which the day view takes off to fit the week's hours (#184). */
@Composable
internal fun weekDayHeaderHeight(): Dp {
@@ -216,7 +220,7 @@ fun WeekScreen(
viewModel: WeekViewModel = hiltViewModel(),
) = PagedTimelineScreen(
viewModel = viewModel,
showWeekNumber = true,
kind = TimelineKind.Week,
selectedView = selectedView,
onSelectView = onSelectView,
onOpenDay = onOpenDay,
@@ -247,8 +251,7 @@ fun MultiDayScreen(
viewModel: MultiDayViewModel = hiltViewModel(),
) = PagedTimelineScreen(
viewModel = viewModel,
// A run of days often straddles two weeks, so no single number fits it.
showWeekNumber = false,
kind = TimelineKind.MultiDay,
selectedView = selectedView,
onSelectView = onSelectView,
onOpenDay = onOpenDay,
@@ -262,11 +265,66 @@ fun MultiDayScreen(
modifier = modifier,
)
/** The day view: the week view's timeline one day at a time. */
@Composable
fun DayScreen(
selectedView: CalendarView,
onSelectView: (CalendarView) -> Unit,
onEventClick: (EventInstance) -> Unit,
onOpenSettings: () -> Unit,
onOpenSearch: () -> Unit,
onCreateEvent: (LocalDate, Int?) -> Unit,
quickSwitchViews: List<CalendarView> = IMPLEMENTED_VIEWS,
drawerViewOrder: List<CalendarView> = IMPLEMENTED_VIEWS,
todayInToolbar: Boolean = false,
modifier: Modifier = Modifier,
initialDateIso: String? = null,
viewModel: DayViewModel = hiltViewModel(),
) = PagedTimelineScreen(
viewModel = viewModel,
kind = TimelineKind.Day,
selectedView = selectedView,
onSelectView = onSelectView,
// Tapping the one column's header would only reopen this day.
onOpenDay = {},
onEventClick = onEventClick,
onOpenSettings = onOpenSettings,
onOpenSearch = onOpenSearch,
onCreateEvent = onCreateEvent,
quickSwitchViews = quickSwitchViews,
drawerViewOrder = drawerViewOrder,
todayInToolbar = todayInToolbar,
modifier = modifier,
initialDate = initialDateIso?.let(LocalDate::parse),
)
/** What sets the timeline views apart; everything else they share. */
internal enum class TimelineKind(
val showWeekNumber: Boolean,
val todayText: Int,
/** Where a header cell sits in its column: centred, or at the start like the event titles. */
val headerBias: Float,
) {
Week(showWeekNumber = true, todayText = R.string.week_today_action, headerBias = 0f),
// A run of days often straddles two weeks, so no single number fits it.
MultiDay(showWeekNumber = false, todayText = R.string.week_today_action, headerBias = 0f),
Day(showWeekNumber = false, todayText = R.string.day_today_action, headerBias = -1f),
;
fun title(start: LocalDate, end: LocalDate, locale: Locale, currentYear: Int, abbreviated: Boolean): String =
if (this == Day) {
formatDayTitle(start, locale, currentYear, abbreviated)
} else {
formatWeekTitle(start, end, locale, currentYear, abbreviated)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PagedTimelineScreen(
viewModel: PagedTimelineViewModel,
showWeekNumber: Boolean,
kind: TimelineKind,
selectedView: CalendarView,
onSelectView: (CalendarView) -> Unit,
onOpenDay: (LocalDate) -> Unit,
@@ -278,7 +336,9 @@ private fun PagedTimelineScreen(
drawerViewOrder: List<CalendarView>,
todayInToolbar: Boolean,
modifier: Modifier,
initialDate: LocalDate? = null,
) {
LaunchedEffect(initialDate) { initialDate?.let(viewModel::jumpTo) }
val state by viewModel.state.collectAsStateWithLifecycle()
val position by viewModel.position.collectAsStateWithLifecycle()
val grid = position.grid
@@ -368,6 +428,7 @@ private fun PagedTimelineScreen(
modifier = modifier,
topBar = {
WeekTopBar(
kind = kind,
weekStart = weekStart,
weekEnd = weekEnd,
currentYear = today.year,
@@ -384,7 +445,7 @@ private fun PagedTimelineScreen(
floatingActionButton = {
CalendarFabColumn(
todayVisible = !isOnCurrentWeek && !todayInToolbar,
todayText = stringResource(R.string.week_today_action),
todayText = stringResource(kind.todayText),
onToday = jumpToToday,
onCreate = {
// Anchor on today when it's in view, else the week's first day.
@@ -398,7 +459,7 @@ private fun PagedTimelineScreen(
state = state,
pagerState = pagerState,
grid = grid,
showWeekNumber = showWeekNumber,
kind = kind,
span = viewModel::span,
onRetry = viewModel::goToToday,
onEventClick = onEventClick,
@@ -418,7 +479,7 @@ private fun WeekContent(
state: WeekUiState,
pagerState: PagerState,
grid: PageGrid,
showWeekNumber: Boolean,
kind: TimelineKind,
span: (DaySpan) -> StateFlow<WeekUiState>,
onRetry: () -> Unit,
onEventClick: (EventInstance) -> Unit,
@@ -466,7 +527,7 @@ private fun WeekContent(
is WeekUiState.Success -> WeekPager(
pagerState = pagerState,
grid = grid,
showWeekNumber = showWeekNumber,
kind = kind,
span = span,
today = s.today,
initialStripHeight = s.allDayStripHeight(),
@@ -491,7 +552,7 @@ private fun WeekContent(
private fun WeekPager(
pagerState: PagerState,
grid: PageGrid,
showWeekNumber: Boolean,
kind: TimelineKind,
span: (DaySpan) -> StateFlow<WeekUiState>,
today: LocalDate,
initialStripHeight: Dp,
@@ -506,7 +567,7 @@ private fun WeekPager(
val allDayHeight = pagedHeight(pagerState, stripHeights, initialStripHeight)
WeekFrame(
weekStart = grid.startForPage(pagerState.currentPage),
showWeekNumber = showWeekNumber,
showWeekNumber = kind.showWeekNumber,
allDayHeight = allDayHeight,
scrollState = scrollState,
dragController = dragController,
@@ -537,6 +598,7 @@ private fun WeekPager(
ViewMorphPage(active = morphPageActive(page, pagerState.settledPage)) {
WeekPage(
state = loaded ?: empty,
headerBias = kind.headerBias,
allDayHeight = allDayHeight,
scrollState = scrollState,
dragController = dragController,
@@ -610,6 +672,7 @@ private fun WeekFrame(
internal fun WeekSuccess(
state: WeekUiState.Success,
showWeekNumber: Boolean = true,
headerBias: Float = 0f,
scrollState: ScrollState,
allDayHeight: Dp,
dragController: TimelineDragController,
@@ -627,6 +690,7 @@ internal fun WeekSuccess(
) {
WeekPage(
state = state,
headerBias = headerBias,
allDayHeight = allDayHeight,
scrollState = scrollState,
dragController = dragController,
@@ -648,6 +712,7 @@ internal fun WeekSuccess(
@Composable
private fun WeekPage(
state: WeekUiState.Success,
headerBias: Float,
allDayHeight: Dp,
scrollState: ScrollState,
dragController: TimelineDragController,
@@ -663,7 +728,7 @@ private fun WeekPage(
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface),
) {
WeekDayHeader(days = state.days, today = state.today, onOpenDay = onOpenDay)
WeekDayHeader(days = state.days, today = state.today, bias = headerBias, onOpenDay = onOpenDay)
// This strip is painted on `surface`, so an outlined chip has to
// fill with that and not with the columns' container (#230).
CompositionLocalProvider(
@@ -689,6 +754,7 @@ private fun WeekPage(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun WeekTopBar(
kind: TimelineKind,
weekStart: LocalDate,
weekEnd: LocalDate,
currentYear: Int,
@@ -702,9 +768,9 @@ private fun WeekTopBar(
onToday: () -> Unit,
) {
val locale = currentLocale()
val (title, shortTitle) = remember(weekStart, weekEnd, locale, currentYear) {
formatWeekTitle(weekStart, weekEnd, locale, currentYear) to
formatWeekTitle(weekStart, weekEnd, locale, currentYear, abbreviated = true)
val (title, shortTitle) = remember(kind, weekStart, weekEnd, locale, currentYear) {
kind.title(weekStart, weekEnd, locale, currentYear, abbreviated = false) to
kind.title(weekStart, weekEnd, locale, currentYear, abbreviated = true)
}
TopAppBar(
title = {
@@ -751,6 +817,7 @@ private fun WeekTopBar(
private fun WeekDayHeader(
days: List<LocalDate>,
today: LocalDate,
bias: Float,
onOpenDay: (LocalDate) -> Unit,
) {
val locale = currentLocale()
@@ -766,8 +833,9 @@ private fun WeekDayHeader(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(12.dp))
.clickable { onOpenDay(date) },
horizontalAlignment = Alignment.CenterHorizontally,
.clickable { onOpenDay(date) }
.padding(start = HEADER_START_INSET * -bias),
horizontalAlignment = BiasAlignment.Horizontal(bias),
) {
// One line, as the gutter cell beside it assumes (see WeekNumberCell).
Text(
@@ -1384,3 +1452,17 @@ private fun formatWeekTitle(
forceYear = weekEnd.year != weekStart.year,
)
}
/** The day's title: its weekday and date, abbreviated to the date alone when short of room. */
private fun formatDayTitle(
date: LocalDate,
locale: Locale,
currentYear: Int,
abbreviated: Boolean = false,
): String =
formatCalendarTitle(
date = java.time.LocalDate.of(date.year, date.month.ordinal + 1, date.day),
locale = locale,
currentYear = currentYear,
skeleton = if (abbreviated) "dMMM" else "EEEdMMM",
)
@@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -208,6 +209,20 @@ class MultiDayViewModel @Inject constructor(
initialRule = { DaysGrid.startingAt(it, SettingsPrefs.DEFAULT_MULTI_DAY_COUNT) },
)
/** The day view: the timeline one day at a time. */
@HiltViewModel
class DayViewModel @Inject constructor(
repository: CalendarRepository,
settingsPrefs: SettingsPrefs,
@IoDispatcher io: CoroutineDispatcher,
) : PagedTimelineViewModel(
repository = repository,
settingsPrefs = settingsPrefs,
io = io,
gridRule = flowOf { _: LocalDate -> DayGrid },
initialRule = { DayGrid },
)
/**
* Lay out all-day events as connected horizontal spans across the visible week.
* Each event becomes one [AllDaySpan] from its first to its last covered column;
@@ -50,6 +50,7 @@ internal fun WeekViewPreview(
WeekSuccess(
state = state,
showWeekNumber = dayCount == 7,
headerBias = if (dayCount == 1) TimelineKind.Day.headerBias else 0f,
scrollState = scrollState,
allDayHeight = state.allDayStripHeight(),
dragController = rememberTimelineDragController(),
@@ -1,22 +0,0 @@
package de.jeanlucmakiola.calendula.ui.day
import com.google.common.truth.Truth.assertThat
import kotlinx.datetime.LocalDate
import org.junit.jupiter.api.Test
class DayPagingTest {
@Test
fun `a page and its day round-trip`() {
listOf(LocalDate(2026, 9, 27), LocalDate(1990, 5, 16), LocalDate(2100, 2, 28)).forEach { day ->
assertThat(dayForPage(dayPageFor(day))).isEqualTo(day)
}
}
@Test
fun `adjacent pages are adjacent days across a month end`() {
val page = dayPageFor(LocalDate(2026, 2, 28))
assertThat(dayForPage(page + 1)).isEqualTo(LocalDate(2026, 3, 1))
assertThat(dayForPage(page - 1)).isEqualTo(LocalDate(2026, 2, 27))
}
}
@@ -64,4 +64,18 @@ class WeekPagingTest {
assertThat(grid.startForPage(page)).isEqualTo(date)
assertThat(grid.startForPage(page - 1)).isEqualTo(LocalDate(1990, 5, 13))
}
@Test
fun `a day page and its day round-trip`() {
listOf(LocalDate(2026, 9, 27), LocalDate(1990, 5, 16), LocalDate(2100, 2, 28)).forEach { day ->
assertThat(DayGrid.startForPage(DayGrid.pageFor(day))).isEqualTo(day)
}
}
@Test
fun `adjacent day pages are adjacent days across a month end`() {
val page = DayGrid.pageFor(LocalDate(2026, 2, 28))
assertThat(DayGrid.startForPage(page + 1)).isEqualTo(LocalDate(2026, 3, 1))
assertThat(DayGrid.startForPage(page - 1)).isEqualTo(LocalDate(2026, 2, 27))
}
}