From eab37f83bdb22b20df422999b0410fb033d9bf73 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 20 Jul 2024 09:34:48 +0200 Subject: [PATCH 01/48] Add year calendar composable --- .../kizitonwose/calendar/compose/Calendar.kt | 193 +++++++++++ .../calendar/compose/CalendarMonths.kt | 2 +- .../calendar/compose/CalendarState.kt | 4 +- .../heatmapcalendar/HeatMapCalendarState.kt | 4 +- .../yearcalendar/YearCalendarLayoutInfo.kt | 37 +++ .../yearcalendar/YearCalendarMonths.kt | 193 +++++++++++ .../compose/yearcalendar/YearCalendarState.kt | 301 ++++++++++++++++++ .../yearcalendar/YearContentHeightMode.kt | 30 ++ .../calendar/core/CalendarMonth.kt | 5 +- .../kizitonwose/calendar/core/CalendarYear.kt | 45 +++ .../calendar/core/ExperimentalCalendarApi.kt | 9 + .../com/kizitonwose/calendar/data/Utils.kt | 15 +- .../com/kizitonwose/calendar/data/YearData.kt | 35 ++ .../calendar/sample/compose/Example1Page.kt | 147 ++++++--- .../calendar/sample/compose/Example4Page.kt | 2 +- .../calendar/sample/shared/Utils.kt | 5 +- .../kizitonwose/calendar/view/CalendarView.kt | 6 +- .../calendar/view/WeekCalendarView.kt | 6 +- 18 files changed, 958 insertions(+), 81 deletions(-) create mode 100644 compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt create mode 100644 compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt create mode 100644 compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt create mode 100644 compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt create mode 100644 core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt create mode 100644 core/src/main/java/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt create mode 100644 data/src/main/java/com/kizitonwose/calendar/data/YearData.kt diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt index 9d5bee28..a575d535 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt @@ -1,5 +1,6 @@ package com.kizitonwose.calendar.compose +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -7,6 +8,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior @@ -18,8 +20,14 @@ import com.kizitonwose.calendar.compose.heatmapcalendar.rememberHeatMapCalendarS import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarImpl import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarMonths +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay import java.time.DayOfWeek @@ -293,3 +301,188 @@ public fun HeatMapCalendar( monthHeader = monthHeader, contentPadding = contentPadding, ) + +@ExperimentalCalendarApi +@Composable +public fun HorizontalYearCalendar( + modifier: Modifier = Modifier, + state: YearCalendarState = rememberYearCalendarState(), + columns: Int = 3, + calendarScrollPaged: Boolean = true, + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + contentPadding: PaddingValues = PaddingValues(0.dp), + contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, + monthVerticalArrangement: Arrangement.Vertical = Arrangement.Top, + monthHorizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + yearBodyContentPadding: PaddingValues = PaddingValues(0.dp), + isMonthVisible: (month: CalendarMonth) -> Boolean = remember { { true } }, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)? = null, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)? = null, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)? = null, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)? = null, +): Unit = YearCalendar( + modifier = modifier, + state = state, + columns = columns, + calendarScrollPaged = calendarScrollPaged, + userScrollEnabled = userScrollEnabled, + isHorizontal = true, + reverseLayout = reverseLayout, + contentHeightMode = contentHeightMode, + isMonthVisible = isMonthVisible, + monthVerticalArrangement = monthVerticalArrangement, + monthHorizontalArrangement = monthHorizontalArrangement, + yearBodyContentPadding = yearBodyContentPadding, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, + contentPadding = contentPadding, +) + +@ExperimentalCalendarApi +@Composable +public fun VerticalYearCalendar( + modifier: Modifier = Modifier, + state: YearCalendarState = rememberYearCalendarState(), + columns: Int = 3, + calendarScrollPaged: Boolean = true, + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + contentPadding: PaddingValues = PaddingValues(0.dp), + contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, + monthVerticalArrangement: Arrangement.Vertical = Arrangement.Top, + monthHorizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + yearBodyContentPadding: PaddingValues = PaddingValues(0.dp), + isMonthVisible: (month: CalendarMonth) -> Boolean = remember { { true } }, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)? = null, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)? = null, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)? = null, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)? = null, +): Unit = YearCalendar( + modifier = modifier, + state = state, + columns = columns, + calendarScrollPaged = calendarScrollPaged, + userScrollEnabled = userScrollEnabled, + isHorizontal = false, + reverseLayout = reverseLayout, + contentHeightMode = contentHeightMode, + monthVerticalArrangement = monthVerticalArrangement, + monthHorizontalArrangement = monthHorizontalArrangement, + yearBodyContentPadding = yearBodyContentPadding, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, + contentPadding = contentPadding, +) + +@ExperimentalCalendarApi +@Composable +private fun YearCalendar( + modifier: Modifier, + state: YearCalendarState, + columns: Int, + calendarScrollPaged: Boolean, + userScrollEnabled: Boolean, + isHorizontal: Boolean, + reverseLayout: Boolean, + contentPadding: PaddingValues, + contentHeightMode: YearContentHeightMode, + monthVerticalArrangement: Arrangement.Vertical, + monthHorizontalArrangement: Arrangement.Horizontal, + yearBodyContentPadding: PaddingValues, + isMonthVisible: (month: CalendarMonth) -> Boolean, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)?, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)?, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)?, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)?, +) { + if (isHorizontal) { + LazyRow( + modifier = modifier, + state = state.listState, + flingBehavior = flingBehavior(calendarScrollPaged, state.listState), + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + ) { + YearCalendarMonths( + yearCount = state.calendarInfo.indexCount, + yearData = { offset -> state.store[offset] }, + columns = columns, + contentHeightMode = contentHeightMode, + monthVerticalArrangement = monthVerticalArrangement, + monthHorizontalArrangement = monthHorizontalArrangement, + yearBodyContentPadding = yearBodyContentPadding, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, + ) + } + } else { + LazyColumn( + modifier = modifier, + state = state.listState, + flingBehavior = flingBehavior(calendarScrollPaged, state.listState), + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + ) { + YearCalendarMonths( + yearCount = state.calendarInfo.indexCount, + yearData = { offset -> state.store[offset] }, + columns = columns, + contentHeightMode = contentHeightMode, + monthVerticalArrangement = monthVerticalArrangement, + monthHorizontalArrangement = monthHorizontalArrangement, + yearBodyContentPadding = yearBodyContentPadding, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, + ) + } + } +} diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt index ea2aafcc..eeccedb8 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarMonths.kt @@ -87,4 +87,4 @@ private val defaultMonthContainer: (@Composable LazyItemScope.(CalendarMonth, co private val defaultMonthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit) = { _, content -> content() } -private fun T?.or(default: T) = this ?: default +internal fun T?.or(default: T) = this ?: default diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt index 0f5e901b..d7b554ef 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt @@ -20,7 +20,7 @@ import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.OutDateStyle import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.data.DataStore -import com.kizitonwose.calendar.data.checkDateRange +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.data.getCalendarMonthData import com.kizitonwose.calendar.data.getMonthIndex import com.kizitonwose.calendar.data.getMonthIndicesCount @@ -207,7 +207,7 @@ public class CalendarState internal constructor( private fun monthDataChanged() { store.clear() - checkDateRange(startMonth, endMonth) + checkRange(startMonth, endMonth) // Read the firstDayOfWeek and outDateStyle properties to ensure recomposition // even though they are unused in the CalendarInfo. Alternatively, we could use // mutableStateMapOf() as the backing store for DataStore() to ensure recomposition diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt index bb13151e..0650ae40 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt @@ -21,7 +21,7 @@ import com.kizitonwose.calendar.compose.VisibleItemState import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.data.DataStore -import com.kizitonwose.calendar.data.checkDateRange +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.data.getHeatMapCalendarMonthData import com.kizitonwose.calendar.data.getMonthIndex import com.kizitonwose.calendar.data.getMonthIndicesCount @@ -186,7 +186,7 @@ public class HeatMapCalendarState internal constructor( private fun monthDataChanged() { store.clear() - checkDateRange(startMonth, endMonth) + checkRange(startMonth, endMonth) calendarInfo = CalendarInfo( indexCount = getMonthIndicesCount(startMonth, endMonth), firstDayOfWeek = firstDayOfWeek, diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt new file mode 100644 index 00000000..d8d20595 --- /dev/null +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt @@ -0,0 +1,37 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import com.kizitonwose.calendar.core.CalendarYear + +/** + * Contains useful information about the currently displayed layout state of the calendar. + * For example you can get the list of currently displayed months. + * + * Use [YearCalendarState.layoutInfo] to retrieve this. + * + * @see LazyListLayoutInfo + */ +public class YearCalendarLayoutInfo( + info: LazyListLayoutInfo, + private val getIndexData: (Int) -> CalendarYear, +) : LazyListLayoutInfo by info { + /** + * The list of [YearCalendarItemInfo] representing all the currently visible weeks. + */ + public val visibleWeeksInfo: List + get() = visibleItemsInfo.map { info -> + YearCalendarItemInfo(info, getIndexData(info.index)) + } +} + +/** + * Contains useful information about an individual week on the calendar. + * + * @param week The week in the list. + + * @see YearCalendarLayoutInfo + * @see LazyListItemInfo + */ +public class YearCalendarItemInfo(info: LazyListItemInfo, public val week: CalendarYear) : + LazyListItemInfo by info diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt new file mode 100644 index 00000000..ddb30ead --- /dev/null +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt @@ -0,0 +1,193 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import com.kizitonwose.calendar.compose.or +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.CalendarYear + +@Suppress("FunctionName") +internal fun LazyListScope.YearCalendarMonths( + yearCount: Int, + yearData: (offset: Int) -> CalendarYear, + columns: Int, + contentHeightMode: YearContentHeightMode, + monthVerticalArrangement: Arrangement.Vertical, + monthHorizontalArrangement: Arrangement.Horizontal, + yearBodyContentPadding: PaddingValues, + isMonthVisible: (month: CalendarMonth) -> Boolean, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)?, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)?, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)?, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)?, +) { + items( + count = yearCount, + key = { offset -> yearData(offset).year }, + ) { yearOffset -> + val year = yearData(yearOffset) + val fillHeight = when (contentHeightMode) { + YearContentHeightMode.Wrap -> false + YearContentHeightMode.Fill, + YearContentHeightMode.Stretch, + -> true + } + val hasYearContainer = yearContainer != null + yearContainer.or(defaultYearContainer)(year) { + Column( + modifier = Modifier + .then(if (hasYearContainer) Modifier.fillMaxWidth() else Modifier.fillParentMaxWidth()) + .then( + if (fillHeight) { + if (hasYearContainer) Modifier.fillMaxHeight() else Modifier.fillParentMaxHeight() + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + val months = year.months.filter(isMonthVisible) + yearHeader?.invoke(this, year) + yearBody.or(defaultYearBody)(year) { + CalendarGrid( + modifier = Modifier + .fillMaxWidth() + .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()) + .padding(yearBodyContentPadding), + columns = columns, + itemCount = months.count(), + fillHeight = fillHeight, + monthVerticalArrangement = monthVerticalArrangement, + monthHorizontalArrangement = monthHorizontalArrangement, + ) { monthOffset -> + val month = months[monthOffset] + val hasContainer = monthContainer != null + monthContainer.or(defaultMonthContainer)(month) { + Column( + modifier = Modifier + .then(if (hasContainer) Modifier.fillMaxWidth() else Modifier) + .then( + if (fillHeight) { + if (hasContainer) Modifier.fillMaxHeight() else Modifier + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + monthHeader?.invoke(this, month) + monthBody.or(defaultMonthBody)(month) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), + ) { + for (week in month.weekDays) { + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (contentHeightMode == YearContentHeightMode.Stretch) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for (day in week) { + Box( + modifier = Modifier + .weight(1f) + .clipToBounds(), + ) { + dayContent(day) + } + } + } + } + } + } + monthFooter?.invoke(this, month) + } + } + } + } + yearFooter?.invoke(this, year) + } + } + } +} + +@Composable +private fun CalendarGrid( + columns: Int, + fillHeight: Boolean, + monthVerticalArrangement: Arrangement.Vertical, + monthHorizontalArrangement: Arrangement.Horizontal, + itemCount: Int, + modifier: Modifier = Modifier, + content: @Composable BoxScope.(Int) -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = monthVerticalArrangement, + ) { + var rows = (itemCount / columns) + if (itemCount.mod(columns) > 0) { + rows += 1 + } + + for (rowId in 0 until rows) { + val firstIndex = rowId * columns + + Row( + modifier = Modifier.then( + if (fillHeight) Modifier.weight(1f) else Modifier, + ), + horizontalArrangement = monthHorizontalArrangement, + ) { + for (columnId in 0 until columns) { + val index = firstIndex + columnId + Box( + modifier = Modifier + .weight(1f), + ) { + if (index < itemCount) { + content(index) + } + } + } + } + } + } +} + +private val defaultYearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit) = + { _, container -> container() } + +private val defaultYearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit) = + { _, content -> content() } + +private val defaultMonthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit) = + { _, container -> container() } + +private val defaultMonthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit) = + { _, content -> content() } diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt new file mode 100644 index 00000000..b59f6cc3 --- /dev/null +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -0,0 +1,301 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import android.util.Log +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.kizitonwose.calendar.compose.CalendarInfo +import com.kizitonwose.calendar.compose.CalendarLayoutInfo +import com.kizitonwose.calendar.compose.CalendarState +import com.kizitonwose.calendar.compose.VisibleItemState +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.ExperimentalCalendarApi +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.data.DataStore +import com.kizitonwose.calendar.data.checkRange +import com.kizitonwose.calendar.data.getCalendarYearData +import com.kizitonwose.calendar.data.getYearIndex +import com.kizitonwose.calendar.data.getYearIndicesCount +import java.time.DayOfWeek +import java.time.Year + +/** + * Creates a [CalendarState] that is remembered across compositions. + * + * @param startMonth the initial value for [CalendarState.startMonth] + * @param endMonth the initial value for [CalendarState.endMonth] + * @param firstDayOfWeek the initial value for [CalendarState.firstDayOfWeek] + * @param firstVisibleMonth the initial value for [CalendarState.firstVisibleMonth] + * @param outDateStyle the initial value for [CalendarState.outDateStyle] + */ +@ExperimentalCalendarApi +@Composable +public fun rememberYearCalendarState( + startMonth: Year = Year.now(), + endMonth: Year = startMonth, + firstVisibleMonth: Year = startMonth, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + outDateStyle: OutDateStyle = OutDateStyle.EndOfRow, +): YearCalendarState { + return rememberSaveable( + inputs = arrayOf( + startMonth, + endMonth, + firstVisibleMonth, + firstDayOfWeek, + outDateStyle, + ), + saver = YearCalendarState.Saver, + ) { + YearCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstDayOfWeek = firstDayOfWeek, + firstVisibleMonth = firstVisibleMonth, + outDateStyle = outDateStyle, + visibleItemState = null, + ) + } +} + +/** + * A state object that can be hoisted to control and observe calendar properties. + * + * This should be created via [rememberCalendarState]. + * + * @param startMonth the first month on the calendar. + * @param endMonth the last month on the calendar. + * @param firstDayOfWeek the first day of week on the calendar. + * @param firstVisibleMonth the initial value for [CalendarState.firstVisibleMonth] + * @param outDateStyle the preferred style for out date generation. + */ +@ExperimentalCalendarApi +@Stable +public class YearCalendarState internal constructor( + startMonth: Year, + endMonth: Year, + firstDayOfWeek: DayOfWeek, + firstVisibleMonth: Year, + outDateStyle: OutDateStyle, + visibleItemState: VisibleItemState?, +) : ScrollableState { + /** Backing state for [startMonth] */ + private var _startMonth by mutableStateOf(startMonth) + + /** The first month on the calendar. */ + public var startMonth: Year + get() = _startMonth + set(value) { + if (value != startMonth) { + _startMonth = value + monthDataChanged() + } + } + + /** Backing state for [endMonth] */ + private var _endMonth by mutableStateOf(endMonth) + + /** The last month on the calendar. */ + public var endMonth: Year + get() = _endMonth + set(value) { + if (value != endMonth) { + _endMonth = value + monthDataChanged() + } + } + + /** Backing state for [firstDayOfWeek] */ + private var _firstDayOfWeek by mutableStateOf(firstDayOfWeek) + + /** The first day of week on the calendar. */ + public var firstDayOfWeek: DayOfWeek + get() = _firstDayOfWeek + set(value) { + if (value != firstDayOfWeek) { + _firstDayOfWeek = value + monthDataChanged() + } + } + + /** Backing state for [outDateStyle] */ + private var _outDateStyle by mutableStateOf(outDateStyle) + + /** The preferred style for out date generation. */ + public var outDateStyle: OutDateStyle + get() = _outDateStyle + set(value) { + if (value != outDateStyle) { + _outDateStyle = value + monthDataChanged() + } + } + + /** + * The first month that is visible. + * + * @see [lastVisibleMonth] + */ + public val firstVisibleMonth: CalendarYear by derivedStateOf { + store[listState.firstVisibleItemIndex] + } + + /** + * The last month that is visible. + * + * @see [firstVisibleMonth] + */ + public val lastVisibleMonth: CalendarYear by derivedStateOf { + store[listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0] + } + + /** + * The object of [CalendarLayoutInfo] calculated during the last layout pass. For example, + * you can use it to calculate what items are currently visible. + * + * Note that this property is observable and is updated after every scroll or remeasure. + * If you use it in the composable function it will be recomposed on every change causing + * potential performance issues including infinity recomposition loop. + * Therefore, avoid using it in the composition. + * + * If you need to use it in the composition then consider wrapping the calculation into a + * derived state in order to only have recompositions when the derived value changes. + * See Example6Page in the sample app for usage. + * + * If you want to run some side effects like sending an analytics event or updating a state + * based on this value consider using "snapshotFlow". + * + * see [LazyListLayoutInfo] + */ + public val layoutInfo: YearCalendarLayoutInfo + get() = YearCalendarLayoutInfo(listState.layoutInfo) { index -> store[index] } + + /** + * [InteractionSource] that will be used to dispatch drag events when this + * calendar is being dragged. If you want to know whether the fling (or animated scroll) is in + * progress, use [isScrollInProgress]. + */ + public val interactionSource: InteractionSource + get() = listState.interactionSource + + internal val listState = LazyListState( + firstVisibleItemIndex = visibleItemState?.firstVisibleItemIndex + ?: getScrollIndex(firstVisibleMonth) ?: 0, + firstVisibleItemScrollOffset = visibleItemState?.firstVisibleItemScrollOffset ?: 0, + ) + + internal var calendarInfo by mutableStateOf(CalendarInfo(indexCount = 0)) + + internal val store = DataStore { offset -> + getCalendarYearData( + startYear = this.startMonth, + offset = offset, + firstDayOfWeek = this.firstDayOfWeek, + outDateStyle = this.outDateStyle, + ) + } + + init { + monthDataChanged() // Update monthIndexCount initially. + } + + private fun monthDataChanged() { + store.clear() + checkRange(startMonth, endMonth) + // Read the firstDayOfWeek and outDateStyle properties to ensure recomposition + // even though they are unused in the CalendarInfo. Alternatively, we could use + // mutableStateMapOf() as the backing store for DataStore() to ensure recomposition + // but not sure how compose handles recomposition of a lazy list that reads from + // such map when an entry unrelated to the visible indices changes. + calendarInfo = CalendarInfo( + indexCount = getYearIndicesCount(startMonth, endMonth), + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ) + } + + /** + * Instantly brings the [month] to the top of the viewport. + * + * @param month the month to which to scroll. Must be within the + * range of [startMonth] and [endMonth]. + * + * @see [animateScrollToMonth] + */ + public suspend fun scrollToMonth(month: Year) { + listState.scrollToItem(getScrollIndex(month) ?: return) + } + + /** + * Animate (smooth scroll) to the given [month]. + * + * @param month the month to which to scroll. Must be within the + * range of [startMonth] and [endMonth]. + */ + public suspend fun animateScrollToMonth(month: Year) { + listState.animateScrollToItem(getScrollIndex(month) ?: return) + } + + private fun getScrollIndex(month: Year): Int? { + if (month !in startMonth..endMonth) { + Log.d("CalendarState", "Attempting to scroll out of range: $month") + return null + } + return getYearIndex(startMonth, month) + } + + /** + * Whether this [ScrollableState] is currently scrolling by gesture, fling or programmatically. + */ + override val isScrollInProgress: Boolean + get() = listState.isScrollInProgress + + override fun dispatchRawDelta(delta: Float): Float = listState.dispatchRawDelta(delta) + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ): Unit = listState.scroll(scrollPriority, block) + + public companion object { + internal val Saver: Saver = listSaver( + save = { + listOf( + it.startMonth, + it.endMonth, + it.firstVisibleMonth.year, + it.firstDayOfWeek, + it.outDateStyle, + it.listState.firstVisibleItemIndex, + it.listState.firstVisibleItemScrollOffset, + ) + }, + restore = { + YearCalendarState( + startMonth = it[0] as Year, + endMonth = it[1] as Year, + firstVisibleMonth = it[2] as Year, + firstDayOfWeek = it[3] as DayOfWeek, + outDateStyle = it[4] as OutDateStyle, + visibleItemState = VisibleItemState( + firstVisibleItemIndex = it[5] as Int, + firstVisibleItemScrollOffset = it[6] as Int, + ), + ) + }, + ) + } +} diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt new file mode 100644 index 00000000..25b5bcc5 --- /dev/null +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt @@ -0,0 +1,30 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.ui.Modifier + +/** + * Determines how the height of the day content is calculated. + */ +public enum class YearContentHeightMode { + /** + * The day container will wrap its height. This allows you to + * use [Modifier.aspectRatio] if you want square day content + * or [Modifier.height] if you want a specific height value + * for the day content. + */ + Wrap, + + /** + * The days in each month will spread to fill the parent's height after + * any available header and footer content height has been accounted for. + * This allows you to use [Modifier.fillMaxHeight] for the day content + * height. With this option, your Calendar composable should also + * be created with [Modifier.fillMaxHeight] or [Modifier.height]. + */ + Fill, + + Stretch, +} diff --git a/core/src/main/java/com/kizitonwose/calendar/core/CalendarMonth.kt b/core/src/main/java/com/kizitonwose/calendar/core/CalendarMonth.kt index 62511279..4f476729 100644 --- a/core/src/main/java/com/kizitonwose/calendar/core/CalendarMonth.kt +++ b/core/src/main/java/com/kizitonwose/calendar/core/CalendarMonth.kt @@ -37,8 +37,9 @@ public data class CalendarMonth( override fun toString(): String { return "CalendarMonth { " + - "first = ${weekDays.first().first()}, " + - "last = ${weekDays.last().last()} " + + "yearMonth = $yearMonth, " + + "firstDay = ${weekDays.first().first()}, " + + "lastDay = ${weekDays.last().last()} " + "} " } } diff --git a/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt b/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt new file mode 100644 index 00000000..d902a8d9 --- /dev/null +++ b/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt @@ -0,0 +1,45 @@ +package com.kizitonwose.calendar.core + +import androidx.compose.runtime.Immutable +import java.io.Serializable +import java.time.Year + +/** + * Represents a month on the calendar. + * + * @param yearMonth the calendar month value. + * @param weekDays the weeks in this month. + */ +@Immutable +public data class CalendarYear( + val year: Year, + val months: List, +) : Serializable { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CalendarYear + + if (year != other.year) return false + if (months.first() != other.months.first()) return false + if (months.last() != other.months.last()) return false + + return true + } + + override fun hashCode(): Int { + var result = year.hashCode() + result = 31 * result + months.first().hashCode() + result = 31 * result + months.last().hashCode() + return result + } + + override fun toString(): String { + return "CalendarYear { " + + "year = $year, " + + "firstMonth = ${months.first()}, " + + "lastMonth = ${months.last()} " + + "} " + } +} diff --git a/core/src/main/java/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt b/core/src/main/java/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt new file mode 100644 index 00000000..df02b05a --- /dev/null +++ b/core/src/main/java/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt @@ -0,0 +1,9 @@ +package com.kizitonwose.calendar.core + +@RequiresOptIn( + message = "This calendar API is experimental and is " + + "likely to change or to be removed in the future.", + level = RequiresOptIn.Level.ERROR, +) +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalCalendarApi diff --git a/data/src/main/java/com/kizitonwose/calendar/data/Utils.kt b/data/src/main/java/com/kizitonwose/calendar/data/Utils.kt index ab9cfc8f..7b08c82b 100644 --- a/data/src/main/java/com/kizitonwose/calendar/data/Utils.kt +++ b/data/src/main/java/com/kizitonwose/calendar/data/Utils.kt @@ -1,16 +1,7 @@ package com.kizitonwose.calendar.data -import java.time.LocalDate -import java.time.YearMonth - -public fun checkDateRange(startMonth: YearMonth, endMonth: YearMonth) { - check(endMonth >= startMonth) { - "startMonth: $startMonth is greater than endMonth: $endMonth" - } -} - -public fun checkDateRange(startDate: LocalDate, endDate: LocalDate) { - check(endDate >= startDate) { - "startDate: $startDate is greater than endDate: $endDate" +public fun > checkRange(start: T, end: T) { + check(end >= start) { + "start: $start is greater than end: $end" } } diff --git a/data/src/main/java/com/kizitonwose/calendar/data/YearData.kt b/data/src/main/java/com/kizitonwose/calendar/data/YearData.kt new file mode 100644 index 00000000..13096349 --- /dev/null +++ b/data/src/main/java/com/kizitonwose/calendar/data/YearData.kt @@ -0,0 +1,35 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.OutDateStyle +import java.time.DayOfWeek +import java.time.Month +import java.time.Year +import java.time.temporal.ChronoUnit + +public fun getCalendarYearData( + startYear: Year, + offset: Int, + firstDayOfWeek: DayOfWeek, + outDateStyle: OutDateStyle, +): CalendarYear { + val year = startYear.plusYears(offset.toLong()) + val months = List(Month.entries.size) { index -> + getCalendarMonthData( + startMonth = year.atMonth(Month.JANUARY), + offset = index, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ).calendarMonth + } + return CalendarYear(year, months) +} + +public fun getYearIndex(startYear: Year, targetYear: Year): Int { + return ChronoUnit.YEARS.between(startYear, targetYear).toInt() +} + +public fun getYearIndicesCount(startYear: Year, endYear: Year): Int { + // Add one to include the start year itself! + return getYearIndex(startYear, endYear) + 1 +} diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt index 89d20a9d..5d649f1e 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt @@ -1,13 +1,16 @@ package com.kizitonwose.calendar.sample.compose import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -22,27 +25,30 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.kizitonwose.calendar.compose.HorizontalCalendar -import com.kizitonwose.calendar.compose.rememberCalendarState +import com.kizitonwose.calendar.compose.VerticalYearCalendar +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.daysOfWeek -import com.kizitonwose.calendar.core.nextMonth -import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.sample.R import com.kizitonwose.calendar.sample.shared.displayText -import kotlinx.coroutines.launch -import java.time.DayOfWeek +import java.time.Year import java.time.YearMonth +@OptIn(ExperimentalCalendarApi::class) @Composable -fun Example1Page(adjacentMonths: Long = 500) { +fun Example1Page(adjacentMonths: Long = 20) { val currentMonth = remember { YearMonth.now() } - val startMonth = remember { currentMonth.minusMonths(adjacentMonths) } - val endMonth = remember { currentMonth.plusMonths(adjacentMonths) } + val currentYear = remember { Year.of(currentMonth.year) } + val startYear = remember { currentYear } + val endYear = remember { currentYear.plusYears(adjacentMonths) } val selections = remember { mutableStateListOf() } val daysOfWeek = remember { daysOfWeek() } Column( @@ -50,30 +56,32 @@ fun Example1Page(adjacentMonths: Long = 500) { .fillMaxSize() .background(Color.White), ) { - val state = rememberCalendarState( - startMonth = startMonth, - endMonth = endMonth, - firstVisibleMonth = currentMonth, + val state = rememberYearCalendarState( + startMonth = currentYear, + endMonth = endYear, + firstVisibleMonth = currentYear, firstDayOfWeek = daysOfWeek.first(), ) val coroutineScope = rememberCoroutineScope() - val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f) - SimpleCalendarTitle( - modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), - currentMonth = visibleMonth.yearMonth, - goToPrevious = { - coroutineScope.launch { - state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) - } - }, - goToNext = { - coroutineScope.launch { - state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth) - } - }, - ) - HorizontalCalendar( - modifier = Modifier.testTag("Calendar"), +// val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f) +// SimpleCalendarTitle( +// modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), +// currentMonth = visibleMonth.yearMonth, +// goToPrevious = { +// coroutineScope.launch { +// state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) +// } +// }, +// goToNext = { +// coroutineScope.launch { +// state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth) +// } +// }, +// ) + VerticalYearCalendar( + modifier = Modifier + .fillMaxSize() + .testTag("Calendar"), state = state, dayContent = { day -> Day(day, isSelected = selections.contains(day)) { clicked -> @@ -84,37 +92,73 @@ fun Example1Page(adjacentMonths: Long = 500) { } } }, + contentHeightMode = YearContentHeightMode.Wrap, + monthVerticalArrangement = Arrangement.spacedBy(20.dp), + monthHorizontalArrangement = Arrangement.spacedBy(20.dp), + yearBodyContentPadding = PaddingValues(horizontal = 20.dp), + isMonthVisible = { + it.yearMonth >= currentMonth + }, + yearHeader = { + YearHeader(it.year) + }, monthHeader = { - MonthHeader(daysOfWeek = daysOfWeek) + MonthHeader(it) }, ) } } @Composable -private fun MonthHeader(daysOfWeek: List) { - Row( +private fun MonthHeader(calendarMonth: CalendarMonth) { + val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } + Column( modifier = Modifier - .fillMaxWidth() - .testTag("MonthHeader"), + .wrapContentHeight() + .padding(top = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), ) { - for (dayOfWeek in daysOfWeek) { - Text( - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - fontSize = 15.sp, - text = dayOfWeek.displayText(), - fontWeight = FontWeight.Medium, - ) + Text( + modifier = Modifier.fillMaxWidth(), + text = calendarMonth.yearMonth.displayText(short = true), + fontSize = 16.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + ) + Row(modifier = Modifier.fillMaxWidth()) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = 11.sp, + text = dayOfWeek.displayText(uppercase = true, narrow = true), + fontWeight = FontWeight.Medium, + ) + } } } } +@Composable +private fun YearHeader(year: Year) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 10.dp) + .testTag("MonthHeader"), + textAlign = TextAlign.Center, + fontSize = 52.sp, + text = year.toString(), + fontWeight = FontWeight.Medium, + ) +} + @Composable private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { Box( modifier = Modifier .aspectRatio(1f) // This is important for square-sizing! +// .fillMaxSize() .testTag("MonthDay") .padding(6.dp) .clip(CircleShape) @@ -127,20 +171,17 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> ), contentAlignment = Alignment.Center, ) { - val textColor = when (day.position) { - // Color.Unspecified will use the default text color from the current theme - DayPosition.MonthDate -> if (isSelected) Color.White else Color.Unspecified - DayPosition.InDate, DayPosition.OutDate -> colorResource(R.color.inactive_text_color) + if (day.position == DayPosition.MonthDate) { + Text( + text = day.date.dayOfMonth.toString(), + fontSize = 10.sp, + ) } - Text( - text = day.date.dayOfMonth.toString(), - color = textColor, - fontSize = 14.sp, - ) + } } -@Preview +@Preview(showBackground = true, heightDp = 1280, widthDp = 800, device = PIXEL_TABLET) @Composable private fun Example1Preview() { Example1Page() diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt index c289c55c..a161b470 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt @@ -120,7 +120,7 @@ private fun MonthHeader(calendarMonth: CalendarMonth) { modifier = Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 15.sp, - text = dayOfWeek.name.first().toString(), + text = dayOfWeek.displayText(uppercase = true, narrow = true), fontWeight = FontWeight.Medium, ) } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt index 798103c1..645c5304 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt @@ -20,8 +20,9 @@ fun Month.displayText(short: Boolean = true): String { return getDisplayName(style, Locale.ENGLISH) } -fun DayOfWeek.displayText(uppercase: Boolean = false): String { - return getDisplayName(TextStyle.SHORT, Locale.ENGLISH).let { value -> +fun DayOfWeek.displayText(uppercase: Boolean = false, narrow: Boolean = false): String { + val style = if (narrow) TextStyle.NARROW else TextStyle.SHORT + return getDisplayName(style, Locale.ENGLISH).let { value -> if (uppercase) value.uppercase(Locale.ENGLISH) else value } } diff --git a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt index ec6f77de..1769ad55 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/CalendarView.kt @@ -10,7 +10,7 @@ import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.OutDateStyle -import com.kizitonwose.calendar.data.checkDateRange +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelper import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelperLegacy import com.kizitonwose.calendar.view.internal.monthcalendar.MonthCalendarAdapter @@ -434,7 +434,7 @@ public open class CalendarView : RecyclerView { * @param firstDayOfWeek A [DayOfWeek] to be the first day of week. */ public fun setup(startMonth: YearMonth, endMonth: YearMonth, firstDayOfWeek: DayOfWeek) { - checkDateRange(startMonth = startMonth, endMonth = endMonth) + checkRange(start = startMonth, end = endMonth) this.startMonth = startMonth this.endMonth = endMonth this.firstDayOfWeek = firstDayOfWeek @@ -464,7 +464,7 @@ public open class CalendarView : RecyclerView { endMonth: YearMonth = requireEndMonth(), firstDayOfWeek: DayOfWeek = requireFirstDayOfWeek(), ) { - checkDateRange(startMonth = startMonth, endMonth = endMonth) + checkRange(start = startMonth, end = endMonth) this.startMonth = startMonth this.endMonth = endMonth this.firstDayOfWeek = firstDayOfWeek diff --git a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt index e4fc63f4..7e461c1f 100644 --- a/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt +++ b/view/src/main/java/com/kizitonwose/calendar/view/WeekCalendarView.kt @@ -7,7 +7,7 @@ import androidx.core.content.withStyledAttributes import androidx.recyclerview.widget.RecyclerView import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay -import com.kizitonwose.calendar.data.checkDateRange +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.view.internal.CalendarPageSnapHelperLegacy import com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarAdapter import com.kizitonwose.calendar.view.internal.weekcalendar.WeekCalendarLayoutManager @@ -387,7 +387,7 @@ public open class WeekCalendarView : RecyclerView { * @param firstDayOfWeek A [DayOfWeek] to be the first day of week. */ public fun setup(startDate: LocalDate, endDate: LocalDate, firstDayOfWeek: DayOfWeek) { - checkDateRange(startDate = startDate, endDate = endDate) + checkRange(start = startDate, end = endDate) this.startDate = startDate this.endDate = endDate this.firstDayOfWeek = firstDayOfWeek @@ -416,7 +416,7 @@ public open class WeekCalendarView : RecyclerView { endDate: LocalDate = requireEndDate(), firstDayOfWeek: DayOfWeek = requireFirstDayOfWeek(), ) { - checkDateRange(startDate = startDate, endDate = endDate) + checkRange(start = startDate, end = endDate) this.startDate = startDate this.endDate = endDate this.firstDayOfWeek = firstDayOfWeek From b3b13c184b65d1f12d5d267e655b41a4902ad56c Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 20 Jul 2024 22:59:12 +0200 Subject: [PATCH 02/48] Improve year calendar samples --- .../sample/compose/CalendarComposeActivity.kt | 2 + .../calendar/sample/compose/Example10Page.kt | 241 ++++++++++++++++++ .../calendar/sample/compose/Example11Page.kt | 176 +++++++++++++ .../calendar/sample/compose/Example1Page.kt | 147 ++++------- .../calendar/sample/compose/ListPage.kt | 10 + .../calendar/sample/compose/Utils.kt | 86 +++++++ 6 files changed, 568 insertions(+), 94 deletions(-) create mode 100644 sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt create mode 100644 sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt index dadd1eef..1cc58d5a 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt @@ -117,6 +117,8 @@ class CalendarComposeActivity : AppCompatActivity() { horizontallyAnimatedComposable(Page.Example7.name) { Example7Page() } verticallyAnimatedComposable(Page.Example8.name) { Example8Page() } horizontallyAnimatedComposable(Page.Example9.name) { Example9Page() } + horizontallyAnimatedComposable(Page.Example10.name) { Example10Page() } + horizontallyAnimatedComposable(Page.Example11.name) { Example11Page() } } } } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt new file mode 100644 index 00000000..f8f3c201 --- /dev/null +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -0,0 +1,241 @@ +package com.kizitonwose.calendar.sample.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kizitonwose.calendar.compose.HorizontalYearCalendar +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.ExperimentalCalendarApi +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.sample.shared.displayText +import kotlinx.coroutines.launch +import java.time.Year +import java.time.YearMonth +import java.time.temporal.ChronoUnit +import kotlin.math.abs + +@OptIn(ExperimentalCalendarApi::class) +@Composable +fun Example10Page(adjacentYears: Long = 50) { + val currentMonth = remember { YearMonth.now() } + val currentYear = remember { Year.of(currentMonth.year) } + val startYear = remember { currentYear.minusYears(adjacentYears) } + val endYear = remember { currentYear.plusYears(adjacentYears) } + val selections = remember { mutableStateListOf() } + val daysOfWeek = remember { daysOfWeek() } + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + ) { + val scope = rememberCoroutineScope() + val state = rememberYearCalendarState( + startMonth = startYear, + endMonth = endYear, + firstVisibleMonth = currentYear, + firstDayOfWeek = daysOfWeek.first(), + ) + val visibleYear = rememberFirstVisibleYearAfterScroll(state) + val headerState = rememberLazyListState() + LaunchedEffect(visibleYear) { + val index = ChronoUnit.YEARS.between(startYear, visibleYear.year).toInt() + headerState.animateScrollAndCenterItem(index) + } + YearHeader( + startYear = startYear, + endYear = endYear, + visibleYear = visibleYear.year, + headerState = headerState, + ) click@{ target -> + val visible = visibleYear.year + if (target == visible) return@click + scope.launch { + if (abs(ChronoUnit.YEARS.between(visible, target)) <= 10) { + state.animateScrollToMonth(target) + } else { + val nearbyYear = if (target > visible) { + target.minusYears(8) + } else { + target.plusYears(8) + } + state.scrollToMonth(nearbyYear) + state.animateScrollToMonth(target) + } + } + } + Spacer(modifier = Modifier.size(12.dp)) + HorizontalYearCalendar( + modifier = Modifier + .fillMaxSize() + .testTag("Calendar"), + state = state, + dayContent = { day -> + Day(day, isSelected = selections.contains(day)) { clicked -> + if (selections.contains(clicked)) { + selections.remove(clicked) + } else { + selections.add(clicked) + } + } + }, + contentHeightMode = YearContentHeightMode.Fill, + monthHorizontalArrangement = Arrangement.spacedBy(40.dp), + monthVerticalArrangement = Arrangement.spacedBy(20.dp), + yearBodyContentPadding = PaddingValues(start = 40.dp, end = 40.dp, bottom = 40.dp), + monthHeader = { + MonthHeader(it) + }, + ) + } +} + +@Composable +private fun MonthHeader(calendarMonth: CalendarMonth) { + val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } + Column( + modifier = Modifier + .wrapContentHeight() + .padding(top = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = calendarMonth.yearMonth.month.displayText(short = false), + fontSize = 16.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + ) + Row(modifier = Modifier.fillMaxWidth()) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = 11.sp, + text = dayOfWeek.displayText(uppercase = true, narrow = true), + fontWeight = FontWeight.Medium, + ) + } + } + } +} + +@Composable +private fun YearHeader( + startYear: Year, + endYear: Year, + visibleYear: Year, + headerState: LazyListState, + onClick: (Year) -> Unit, +) { + LazyRow( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .background(headerBackground), + state = headerState, + flingBehavior = rememberSnapFlingBehavior(lazyListState = headerState, SnapPosition.Center), + contentPadding = PaddingValues(horizontal = 40.dp), + ) { + items(count = ChronoUnit.YEARS.between(startYear, endYear).toInt()) { index -> + val year = startYear.plusYears(index.toLong()) + val isSelected = visibleYear == year + Box( + modifier = Modifier + .then( + if (isSelected) { + Modifier.background( + color = simpleTextBackground(isSelected = true), + shape = RoundedCornerShape(4.dp), + ) + } else { + Modifier + }, + ) + .clickable(onClick = { onClick(year) }) + .padding(horizontal = 60.dp, vertical = 10.dp), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = year.value.toString(), + textAlign = TextAlign.Center, + fontSize = 24.sp, + color = simpleTextColor(isSelected), + fontWeight = if (isSelected) FontWeight.Black else FontWeight.Light, + ) + } + } + } +} + +@Composable +private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { + Box( + modifier = Modifier + .aspectRatio(1f) // This is important for square-sizing! + .testTag("MonthDay") + .padding(2.dp) + .clip(CircleShape) + .background(simpleTextBackground(isSelected)) + // Disable clicks on inDates/outDates + .clickable( + enabled = day.position == DayPosition.MonthDate, + showRipple = !isSelected, + onClick = { onClick(day) }, + ), + contentAlignment = Alignment.Center, + ) { + if (day.position == DayPosition.MonthDate) { + Text( + text = day.date.dayOfMonth.toString(), + fontSize = 10.sp, + color = simpleTextColor(isSelected), + ) + } + } +} + +@Preview(showBackground = true, heightDp = 1280, widthDp = 800, device = PIXEL_TABLET) +@Composable +private fun Example10Preview() { + Example10Page() +} + +private val headerBackground = Color(0xFFF1F1F1) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt new file mode 100644 index 00000000..94c9d98c --- /dev/null +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt @@ -0,0 +1,176 @@ +package com.kizitonwose.calendar.sample.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Divider +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kizitonwose.calendar.compose.VerticalYearCalendar +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.ExperimentalCalendarApi +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.sample.R +import com.kizitonwose.calendar.sample.shared.displayText +import java.time.Year +import java.time.YearMonth + +@OptIn(ExperimentalCalendarApi::class) +@Composable +fun Example11Page(adjacentMonths: Long = 50) { + val currentMonth = remember { YearMonth.now() } + val currentYear = remember { Year.of(currentMonth.year) } + val endYear = remember { currentYear.plusYears(adjacentMonths) } + val selections = remember { mutableStateListOf() } + val daysOfWeek = remember { daysOfWeek() } + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + ) { + val state = rememberYearCalendarState( + startMonth = currentYear, + endMonth = endYear, + firstVisibleMonth = currentYear, + firstDayOfWeek = daysOfWeek.first(), + ) + VerticalYearCalendar( + modifier = Modifier + .fillMaxSize() + .testTag("Calendar"), + state = state, + dayContent = { day -> + Day(day, isSelected = selections.contains(day)) { clicked -> + if (selections.contains(clicked)) { + selections.remove(clicked) + } else { + selections.add(clicked) + } + } + }, + calendarScrollPaged = false, + contentHeightMode = YearContentHeightMode.Wrap, + monthVerticalArrangement = Arrangement.spacedBy(20.dp), + monthHorizontalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(horizontal = 20.dp), + isMonthVisible = { + it.yearMonth >= currentMonth + }, + yearHeader = { + YearHeader(it.year) + }, + monthHeader = { + MonthHeader(it) + }, + ) + } +} + +@Composable +private fun MonthHeader(calendarMonth: CalendarMonth) { + val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } + Column( + modifier = Modifier + .wrapContentHeight() + .padding(top = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp), + text = calendarMonth.yearMonth.month.displayText(short = false), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + Row(modifier = Modifier.fillMaxWidth()) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = 11.sp, + text = dayOfWeek.displayText(uppercase = true, narrow = true), + fontWeight = FontWeight.Medium, + ) + } + } + } +} + +@Composable +private fun YearHeader(year: Year) { + Column(modifier = Modifier + .fillMaxWidth() + .padding(10.dp)) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 20.dp) + .testTag("MonthHeader"), + fontSize = 52.sp, + text = year.toString(), + fontWeight = FontWeight.Medium, + ) + Divider() + } +} + +@Composable +private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { + Box( + modifier = Modifier + .aspectRatio(1f) // This is important for square-sizing! + .testTag("MonthDay") + .padding(2.dp) + .clip(CircleShape) + .background(color = if (isSelected) colorResource(R.color.example_1_selection_color) else Color.Transparent) + // Disable clicks on inDates/outDates + .clickable( + enabled = day.position == DayPosition.MonthDate, + showRipple = !isSelected, + onClick = { onClick(day) }, + ), + contentAlignment = Alignment.Center, + ) { + if (day.position == DayPosition.MonthDate) { + Text( + text = day.date.dayOfMonth.toString(), + fontSize = 10.sp, + ) + } + + } +} + +@Preview(showBackground = true, heightDp = 1280, widthDp = 800, device = PIXEL_TABLET) +@Composable +private fun Example11Preview() { + Example11Page() +} diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt index 5d649f1e..89d20a9d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt @@ -1,16 +1,13 @@ package com.kizitonwose.calendar.sample.compose import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -25,30 +22,27 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.kizitonwose.calendar.compose.VerticalYearCalendar -import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode -import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState +import com.kizitonwose.calendar.compose.HorizontalCalendar +import com.kizitonwose.calendar.compose.rememberCalendarState import com.kizitonwose.calendar.core.CalendarDay -import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition -import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.nextMonth +import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.sample.R import com.kizitonwose.calendar.sample.shared.displayText -import java.time.Year +import kotlinx.coroutines.launch +import java.time.DayOfWeek import java.time.YearMonth -@OptIn(ExperimentalCalendarApi::class) @Composable -fun Example1Page(adjacentMonths: Long = 20) { +fun Example1Page(adjacentMonths: Long = 500) { val currentMonth = remember { YearMonth.now() } - val currentYear = remember { Year.of(currentMonth.year) } - val startYear = remember { currentYear } - val endYear = remember { currentYear.plusYears(adjacentMonths) } + val startMonth = remember { currentMonth.minusMonths(adjacentMonths) } + val endMonth = remember { currentMonth.plusMonths(adjacentMonths) } val selections = remember { mutableStateListOf() } val daysOfWeek = remember { daysOfWeek() } Column( @@ -56,32 +50,30 @@ fun Example1Page(adjacentMonths: Long = 20) { .fillMaxSize() .background(Color.White), ) { - val state = rememberYearCalendarState( - startMonth = currentYear, - endMonth = endYear, - firstVisibleMonth = currentYear, + val state = rememberCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = currentMonth, firstDayOfWeek = daysOfWeek.first(), ) val coroutineScope = rememberCoroutineScope() -// val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f) -// SimpleCalendarTitle( -// modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), -// currentMonth = visibleMonth.yearMonth, -// goToPrevious = { -// coroutineScope.launch { -// state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) -// } -// }, -// goToNext = { -// coroutineScope.launch { -// state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth) -// } -// }, -// ) - VerticalYearCalendar( - modifier = Modifier - .fillMaxSize() - .testTag("Calendar"), + val visibleMonth = rememberFirstMostVisibleMonth(state, viewportPercent = 90f) + SimpleCalendarTitle( + modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), + currentMonth = visibleMonth.yearMonth, + goToPrevious = { + coroutineScope.launch { + state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) + } + }, + goToNext = { + coroutineScope.launch { + state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.nextMonth) + } + }, + ) + HorizontalCalendar( + modifier = Modifier.testTag("Calendar"), state = state, dayContent = { day -> Day(day, isSelected = selections.contains(day)) { clicked -> @@ -92,73 +84,37 @@ fun Example1Page(adjacentMonths: Long = 20) { } } }, - contentHeightMode = YearContentHeightMode.Wrap, - monthVerticalArrangement = Arrangement.spacedBy(20.dp), - monthHorizontalArrangement = Arrangement.spacedBy(20.dp), - yearBodyContentPadding = PaddingValues(horizontal = 20.dp), - isMonthVisible = { - it.yearMonth >= currentMonth - }, - yearHeader = { - YearHeader(it.year) - }, monthHeader = { - MonthHeader(it) + MonthHeader(daysOfWeek = daysOfWeek) }, ) } } @Composable -private fun MonthHeader(calendarMonth: CalendarMonth) { - val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } - Column( +private fun MonthHeader(daysOfWeek: List) { + Row( modifier = Modifier - .wrapContentHeight() - .padding(top = 6.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + .fillMaxWidth() + .testTag("MonthHeader"), ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = calendarMonth.yearMonth.displayText(short = true), - fontSize = 16.sp, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Medium, - ) - Row(modifier = Modifier.fillMaxWidth()) { - for (dayOfWeek in daysOfWeek) { - Text( - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - fontSize = 11.sp, - text = dayOfWeek.displayText(uppercase = true, narrow = true), - fontWeight = FontWeight.Medium, - ) - } + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = 15.sp, + text = dayOfWeek.displayText(), + fontWeight = FontWeight.Medium, + ) } } } -@Composable -private fun YearHeader(year: Year) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 10.dp) - .testTag("MonthHeader"), - textAlign = TextAlign.Center, - fontSize = 52.sp, - text = year.toString(), - fontWeight = FontWeight.Medium, - ) -} - @Composable private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { Box( modifier = Modifier .aspectRatio(1f) // This is important for square-sizing! -// .fillMaxSize() .testTag("MonthDay") .padding(6.dp) .clip(CircleShape) @@ -171,17 +127,20 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> ), contentAlignment = Alignment.Center, ) { - if (day.position == DayPosition.MonthDate) { - Text( - text = day.date.dayOfMonth.toString(), - fontSize = 10.sp, - ) + val textColor = when (day.position) { + // Color.Unspecified will use the default text color from the current theme + DayPosition.MonthDate -> if (isSelected) Color.White else Color.Unspecified + DayPosition.InDate, DayPosition.OutDate -> colorResource(R.color.inactive_text_color) } - + Text( + text = day.date.dayOfMonth.toString(), + color = textColor, + fontSize = 14.sp, + ) } } -@Preview(showBackground = true, heightDp = 1280, widthDp = 800, device = PIXEL_TABLET) +@Preview @Composable private fun Example1Preview() { Example1Page() diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt index 36615fa5..b0b68e7c 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt @@ -71,6 +71,16 @@ enum class Page(val title: String, val subtitle: String, val showToolBar: Boolea subtitle = "Month and week calendar toggle with animations.", showToolBar = true, ), + Example10( + title = "Example 10", + subtitle = "Horizontal year calendar - Year header and paged scrolling. Best suited for large displays.", + showToolBar = true, + ), + Example11( + title = "Example 11", + subtitle = "Vertical year calendar - Hidden past months with continuous scroll. Best used on large displays.", + showToolBar = true, + ), } @Composable diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index b5e699e5..b9df405e 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -2,15 +2,19 @@ package com.kizitonwose.calendar.sample.compose import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon +import androidx.compose.material.darkColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf @@ -30,7 +34,11 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarLayoutInfo +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.sample.shared.StatusBarColorLifecycleObserver import com.kizitonwose.calendar.sample.shared.findActivity @@ -156,6 +164,43 @@ fun rememberFirstMostVisibleMonth( return visibleMonth.value } +/** + * Find the first year on the calendar visible up to the given [viewportPercent] size. + * + * @see [rememberFirstVisibleYearAfterScroll] + */ +@OptIn(ExperimentalCalendarApi::class) +@Composable +fun rememberFirstMostVisibleYear( + state: YearCalendarState, + viewportPercent: Float = 50f, +): CalendarYear { + val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) } + LaunchedEffect(state) { + snapshotFlow { state.layoutInfo.firstMostVisibleYear(viewportPercent) } + .filterNotNull() + .collect { month -> visibleMonth.value = month } + } + return visibleMonth.value +} + +/** + * Returns the first visible year in a paged calendar **after** scrolling stops. + * + * @see [rememberFirstMostVisibleYear] + */ +@OptIn(ExperimentalCalendarApi::class) +@Composable +fun rememberFirstVisibleYearAfterScroll(state: YearCalendarState): CalendarYear { + val visibleYear = remember(state) { mutableStateOf(state.firstVisibleMonth) } + LaunchedEffect(state) { + snapshotFlow { state.isScrollInProgress } + .filter { scrolling -> !scrolling } + .collect { visibleYear.value = state.firstVisibleMonth } + } + return visibleYear.value +} + private val CalendarLayoutInfo.completelyVisibleMonths: List get() { val visibleItemsInfo = this.visibleMonthsInfo.toMutableList() @@ -189,3 +234,44 @@ private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50 }?.month } } + +private fun YearCalendarLayoutInfo.firstMostVisibleYear(viewportPercent: Float = 50f): CalendarYear? { + return if (visibleWeeksInfo.isEmpty()) { + null + } else { + val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f + visibleWeeksInfo.firstOrNull { itemInfo -> + if (itemInfo.offset < 0) { + itemInfo.offset + itemInfo.size >= viewportSize + } else { + itemInfo.size - itemInfo.offset >= viewportSize + } + }?.week + } +} + +suspend fun LazyListState.animateScrollAndCenterItem(index: Int) { + suspend fun animateScrollIfVisible(): Boolean { + val layoutInfo = layoutInfo + val containerSize = layoutInfo.viewportSize.width - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding + val target = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return false + val targetOffset = containerSize / 2f - target.size / 2f + animateScrollBy(target.offset - targetOffset) + return true + } + if (!animateScrollIfVisible()) { + val visibleItemsInfo = layoutInfo.visibleItemsInfo + val currentIndex = visibleItemsInfo.getOrNull(visibleItemsInfo.size / 2)?.index ?: -1 + scrollToItem( + if (index > currentIndex) { + (index - visibleItemsInfo.size + 1) + } else { + index + }.coerceIn(0, layoutInfo.totalItemsCount), + ) + animateScrollIfVisible() + } +} + +fun simpleTextColor(isSelected: Boolean) = if (isSelected) darkColors().onSurface else lightColors().onSurface +fun simpleTextBackground(isSelected: Boolean) = if (isSelected) darkColors().surface else lightColors().surface From 6bcbadc0b0ddcff668cbf82d2fa6b518b610c4ac Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 21 Jul 2024 08:51:05 +0200 Subject: [PATCH 03/48] Optimise year calendar samples for small screens. --- .../calendar/sample/compose/Example10Page.kt | 152 +++++++++++------- .../calendar/sample/compose/Example11Page.kt | 37 +++-- .../calendar/sample/compose/Utils.kt | 5 - 3 files changed, 122 insertions(+), 72 deletions(-) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index f8f3c201..3a9a1114 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -8,12 +8,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow @@ -21,18 +19,25 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Text +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices.PIXEL_7 import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -61,6 +66,9 @@ fun Example10Page(adjacentYears: Long = 50) { val endYear = remember { currentYear.plusYears(adjacentYears) } val selections = remember { mutableStateListOf() } val daysOfWeek = remember { daysOfWeek() } + val config = LocalConfiguration.current + val isTablet = config.smallestScreenWidthDp >= 600 + val isPortrait = config.screenHeightDp > config.screenWidthDp Column( modifier = Modifier .fillMaxSize() @@ -73,42 +81,48 @@ fun Example10Page(adjacentYears: Long = 50) { firstVisibleMonth = currentYear, firstDayOfWeek = daysOfWeek.first(), ) - val visibleYear = rememberFirstVisibleYearAfterScroll(state) + val visibleYearAfterScroll = rememberFirstVisibleYearAfterScroll(state).year + var visibleYear by remember(visibleYearAfterScroll) { mutableStateOf(visibleYearAfterScroll) } val headerState = rememberLazyListState() LaunchedEffect(visibleYear) { - val index = ChronoUnit.YEARS.between(startYear, visibleYear.year).toInt() + val index = ChronoUnit.YEARS.between(startYear, visibleYear).toInt() headerState.animateScrollAndCenterItem(index) } YearHeader( startYear = startYear, endYear = endYear, - visibleYear = visibleYear.year, + visibleYear = visibleYear, headerState = headerState, - ) click@{ target -> - val visible = visibleYear.year - if (target == visible) return@click + isTablet = isTablet, + ) click@{ targetYear -> + if (targetYear == visibleYear) return@click + visibleYear = targetYear scope.launch { - if (abs(ChronoUnit.YEARS.between(visible, target)) <= 10) { - state.animateScrollToMonth(target) + if (abs(ChronoUnit.YEARS.between(visibleYear, targetYear)) <= 8) { + state.animateScrollToMonth(targetYear) } else { - val nearbyYear = if (target > visible) { - target.minusYears(8) + val nearbyYear = if (targetYear > visibleYear) { + targetYear.minusYears(5) } else { - target.plusYears(8) + targetYear.plusYears(5) } state.scrollToMonth(nearbyYear) - state.animateScrollToMonth(target) + state.animateScrollToMonth(targetYear) } } } - Spacer(modifier = Modifier.size(12.dp)) HorizontalYearCalendar( modifier = Modifier .fillMaxSize() .testTag("Calendar"), state = state, + columns = if (isPortrait) 3 else if (isTablet) 4 else 6, dayContent = { day -> - Day(day, isSelected = selections.contains(day)) { clicked -> + Day( + day = day, + isSelected = selections.contains(day), + isTablet = isTablet, + ) { clicked -> if (selections.contains(clicked)) { selections.remove(clicked) } else { @@ -117,62 +131,41 @@ fun Example10Page(adjacentYears: Long = 50) { } }, contentHeightMode = YearContentHeightMode.Fill, - monthHorizontalArrangement = Arrangement.spacedBy(40.dp), - monthVerticalArrangement = Arrangement.spacedBy(20.dp), - yearBodyContentPadding = PaddingValues(start = 40.dp, end = 40.dp, bottom = 40.dp), + monthHorizontalArrangement = Arrangement.spacedBy(if (isTablet) if (isPortrait) 52.dp else 92.dp else 10.dp), + monthVerticalArrangement = Arrangement.spacedBy(if (isTablet) 20.dp else 4.dp), + yearBodyContentPadding = if (isTablet) { + PaddingValues(horizontal = if (isPortrait) 52.dp else 92.dp, vertical = 20.dp) + } else { + PaddingValues(all = 10.dp) + }, monthHeader = { - MonthHeader(it) + MonthHeader( + calendarMonth = it, + isTablet = isTablet, + ) }, ) } } -@Composable -private fun MonthHeader(calendarMonth: CalendarMonth) { - val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } - Column( - modifier = Modifier - .wrapContentHeight() - .padding(top = 6.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Text( - modifier = Modifier.fillMaxWidth(), - text = calendarMonth.yearMonth.month.displayText(short = false), - fontSize = 16.sp, - textAlign = TextAlign.Center, - fontWeight = FontWeight.Medium, - ) - Row(modifier = Modifier.fillMaxWidth()) { - for (dayOfWeek in daysOfWeek) { - Text( - modifier = Modifier.weight(1f), - textAlign = TextAlign.Center, - fontSize = 11.sp, - text = dayOfWeek.displayText(uppercase = true, narrow = true), - fontWeight = FontWeight.Medium, - ) - } - } - } -} - @Composable private fun YearHeader( startYear: Year, endYear: Year, visibleYear: Year, headerState: LazyListState, + isTablet: Boolean, + modifier: Modifier = Modifier, onClick: (Year) -> Unit, ) { LazyRow( - modifier = Modifier + modifier = modifier .fillMaxWidth() .wrapContentHeight() .background(headerBackground), state = headerState, flingBehavior = rememberSnapFlingBehavior(lazyListState = headerState, SnapPosition.Center), - contentPadding = PaddingValues(horizontal = 40.dp), + contentPadding = PaddingValues(horizontal = if (isTablet) 40.dp else 10.dp), ) { items(count = ChronoUnit.YEARS.between(startYear, endYear).toInt()) { index -> val year = startYear.plusYears(index.toLong()) @@ -190,13 +183,16 @@ private fun YearHeader( }, ) .clickable(onClick = { onClick(year) }) - .padding(horizontal = 60.dp, vertical = 10.dp), + .padding( + horizontal = if (isTablet) 60.dp else 28.dp, + vertical = if (isTablet) 10.dp else 6.dp, + ), ) { Text( modifier = Modifier.align(Alignment.Center), text = year.value.toString(), textAlign = TextAlign.Center, - fontSize = 24.sp, + fontSize = if (isTablet) 24.sp else 18.sp, color = simpleTextColor(isSelected), fontWeight = if (isSelected) FontWeight.Black else FontWeight.Light, ) @@ -206,12 +202,49 @@ private fun YearHeader( } @Composable -private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { +private fun MonthHeader( + calendarMonth: CalendarMonth, + isTablet: Boolean, + modifier: Modifier = Modifier, +) { + val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(if (isTablet) 12.dp else 8.dp), + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = calendarMonth.yearMonth.month.displayText(short = false), + fontSize = if (isTablet) 16.sp else 12.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + ) + Row(modifier = Modifier.fillMaxWidth()) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = if (isTablet) 11.sp else 9.sp, + text = dayOfWeek.displayText(uppercase = true, narrow = true), + fontWeight = FontWeight.SemiBold, + ) + } + } + } +} + +@Composable +private fun Day( + day: CalendarDay, + isSelected: Boolean, + isTablet: Boolean, + onClick: (CalendarDay) -> Unit, +) { Box( modifier = Modifier .aspectRatio(1f) // This is important for square-sizing! .testTag("MonthDay") - .padding(2.dp) + .padding(if (isTablet) 2.dp else 0.dp) .clip(CircleShape) .background(simpleTextBackground(isSelected)) // Disable clicks on inDates/outDates @@ -225,7 +258,7 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> if (day.position == DayPosition.MonthDate) { Text( text = day.date.dayOfMonth.toString(), - fontSize = 10.sp, + fontSize = if (isTablet) 11.sp else 9.sp, color = simpleTextColor(isSelected), ) } @@ -233,9 +266,14 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> } @Preview(showBackground = true, heightDp = 1280, widthDp = 800, device = PIXEL_TABLET) +@Preview(showBackground = true, heightDp = 800, widthDp = 1280, device = PIXEL_TABLET) +@Preview(showBackground = true, heightDp = 891, widthDp = 411, device = PIXEL_7) +@Preview(showBackground = true, heightDp = 411, widthDp = 891, device = PIXEL_7) @Composable private fun Example10Preview() { Example10Page() } private val headerBackground = Color(0xFFF1F1F1) +private fun simpleTextColor(isSelected: Boolean) = if (isSelected) darkColors().onSurface else lightColors().onSurface +private fun simpleTextBackground(isSelected: Boolean) = if (isSelected) darkColors().surface else lightColors().surface diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt index 94c9d98c..14f0cd8a 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt @@ -21,10 +21,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Devices.PIXEL_7 import androidx.compose.ui.tooling.preview.Devices.PIXEL_TABLET import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -50,6 +52,8 @@ fun Example11Page(adjacentMonths: Long = 50) { val endYear = remember { currentYear.plusYears(adjacentMonths) } val selections = remember { mutableStateListOf() } val daysOfWeek = remember { daysOfWeek() } + val config = LocalConfiguration.current + val isTablet = config.smallestScreenWidthDp >= 600 Column( modifier = Modifier .fillMaxSize() @@ -67,7 +71,11 @@ fun Example11Page(adjacentMonths: Long = 50) { .testTag("Calendar"), state = state, dayContent = { day -> - Day(day, isSelected = selections.contains(day)) { clicked -> + Day( + day = day, + isSelected = selections.contains(day), + isTablet = isTablet, + ) { clicked -> if (selections.contains(clicked)) { selections.remove(clicked) } else { @@ -78,8 +86,8 @@ fun Example11Page(adjacentMonths: Long = 50) { calendarScrollPaged = false, contentHeightMode = YearContentHeightMode.Wrap, monthVerticalArrangement = Arrangement.spacedBy(20.dp), - monthHorizontalArrangement = Arrangement.spacedBy(20.dp), - contentPadding = PaddingValues(horizontal = 20.dp), + monthHorizontalArrangement = Arrangement.spacedBy(if (isTablet) 52.dp else 10.dp), + contentPadding = PaddingValues(horizontal = if (isTablet) 52.dp else 10.dp), isMonthVisible = { it.yearMonth >= currentMonth }, @@ -126,9 +134,11 @@ private fun MonthHeader(calendarMonth: CalendarMonth) { @Composable private fun YearHeader(year: Year) { - Column(modifier = Modifier - .fillMaxWidth() - .padding(10.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + ) { Text( modifier = Modifier .fillMaxWidth() @@ -143,15 +153,20 @@ private fun YearHeader(year: Year) { } @Composable -private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> Unit) { +private fun Day( + day: CalendarDay, + isSelected: Boolean, + isTablet: Boolean, + onClick: (CalendarDay) -> Unit, +) { Box( modifier = Modifier .aspectRatio(1f) // This is important for square-sizing! .testTag("MonthDay") - .padding(2.dp) + .padding(if (isTablet) 2.dp else 0.dp) .clip(CircleShape) .background(color = if (isSelected) colorResource(R.color.example_1_selection_color) else Color.Transparent) - // Disable clicks on inDates/outDates +// Disable clicks on inDates/outDates .clickable( enabled = day.position == DayPosition.MonthDate, showRipple = !isSelected, @@ -162,7 +177,8 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> if (day.position == DayPosition.MonthDate) { Text( text = day.date.dayOfMonth.toString(), - fontSize = 10.sp, + fontSize = if (isTablet) 10.sp else 9.sp, + color = if (isSelected) Color.White else Color.Unspecified, ) } @@ -170,6 +186,7 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> } @Preview(showBackground = true, heightDp = 1280, widthDp = 800, device = PIXEL_TABLET) +@Preview(showBackground = true, heightDp = 891, widthDp = 411, device = PIXEL_7) @Composable private fun Example11Preview() { Example11Page() diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index b9df405e..1a729a8b 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -11,10 +11,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon -import androidx.compose.material.darkColors import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf @@ -272,6 +270,3 @@ suspend fun LazyListState.animateScrollAndCenterItem(index: Int) { animateScrollIfVisible() } } - -fun simpleTextColor(isSelected: Boolean) = if (isSelected) darkColors().onSurface else lightColors().onSurface -fun simpleTextBackground(isSelected: Boolean) = if (isSelected) darkColors().surface else lightColors().surface From 3e30f6f6c9a6398e7fb4856118f0e3155c8d303e Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 21 Jul 2024 16:35:45 +0200 Subject: [PATCH 04/48] Add docs for year calendar --- .../kizitonwose/calendar/compose/Calendar.kt | 8 +- .../calendar/compose/CalendarState.kt | 3 +- .../heatmapcalendar/HeatMapCalendarState.kt | 3 +- .../kizitonwose/calendar/compose/Calendar.kt | 120 +++++++++++++- .../calendar/compose/CalendarState.kt | 2 +- .../heatmapcalendar/HeatMapCalendarState.kt | 2 +- .../yearcalendar/YearCalendarLayoutInfo.kt | 12 +- .../compose/yearcalendar/YearCalendarState.kt | 147 +++++++++--------- .../yearcalendar/YearContentHeightMode.kt | 23 ++- .../calendar/sample/compose/Example10Page.kt | 33 ++-- .../calendar/sample/compose/Example11Page.kt | 9 +- .../calendar/sample/compose/Utils.kt | 12 +- 12 files changed, 251 insertions(+), 123 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt index 981d03e9..cf14adb4 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt @@ -25,7 +25,7 @@ import com.kizitonwose.calendar.core.WeekDay import kotlinx.datetime.DayOfWeek /** - * A horizontally scrolling calendar. + * A horizontally scrolling month calendar. * * @param modifier the modifier to apply to this calendar. * @param state the state object to be used to control or observe the calendar's properties. @@ -39,7 +39,7 @@ import kotlinx.datetime.DayOfWeek * @param contentPadding a padding around the whole calendar. This will add padding for the * content after it has been clipped, which is not possible via [modifier] param. You can use it * to add a padding before the first month or after the last one. If you want to add a spacing - * between each month use the [monthContainer] composable. + * between each month, use the [monthContainer] composable. * @param contentHeightMode Determines how the height of the day content is calculated. * @param dayContent a composable block which describes the day content. * @param monthHeader a composable block which describes the month header content. The header is @@ -88,7 +88,7 @@ public fun HorizontalCalendar( ) /** - * A vertically scrolling calendar. + * A vertically scrolling month calendar. * * @param modifier the modifier to apply to this calendar. * @param state the state object to be used to control or observe the calendar's properties. @@ -102,7 +102,7 @@ public fun HorizontalCalendar( * @param contentPadding a padding around the whole calendar. This will add padding for the * content after it has been clipped, which is not possible via [modifier] param. You can use it * to add a padding before the first month or after the last one. If you want to add a spacing - * between each month use the [monthContainer] composable. + * between each month, use the [monthContainer] composable. * @param contentHeightMode Determines how the height of the day content is calculated. * @param dayContent a composable block which describes the day content. * @param monthHeader a composable block which describes the month header content. The header is diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt index 3ae6bd8a..ac4f221a 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.setValue import com.kizitonwose.calendar.core.OutDateStyle import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import com.kizitonwose.calendar.core.now import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState import com.kizitonwose.calendar.data.checkDateRange @@ -202,7 +201,7 @@ public class CalendarState internal constructor( } init { - monthDataChanged() // Update monthIndexCount initially. + monthDataChanged() // Update indexCount initially. } private fun monthDataChanged() { diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt index bbf0fc97..c40b75c3 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt @@ -19,7 +19,6 @@ import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import com.kizitonwose.calendar.core.now import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState import com.kizitonwose.calendar.data.checkDateRange @@ -181,7 +180,7 @@ public class HeatMapCalendarState internal constructor( } init { - monthDataChanged() // Update monthIndexCount initially. + monthDataChanged() // Update indexCount initially. } private fun monthDataChanged() { diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt index a575d535..e016b2eb 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt @@ -33,7 +33,7 @@ import com.kizitonwose.calendar.core.WeekDay import java.time.DayOfWeek /** - * A horizontally scrolling calendar. + * A horizontally scrolling month calendar. * * @param modifier the modifier to apply to this calendar. * @param state the state object to be used to control or observe the calendar's properties. @@ -47,7 +47,7 @@ import java.time.DayOfWeek * @param contentPadding a padding around the whole calendar. This will add padding for the * content after it has been clipped, which is not possible via [modifier] param. You can use it * to add a padding before the first month or after the last one. If you want to add a spacing - * between each month use the [monthContainer] composable. + * between each month, use the [monthContainer] composable. * @param contentHeightMode Determines how the height of the day content is calculated. * @param dayContent a composable block which describes the day content. * @param monthHeader a composable block which describes the month header content. The header is @@ -96,7 +96,7 @@ public fun HorizontalCalendar( ) /** - * A vertically scrolling calendar. + * A vertically scrolling month calendar. * * @param modifier the modifier to apply to this calendar. * @param state the state object to be used to control or observe the calendar's properties. @@ -110,7 +110,7 @@ public fun HorizontalCalendar( * @param contentPadding a padding around the whole calendar. This will add padding for the * content after it has been clipped, which is not possible via [modifier] param. You can use it * to add a padding before the first month or after the last one. If you want to add a spacing - * between each month use the [monthContainer] composable. + * between each month, use the [monthContainer] composable. * @param contentHeightMode Determines how the height of the day content is calculated. * @param dayContent a composable block which describes the day content. * @param monthHeader a composable block which describes the month header content. The header is @@ -302,6 +302,62 @@ public fun HeatMapCalendar( contentPadding = contentPadding, ) +/** + * A horizontally scrolling year calendar. + * + * @param modifier the modifier to apply to this calendar. + * @param state the state object to be used to control or observe the calendar's properties. + * Examples: `startYear`, `endYear`, `firstDayOfWeek`, `firstVisibleYear`, `outDateStyle`. + * @param columns the number of months columns in each year on the calendar. + * @param calendarScrollPaged the scrolling behavior of the calendar. When `true`, the calendar will + * snap to the nearest year after a scroll or swipe action. When `false`, the calendar scrolls normally. + * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions + * is allowed. You can still scroll programmatically using the state even when it is disabled. + * @param reverseLayout reverse the direction of scrolling and layout. When `true`, years will be + * composed from the end to the start and [YearCalendarState.startYear] will be located at the end. + * @param contentPadding a padding around the whole calendar. This will add padding for the + * content after it has been clipped, which is not possible via [modifier] param. You can use it + * to add a padding before the first year or after the last one. If you want to add a spacing + * between each year, use the [yearContainer] composable or the [yearBodyContentPadding] parameter. + * @param contentHeightMode Determines how the height of the month and day content is calculated. + * @param monthVerticalArrangement the vertical arrangement of the month rows. Use [Arrangement.spacedBy] + * to add a space between each row. + * @param monthHorizontalArrangement the horizontal arrangement of the month columns. Use [Arrangement.spacedBy] + * to add a space between each column. + * @param yearBodyContentPadding a padding around the year body content. Alternatively, you can + * also provide a [yearBody] with the desired padding to achieve the same result. + * @param isMonthVisible Determines if a month is shown on the calendar grid. For example, you can + * use this to hide all past months. + * @param dayContent a composable block which describes the day content. + * @param monthHeader a composable block which describes the month header content. The header is + * placed above each month on the calendar. + * @param monthBody a composable block which describes the month body content. This is the container + * where all the month days are placed, excluding the header and footer. This is useful if you + * want to customize the day container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param monthFooter a composable block which describes the month footer content. The footer is + * placed below each month on the calendar. + * @param monthContainer a composable block which describes the entire month content. This is the + * container where all the month contents are placed (header => days => footer). This is useful if + * you want to customize the month container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearHeader a composable block which describes the year header content. The header is + * placed above each year on the calendar. + * @param yearBody a composable block which describes the year body content. This is the container + * where all the months in the year are placed, excluding the year header and footer. This is useful + * if you want to customize the month container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearFooter a composable block which describes the year footer content. The footer is + * placed below each year on the calendar. + * @param yearContainer a composable block which describes the entire year content. This is the + * container where all the year contents are placed (header => months => footer). This is useful if + * you want to customize the year container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + */ @ExperimentalCalendarApi @Composable public fun HorizontalYearCalendar( @@ -351,6 +407,62 @@ public fun HorizontalYearCalendar( contentPadding = contentPadding, ) +/** + * A vertically scrolling year calendar. + * + * @param modifier the modifier to apply to this calendar. + * @param state the state object to be used to control or observe the calendar's properties. + * Examples: `startYear`, `endYear`, `firstDayOfWeek`, `firstVisibleYear`, `outDateStyle`. + * @param columns the number of months columns in each year on the calendar. + * @param calendarScrollPaged the scrolling behavior of the calendar. When `true`, the calendar will + * snap to the nearest year after a scroll or swipe action. When `false`, the calendar scrolls normally. + * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions + * is allowed. You can still scroll programmatically using the state even when it is disabled. + * @param reverseLayout reverse the direction of scrolling and layout. When `true`, years will be + * composed from the end to the start and [YearCalendarState.startYear] will be located at the end. + * @param contentPadding a padding around the whole calendar. This will add padding for the + * content after it has been clipped, which is not possible via [modifier] param. You can use it + * to add a padding before the first year or after the last one. If you want to add a spacing + * between each year, use the [yearContainer] composable or the [yearBodyContentPadding] parameter. + * @param contentHeightMode Determines how the height of the month and day content is calculated. + * @param monthVerticalArrangement the vertical arrangement of the month rows. Use [Arrangement.spacedBy] + * to add a space between each row. + * @param monthHorizontalArrangement the horizontal arrangement of the month columns. Use [Arrangement.spacedBy] + * to add a space between each column. + * @param yearBodyContentPadding a padding around the year body content. Alternatively, you can + * also provide a [yearBody] with the desired padding to achieve the same result. + * @param isMonthVisible Determines if a month is shown on the calendar grid. For example, you can + * use this to hide all past months. + * @param dayContent a composable block which describes the day content. + * @param monthHeader a composable block which describes the month header content. The header is + * placed above each month on the calendar. + * @param monthBody a composable block which describes the month body content. This is the container + * where all the month days are placed, excluding the header and footer. This is useful if you + * want to customize the day container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param monthFooter a composable block which describes the month footer content. The footer is + * placed below each month on the calendar. + * @param monthContainer a composable block which describes the entire month content. This is the + * container where all the month contents are placed (header => days => footer). This is useful if + * you want to customize the month container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearHeader a composable block which describes the year header content. The header is + * placed above each year on the calendar. + * @param yearBody a composable block which describes the year body content. This is the container + * where all the months in the year are placed, excluding the year header and footer. This is useful + * if you want to customize the month container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearFooter a composable block which describes the year footer content. The footer is + * placed below each year on the calendar. + * @param yearContainer a composable block which describes the entire year content. This is the + * container where all the year contents are placed (header => months => footer). This is useful if + * you want to customize the year container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + */ @ExperimentalCalendarApi @Composable public fun VerticalYearCalendar( diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt index d7b554ef..f11e66f7 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarState.kt @@ -202,7 +202,7 @@ public class CalendarState internal constructor( } init { - monthDataChanged() // Update monthIndexCount initially. + monthDataChanged() // Update indexCount initially. } private fun monthDataChanged() { diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt index 0650ae40..5b821179 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt @@ -181,7 +181,7 @@ public class HeatMapCalendarState internal constructor( } init { - monthDataChanged() // Update monthIndexCount initially. + monthDataChanged() // Update indexCount initially. } private fun monthDataChanged() { diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt index d8d20595..d8eda40a 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt @@ -6,7 +6,7 @@ import com.kizitonwose.calendar.core.CalendarYear /** * Contains useful information about the currently displayed layout state of the calendar. - * For example you can get the list of currently displayed months. + * For example you can get the list of currently displayed years. * * Use [YearCalendarState.layoutInfo] to retrieve this. * @@ -17,21 +17,21 @@ public class YearCalendarLayoutInfo( private val getIndexData: (Int) -> CalendarYear, ) : LazyListLayoutInfo by info { /** - * The list of [YearCalendarItemInfo] representing all the currently visible weeks. + * The list of [YearCalendarItemInfo] representing all the currently visible years. */ - public val visibleWeeksInfo: List + public val visibleYearsInfo: List get() = visibleItemsInfo.map { info -> YearCalendarItemInfo(info, getIndexData(info.index)) } } /** - * Contains useful information about an individual week on the calendar. + * Contains useful information about an individual year on the calendar. * - * @param week The week in the list. + * @param year The year in the list. * @see YearCalendarLayoutInfo * @see LazyListItemInfo */ -public class YearCalendarItemInfo(info: LazyListItemInfo, public val week: CalendarYear) : +public class YearCalendarItemInfo(info: LazyListItemInfo, public val year: CalendarYear) : LazyListItemInfo by info diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt index b59f6cc3..263f59fb 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.kizitonwose.calendar.compose.CalendarInfo import com.kizitonwose.calendar.compose.CalendarLayoutInfo -import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.VisibleItemState import com.kizitonwose.calendar.core.CalendarYear import com.kizitonwose.calendar.core.ExperimentalCalendarApi @@ -33,38 +32,38 @@ import java.time.DayOfWeek import java.time.Year /** - * Creates a [CalendarState] that is remembered across compositions. + * Creates a [YearCalendarState] that is remembered across compositions. * - * @param startMonth the initial value for [CalendarState.startMonth] - * @param endMonth the initial value for [CalendarState.endMonth] - * @param firstDayOfWeek the initial value for [CalendarState.firstDayOfWeek] - * @param firstVisibleMonth the initial value for [CalendarState.firstVisibleMonth] - * @param outDateStyle the initial value for [CalendarState.outDateStyle] + * @param startYear the initial value for [YearCalendarState.startYear] + * @param endYear the initial value for [YearCalendarState.endYear] + * @param firstDayOfWeek the initial value for [YearCalendarState.firstDayOfWeek] + * @param firstVisibleYear the initial value for [YearCalendarState.firstVisibleYear] + * @param outDateStyle the initial value for [YearCalendarState.outDateStyle] */ @ExperimentalCalendarApi @Composable public fun rememberYearCalendarState( - startMonth: Year = Year.now(), - endMonth: Year = startMonth, - firstVisibleMonth: Year = startMonth, + startYear: Year = Year.now(), + endYear: Year = startYear, + firstVisibleYear: Year = startYear, firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), outDateStyle: OutDateStyle = OutDateStyle.EndOfRow, ): YearCalendarState { return rememberSaveable( inputs = arrayOf( - startMonth, - endMonth, - firstVisibleMonth, + startYear, + endYear, + firstVisibleYear, firstDayOfWeek, outDateStyle, ), saver = YearCalendarState.Saver, ) { YearCalendarState( - startMonth = startMonth, - endMonth = endMonth, + startYear = startYear, + endYear = endYear, firstDayOfWeek = firstDayOfWeek, - firstVisibleMonth = firstVisibleMonth, + firstVisibleYear = firstVisibleYear, outDateStyle = outDateStyle, visibleItemState = null, ) @@ -74,47 +73,47 @@ public fun rememberYearCalendarState( /** * A state object that can be hoisted to control and observe calendar properties. * - * This should be created via [rememberCalendarState]. + * This should be created via [rememberYearCalendarState]. * - * @param startMonth the first month on the calendar. - * @param endMonth the last month on the calendar. + * @param startYear the first month on the calendar. + * @param endYear the last month on the calendar. * @param firstDayOfWeek the first day of week on the calendar. - * @param firstVisibleMonth the initial value for [CalendarState.firstVisibleMonth] + * @param firstVisibleYear the initial value for [YearCalendarState.firstVisibleYear] * @param outDateStyle the preferred style for out date generation. */ @ExperimentalCalendarApi @Stable public class YearCalendarState internal constructor( - startMonth: Year, - endMonth: Year, + startYear: Year, + endYear: Year, firstDayOfWeek: DayOfWeek, - firstVisibleMonth: Year, + firstVisibleYear: Year, outDateStyle: OutDateStyle, visibleItemState: VisibleItemState?, ) : ScrollableState { - /** Backing state for [startMonth] */ - private var _startMonth by mutableStateOf(startMonth) + /** Backing state for [startYear] */ + private var _startYear by mutableStateOf(startYear) - /** The first month on the calendar. */ - public var startMonth: Year - get() = _startMonth + /** The first year on the calendar. */ + public var startYear: Year + get() = _startYear set(value) { - if (value != startMonth) { - _startMonth = value - monthDataChanged() + if (value != startYear) { + _startYear = value + yearDataChanged() } } - /** Backing state for [endMonth] */ - private var _endMonth by mutableStateOf(endMonth) + /** Backing state for [endYear] */ + private var _endYear by mutableStateOf(endYear) - /** The last month on the calendar. */ - public var endMonth: Year - get() = _endMonth + /** The last year on the calendar. */ + public var endYear: Year + get() = _endYear set(value) { - if (value != endMonth) { - _endMonth = value - monthDataChanged() + if (value != endYear) { + _endYear = value + yearDataChanged() } } @@ -127,7 +126,7 @@ public class YearCalendarState internal constructor( set(value) { if (value != firstDayOfWeek) { _firstDayOfWeek = value - monthDataChanged() + yearDataChanged() } } @@ -140,25 +139,25 @@ public class YearCalendarState internal constructor( set(value) { if (value != outDateStyle) { _outDateStyle = value - monthDataChanged() + yearDataChanged() } } /** - * The first month that is visible. + * The first year that is visible. * - * @see [lastVisibleMonth] + * @see [lastVisibleYear] */ - public val firstVisibleMonth: CalendarYear by derivedStateOf { + public val firstVisibleYear: CalendarYear by derivedStateOf { store[listState.firstVisibleItemIndex] } /** - * The last month that is visible. + * The last year that is visible. * - * @see [firstVisibleMonth] + * @see [firstVisibleYear] */ - public val lastVisibleMonth: CalendarYear by derivedStateOf { + public val lastVisibleYear: CalendarYear by derivedStateOf { store[listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0] } @@ -193,7 +192,7 @@ public class YearCalendarState internal constructor( internal val listState = LazyListState( firstVisibleItemIndex = visibleItemState?.firstVisibleItemIndex - ?: getScrollIndex(firstVisibleMonth) ?: 0, + ?: getScrollIndex(firstVisibleYear) ?: 0, firstVisibleItemScrollOffset = visibleItemState?.firstVisibleItemScrollOffset ?: 0, ) @@ -201,7 +200,7 @@ public class YearCalendarState internal constructor( internal val store = DataStore { offset -> getCalendarYearData( - startYear = this.startMonth, + startYear = this.startYear, offset = offset, firstDayOfWeek = this.firstDayOfWeek, outDateStyle = this.outDateStyle, @@ -209,52 +208,52 @@ public class YearCalendarState internal constructor( } init { - monthDataChanged() // Update monthIndexCount initially. + yearDataChanged() // Update indexCount initially. } - private fun monthDataChanged() { + private fun yearDataChanged() { store.clear() - checkRange(startMonth, endMonth) + checkRange(startYear, endYear) // Read the firstDayOfWeek and outDateStyle properties to ensure recomposition // even though they are unused in the CalendarInfo. Alternatively, we could use // mutableStateMapOf() as the backing store for DataStore() to ensure recomposition // but not sure how compose handles recomposition of a lazy list that reads from // such map when an entry unrelated to the visible indices changes. calendarInfo = CalendarInfo( - indexCount = getYearIndicesCount(startMonth, endMonth), + indexCount = getYearIndicesCount(startYear, endYear), firstDayOfWeek = firstDayOfWeek, outDateStyle = outDateStyle, ) } /** - * Instantly brings the [month] to the top of the viewport. + * Instantly brings the [year] to the top of the viewport. * - * @param month the month to which to scroll. Must be within the - * range of [startMonth] and [endMonth]. + * @param year the year to which to scroll. Must be within the + * range of [startYear] and [endYear]. * - * @see [animateScrollToMonth] + * @see [animateScrollToYear] */ - public suspend fun scrollToMonth(month: Year) { - listState.scrollToItem(getScrollIndex(month) ?: return) + public suspend fun scrollToYear(year: Year) { + listState.scrollToItem(getScrollIndex(year) ?: return) } /** - * Animate (smooth scroll) to the given [month]. + * Animate (smooth scroll) to the given [year]. * - * @param month the month to which to scroll. Must be within the - * range of [startMonth] and [endMonth]. + * @param year the year to which to scroll. Must be within the + * range of [startYear] and [endYear]. */ - public suspend fun animateScrollToMonth(month: Year) { - listState.animateScrollToItem(getScrollIndex(month) ?: return) + public suspend fun animateScrollToYear(year: Year) { + listState.animateScrollToItem(getScrollIndex(year) ?: return) } - private fun getScrollIndex(month: Year): Int? { - if (month !in startMonth..endMonth) { - Log.d("CalendarState", "Attempting to scroll out of range: $month") + private fun getScrollIndex(year: Year): Int? { + if (year !in startYear..endYear) { + Log.d("CalendarState", "Attempting to scroll out of range: $year") return null } - return getYearIndex(startMonth, month) + return getYearIndex(startYear, year) } /** @@ -274,9 +273,9 @@ public class YearCalendarState internal constructor( internal val Saver: Saver = listSaver( save = { listOf( - it.startMonth, - it.endMonth, - it.firstVisibleMonth.year, + it.startYear, + it.endYear, + it.firstVisibleYear.year, it.firstDayOfWeek, it.outDateStyle, it.listState.firstVisibleItemIndex, @@ -285,9 +284,9 @@ public class YearCalendarState internal constructor( }, restore = { YearCalendarState( - startMonth = it[0] as Year, - endMonth = it[1] as Year, - firstVisibleMonth = it[2] as Year, + startYear = it[0] as Year, + endYear = it[1] as Year, + firstVisibleYear = it[2] as Year, firstDayOfWeek = it[3] as DayOfWeek, outDateStyle = it[4] as OutDateStyle, visibleItemState = VisibleItemState( diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt index 25b5bcc5..227c72b3 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt @@ -6,25 +6,32 @@ import androidx.compose.foundation.layout.height import androidx.compose.ui.Modifier /** - * Determines how the height of the day content is calculated. + * Determines how the height of the month content is calculated. */ public enum class YearContentHeightMode { /** - * The day container will wrap its height. This allows you to - * use [Modifier.aspectRatio] if you want square day content + * The calendar months and days will wrap content height. This allows + * you to use [Modifier.aspectRatio] if you want square day content * or [Modifier.height] if you want a specific height value * for the day content. */ Wrap, /** - * The days in each month will spread to fill the parent's height after - * any available header and footer content height has been accounted for. - * This allows you to use [Modifier.fillMaxHeight] for the day content - * height. With this option, your Calendar composable should also - * be created with [Modifier.fillMaxHeight] or [Modifier.height]. + * The calendar months will be distributed uniformly to fill the + * parent's height. However, the days within the calendar months will + * wrap content height. This allows you to spread the calendar months + * evenly across the screen while using [Modifier.aspectRatio] if you + * want square day content or [Modifier.height] if you want a specific + * height value for the day content. */ Fill, + /** + * The calendar months and days will uniformly stretch to fill the + * parent's height. This allows you to use [Modifier.fillMaxHeight] for + * the day content height. With this option, your Calendar composable should + * also be created with [Modifier.fillMaxHeight] or [Modifier.height]. + */ Stretch, } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index 3a9a1114..b74665f8 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -76,9 +76,9 @@ fun Example10Page(adjacentYears: Long = 50) { ) { val scope = rememberCoroutineScope() val state = rememberYearCalendarState( - startMonth = startYear, - endMonth = endYear, - firstVisibleMonth = currentYear, + startYear = startYear, + endYear = endYear, + firstVisibleYear = currentYear, firstDayOfWeek = daysOfWeek.first(), ) val visibleYearAfterScroll = rememberFirstVisibleYearAfterScroll(state).year @@ -99,15 +99,15 @@ fun Example10Page(adjacentYears: Long = 50) { visibleYear = targetYear scope.launch { if (abs(ChronoUnit.YEARS.between(visibleYear, targetYear)) <= 8) { - state.animateScrollToMonth(targetYear) + state.animateScrollToYear(targetYear) } else { val nearbyYear = if (targetYear > visibleYear) { targetYear.minusYears(5) } else { targetYear.plusYears(5) } - state.scrollToMonth(nearbyYear) - state.animateScrollToMonth(targetYear) + state.scrollToYear(nearbyYear) + state.animateScrollToYear(targetYear) } } } @@ -116,7 +116,11 @@ fun Example10Page(adjacentYears: Long = 50) { .fillMaxSize() .testTag("Calendar"), state = state, - columns = if (isPortrait) 3 else if (isTablet) 4 else 6, + columns = if (isPortrait) { + 3 + } else { + if (isTablet) 4 else 6 + }, dayContent = { day -> Day( day = day, @@ -131,7 +135,13 @@ fun Example10Page(adjacentYears: Long = 50) { } }, contentHeightMode = YearContentHeightMode.Fill, - monthHorizontalArrangement = Arrangement.spacedBy(if (isTablet) if (isPortrait) 52.dp else 92.dp else 10.dp), + monthHorizontalArrangement = Arrangement.spacedBy( + if (isTablet) { + if (isPortrait) 52.dp else 92.dp + } else { + 10.dp + }, + ), monthVerticalArrangement = Arrangement.spacedBy(if (isTablet) 20.dp else 4.dp), yearBodyContentPadding = if (isTablet) { PaddingValues(horizontal = if (isPortrait) 52.dp else 92.dp, vertical = 20.dp) @@ -275,5 +285,8 @@ private fun Example10Preview() { } private val headerBackground = Color(0xFFF1F1F1) -private fun simpleTextColor(isSelected: Boolean) = if (isSelected) darkColors().onSurface else lightColors().onSurface -private fun simpleTextBackground(isSelected: Boolean) = if (isSelected) darkColors().surface else lightColors().surface +private fun simpleTextColor(isSelected: Boolean) = + if (isSelected) darkColors().onSurface else lightColors().onSurface + +private fun simpleTextBackground(isSelected: Boolean) = + if (isSelected) darkColors().surface else lightColors().surface diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt index 14f0cd8a..ac63a730 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt @@ -60,9 +60,9 @@ fun Example11Page(adjacentMonths: Long = 50) { .background(Color.White), ) { val state = rememberYearCalendarState( - startMonth = currentYear, - endMonth = endYear, - firstVisibleMonth = currentYear, + startYear = currentYear, + endYear = endYear, + firstVisibleYear = currentYear, firstDayOfWeek = daysOfWeek.first(), ) VerticalYearCalendar( @@ -166,7 +166,7 @@ private fun Day( .padding(if (isTablet) 2.dp else 0.dp) .clip(CircleShape) .background(color = if (isSelected) colorResource(R.color.example_1_selection_color) else Color.Transparent) -// Disable clicks on inDates/outDates + // Disable clicks on inDates/outDates .clickable( enabled = day.position == DayPosition.MonthDate, showRipple = !isSelected, @@ -181,7 +181,6 @@ private fun Day( color = if (isSelected) Color.White else Color.Unspecified, ) } - } } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index 1a729a8b..73b9025b 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -173,7 +173,7 @@ fun rememberFirstMostVisibleYear( state: YearCalendarState, viewportPercent: Float = 50f, ): CalendarYear { - val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) } + val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleYear) } LaunchedEffect(state) { snapshotFlow { state.layoutInfo.firstMostVisibleYear(viewportPercent) } .filterNotNull() @@ -190,11 +190,11 @@ fun rememberFirstMostVisibleYear( @OptIn(ExperimentalCalendarApi::class) @Composable fun rememberFirstVisibleYearAfterScroll(state: YearCalendarState): CalendarYear { - val visibleYear = remember(state) { mutableStateOf(state.firstVisibleMonth) } + val visibleYear = remember(state) { mutableStateOf(state.firstVisibleYear) } LaunchedEffect(state) { snapshotFlow { state.isScrollInProgress } .filter { scrolling -> !scrolling } - .collect { visibleYear.value = state.firstVisibleMonth } + .collect { visibleYear.value = state.firstVisibleYear } } return visibleYear.value } @@ -234,17 +234,17 @@ private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50 } private fun YearCalendarLayoutInfo.firstMostVisibleYear(viewportPercent: Float = 50f): CalendarYear? { - return if (visibleWeeksInfo.isEmpty()) { + return if (visibleYearsInfo.isEmpty()) { null } else { val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f - visibleWeeksInfo.firstOrNull { itemInfo -> + visibleYearsInfo.firstOrNull { itemInfo -> if (itemInfo.offset < 0) { itemInfo.offset + itemInfo.size >= viewportSize } else { itemInfo.size - itemInfo.offset >= viewportSize } - }?.week + }?.year } } From 0b83e4521e1e8aabfa2c2f81ef020983f3ee4e24 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 21 Jul 2024 16:56:23 +0200 Subject: [PATCH 05/48] Update dependencies --- .editorconfig | 3 +++ gradle/libs.versions.toml | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.editorconfig b/.editorconfig index d9e3ecad..7efbeee7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,6 +13,9 @@ ktlint = disabled ij_kotlin_allow_trailing_comma = true ij_kotlin_allow_trailing_comma_on_call_site = true ktlint_standard_multiline-expression-wrapping = disabled +ktlint_standard_function-expression-body = disabled +ktlint_standard_chain-method-continuation = disabled +ktlint_standard_class-signature = disabled # string-template-indent requires multiline-expression-wrapping to be enabled ktlint_standard_string-template-indent = disabled ktlint_standard_parameter-list-wrapping = disabled diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 75d3da56..df226c6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [versions] -agp = "8.5.0" +agp = "8.5.1" kotlin = "2.0.0" -compose = "1.7.0-beta03" -espresso = "3.5.1" +compose = "1.7.0-beta05" +espresso = "3.6.1" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -18,9 +18,9 @@ material-view = { module = "com.google.android.material:material", version = "1. androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espresso" } -androidx-test-runner = { module = "androidx.test:runner", version = "1.5.2" } -androidx-test-rules = { module = "androidx.test:rules", version = "1.5.0" } -androidx-test-junit = { module = "androidx.test.ext:junit", version = "1.1.5" } +androidx-test-runner = { module = "androidx.test:runner", version = "1.6.1" } +androidx-test-rules = { module = "androidx.test:rules", version = "1.6.1" } +androidx-test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } test-junit = { module = "junit:junit", version = "4.13.2" } compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } @@ -42,8 +42,8 @@ androidLibrary = { id = "com.android.library", version.ref = "agp" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlinter = { id = "org.jmailen.kotlinter", version = "4.3.0" } -mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.28.0" } +kotlinter = { id = "org.jmailen.kotlinter", version = "4.4.1" } +mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.29.0" } versionCheck = { id = "com.github.ben-manes.versions", version = "0.51.0" } bcv = "org.jetbrains.kotlinx.binary-compatibility-validator:0.15.1" From 3d84e4bd69cf6008e7be87a14d6a9b8b294f6e01 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 21 Jul 2024 17:21:26 +0200 Subject: [PATCH 06/48] Improve year sample --- .../java/com/kizitonwose/calendar/core/CalendarYear.kt | 6 +++--- .../kizitonwose/calendar/sample/compose/Example10Page.kt | 7 +------ .../com/kizitonwose/calendar/sample/compose/ListPage.kt | 4 ++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt b/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt index d902a8d9..1f785a0e 100644 --- a/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt +++ b/core/src/main/java/com/kizitonwose/calendar/core/CalendarYear.kt @@ -5,10 +5,10 @@ import java.io.Serializable import java.time.Year /** - * Represents a month on the calendar. + * Represents a year on the calendar. * - * @param yearMonth the calendar month value. - * @param weekDays the weeks in this month. + * @param year the calendar year value. + * @param months the months in this year. */ @Immutable public data class CalendarYear( diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index b74665f8..b9bf02d8 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -23,12 +23,9 @@ import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -81,8 +78,7 @@ fun Example10Page(adjacentYears: Long = 50) { firstVisibleYear = currentYear, firstDayOfWeek = daysOfWeek.first(), ) - val visibleYearAfterScroll = rememberFirstVisibleYearAfterScroll(state).year - var visibleYear by remember(visibleYearAfterScroll) { mutableStateOf(visibleYearAfterScroll) } + val visibleYear = rememberFirstVisibleYearAfterScroll(state).year val headerState = rememberLazyListState() LaunchedEffect(visibleYear) { val index = ChronoUnit.YEARS.between(startYear, visibleYear).toInt() @@ -96,7 +92,6 @@ fun Example10Page(adjacentYears: Long = 50) { isTablet = isTablet, ) click@{ targetYear -> if (targetYear == visibleYear) return@click - visibleYear = targetYear scope.launch { if (abs(ChronoUnit.YEARS.between(visibleYear, targetYear)) <= 8) { state.animateScrollToYear(targetYear) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt index b0b68e7c..9f1fdfbc 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt @@ -73,12 +73,12 @@ enum class Page(val title: String, val subtitle: String, val showToolBar: Boolea ), Example10( title = "Example 10", - subtitle = "Horizontal year calendar - Year header and paged scrolling. Best suited for large displays.", + subtitle = "Horizontal year calendar - Year header and paged scrolling. Best suited for large screens.", showToolBar = true, ), Example11( title = "Example 11", - subtitle = "Vertical year calendar - Hidden past months with continuous scroll. Best used on large displays.", + subtitle = "Vertical year calendar - Hidden past months with continuous scroll. Best suited for large screens.", showToolBar = true, ), } From 712457766d9e369915358f42fe4ef5bf8031b4c1 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sun, 21 Jul 2024 18:52:57 +0200 Subject: [PATCH 07/48] Change horizontal and vertical arrangement parameters to spacing. --- .../kizitonwose/calendar/compose/Calendar.kt | 62 +++++++++---------- .../yearcalendar/YearCalendarMonths.kt | 19 +++--- .../calendar/sample/compose/Example10Page.kt | 14 ++--- .../calendar/sample/compose/Example11Page.kt | 4 +- 4 files changed, 47 insertions(+), 52 deletions(-) diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt index e016b2eb..1baf8df2 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt @@ -1,6 +1,5 @@ package com.kizitonwose.calendar.compose -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -10,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarImpl @@ -319,13 +319,11 @@ public fun HeatMapCalendar( * content after it has been clipped, which is not possible via [modifier] param. You can use it * to add a padding before the first year or after the last one. If you want to add a spacing * between each year, use the [yearContainer] composable or the [yearBodyContentPadding] parameter. - * @param contentHeightMode Determines how the height of the month and day content is calculated. - * @param monthVerticalArrangement the vertical arrangement of the month rows. Use [Arrangement.spacedBy] - * to add a space between each row. - * @param monthHorizontalArrangement the horizontal arrangement of the month columns. Use [Arrangement.spacedBy] - * to add a space between each column. * @param yearBodyContentPadding a padding around the year body content. Alternatively, you can * also provide a [yearBody] with the desired padding to achieve the same result. + * @param monthVerticalSpacing the vertical spacing between month rows. + * @param monthHorizontalSpacing the horizontal spacing between month columns. + * @param contentHeightMode Determines how the height of the month and day content is calculated. * @param isMonthVisible Determines if a month is shown on the calendar grid. For example, you can * use this to hide all past months. * @param dayContent a composable block which describes the day content. @@ -368,10 +366,10 @@ public fun HorizontalYearCalendar( userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), - contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, - monthVerticalArrangement: Arrangement.Vertical = Arrangement.Top, - monthHorizontalArrangement: Arrangement.Horizontal = Arrangement.Start, yearBodyContentPadding: PaddingValues = PaddingValues(0.dp), + monthVerticalSpacing: Dp = 0.dp, + monthHorizontalSpacing: Dp = 0.dp, + contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, isMonthVisible: (month: CalendarMonth) -> Boolean = remember { { true } }, dayContent: @Composable BoxScope.(CalendarDay) -> Unit, monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, @@ -390,11 +388,12 @@ public fun HorizontalYearCalendar( userScrollEnabled = userScrollEnabled, isHorizontal = true, reverseLayout = reverseLayout, + contentPadding = contentPadding, + yearBodyContentPadding = yearBodyContentPadding, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, contentHeightMode = contentHeightMode, isMonthVisible = isMonthVisible, - monthVerticalArrangement = monthVerticalArrangement, - monthHorizontalArrangement = monthHorizontalArrangement, - yearBodyContentPadding = yearBodyContentPadding, dayContent = dayContent, monthHeader = monthHeader, monthBody = monthBody, @@ -404,7 +403,6 @@ public fun HorizontalYearCalendar( yearBody = yearBody, yearFooter = yearFooter, yearContainer = yearContainer, - contentPadding = contentPadding, ) /** @@ -424,13 +422,11 @@ public fun HorizontalYearCalendar( * content after it has been clipped, which is not possible via [modifier] param. You can use it * to add a padding before the first year or after the last one. If you want to add a spacing * between each year, use the [yearContainer] composable or the [yearBodyContentPadding] parameter. - * @param contentHeightMode Determines how the height of the month and day content is calculated. - * @param monthVerticalArrangement the vertical arrangement of the month rows. Use [Arrangement.spacedBy] - * to add a space between each row. - * @param monthHorizontalArrangement the horizontal arrangement of the month columns. Use [Arrangement.spacedBy] - * to add a space between each column. * @param yearBodyContentPadding a padding around the year body content. Alternatively, you can * also provide a [yearBody] with the desired padding to achieve the same result. + * @param monthVerticalSpacing the vertical spacing between month rows. + * @param monthHorizontalSpacing the horizontal spacing between month columns. + * @param contentHeightMode Determines how the height of the month and day content is calculated. * @param isMonthVisible Determines if a month is shown on the calendar grid. For example, you can * use this to hide all past months. * @param dayContent a composable block which describes the day content. @@ -473,10 +469,10 @@ public fun VerticalYearCalendar( userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), - contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, - monthVerticalArrangement: Arrangement.Vertical = Arrangement.Top, - monthHorizontalArrangement: Arrangement.Horizontal = Arrangement.Start, yearBodyContentPadding: PaddingValues = PaddingValues(0.dp), + monthVerticalSpacing: Dp = 0.dp, + monthHorizontalSpacing: Dp = 0.dp, + contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, isMonthVisible: (month: CalendarMonth) -> Boolean = remember { { true } }, dayContent: @Composable BoxScope.(CalendarDay) -> Unit, monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, @@ -495,10 +491,11 @@ public fun VerticalYearCalendar( userScrollEnabled = userScrollEnabled, isHorizontal = false, reverseLayout = reverseLayout, - contentHeightMode = contentHeightMode, - monthVerticalArrangement = monthVerticalArrangement, - monthHorizontalArrangement = monthHorizontalArrangement, + contentPadding = contentPadding, yearBodyContentPadding = yearBodyContentPadding, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + contentHeightMode = contentHeightMode, isMonthVisible = isMonthVisible, dayContent = dayContent, monthHeader = monthHeader, @@ -509,7 +506,6 @@ public fun VerticalYearCalendar( yearBody = yearBody, yearFooter = yearFooter, yearContainer = yearContainer, - contentPadding = contentPadding, ) @ExperimentalCalendarApi @@ -524,8 +520,8 @@ private fun YearCalendar( reverseLayout: Boolean, contentPadding: PaddingValues, contentHeightMode: YearContentHeightMode, - monthVerticalArrangement: Arrangement.Vertical, - monthHorizontalArrangement: Arrangement.Horizontal, + monthVerticalSpacing: Dp, + monthHorizontalSpacing: Dp, yearBodyContentPadding: PaddingValues, isMonthVisible: (month: CalendarMonth) -> Boolean, dayContent: @Composable BoxScope.(CalendarDay) -> Unit, @@ -551,10 +547,10 @@ private fun YearCalendar( yearCount = state.calendarInfo.indexCount, yearData = { offset -> state.store[offset] }, columns = columns, - contentHeightMode = contentHeightMode, - monthVerticalArrangement = monthVerticalArrangement, - monthHorizontalArrangement = monthHorizontalArrangement, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, yearBodyContentPadding = yearBodyContentPadding, + contentHeightMode = contentHeightMode, isMonthVisible = isMonthVisible, dayContent = dayContent, monthHeader = monthHeader, @@ -580,10 +576,10 @@ private fun YearCalendar( yearCount = state.calendarInfo.indexCount, yearData = { offset -> state.store[offset] }, columns = columns, - contentHeightMode = contentHeightMode, - monthVerticalArrangement = monthVerticalArrangement, - monthHorizontalArrangement = monthHorizontalArrangement, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, yearBodyContentPadding = yearBodyContentPadding, + contentHeightMode = contentHeightMode, isMonthVisible = isMonthVisible, dayContent = dayContent, monthHeader = monthHeader, diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt index ddb30ead..3c2bbe80 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.unit.Dp import com.kizitonwose.calendar.compose.or import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth @@ -26,10 +27,10 @@ internal fun LazyListScope.YearCalendarMonths( yearCount: Int, yearData: (offset: Int) -> CalendarYear, columns: Int, - contentHeightMode: YearContentHeightMode, - monthVerticalArrangement: Arrangement.Vertical, - monthHorizontalArrangement: Arrangement.Horizontal, + monthVerticalSpacing: Dp, + monthHorizontalSpacing: Dp, yearBodyContentPadding: PaddingValues, + contentHeightMode: YearContentHeightMode, isMonthVisible: (month: CalendarMonth) -> Boolean, dayContent: @Composable BoxScope.(CalendarDay) -> Unit, monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, @@ -76,8 +77,8 @@ internal fun LazyListScope.YearCalendarMonths( columns = columns, itemCount = months.count(), fillHeight = fillHeight, - monthVerticalArrangement = monthVerticalArrangement, - monthHorizontalArrangement = monthHorizontalArrangement, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, ) { monthOffset -> val month = months[monthOffset] val hasContainer = monthContainer != null @@ -140,15 +141,15 @@ internal fun LazyListScope.YearCalendarMonths( private fun CalendarGrid( columns: Int, fillHeight: Boolean, - monthVerticalArrangement: Arrangement.Vertical, - monthHorizontalArrangement: Arrangement.Horizontal, + monthVerticalSpacing: Dp, + monthHorizontalSpacing: Dp, itemCount: Int, modifier: Modifier = Modifier, content: @Composable BoxScope.(Int) -> Unit, ) { Column( modifier = modifier, - verticalArrangement = monthVerticalArrangement, + verticalArrangement = Arrangement.spacedBy(monthVerticalSpacing), ) { var rows = (itemCount / columns) if (itemCount.mod(columns) > 0) { @@ -162,7 +163,7 @@ private fun CalendarGrid( modifier = Modifier.then( if (fillHeight) Modifier.weight(1f) else Modifier, ), - horizontalArrangement = monthHorizontalArrangement, + horizontalArrangement = Arrangement.spacedBy(monthHorizontalSpacing), ) { for (columnId in 0 until columns) { val index = firstIndex + columnId diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index b9bf02d8..830b5ffa 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -130,14 +130,12 @@ fun Example10Page(adjacentYears: Long = 50) { } }, contentHeightMode = YearContentHeightMode.Fill, - monthHorizontalArrangement = Arrangement.spacedBy( - if (isTablet) { - if (isPortrait) 52.dp else 92.dp - } else { - 10.dp - }, - ), - monthVerticalArrangement = Arrangement.spacedBy(if (isTablet) 20.dp else 4.dp), + monthHorizontalSpacing = if (isTablet) { + if (isPortrait) 52.dp else 92.dp + } else { + 10.dp + }, + monthVerticalSpacing = if (isTablet) 20.dp else 4.dp, yearBodyContentPadding = if (isTablet) { PaddingValues(horizontal = if (isPortrait) 52.dp else 92.dp, vertical = 20.dp) } else { diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt index ac63a730..5b1c001c 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt @@ -85,8 +85,8 @@ fun Example11Page(adjacentMonths: Long = 50) { }, calendarScrollPaged = false, contentHeightMode = YearContentHeightMode.Wrap, - monthVerticalArrangement = Arrangement.spacedBy(20.dp), - monthHorizontalArrangement = Arrangement.spacedBy(if (isTablet) 52.dp else 10.dp), + monthVerticalSpacing = 20.dp, + monthHorizontalSpacing = if (isTablet) 52.dp else 10.dp, contentPadding = PaddingValues(horizontal = if (isTablet) 52.dp else 10.dp), isMonthVisible = { it.yearMonth >= currentMonth From ba1d81029d78606368147bb7b1c725605ecf51e5 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 22 Jul 2024 10:44:12 +0200 Subject: [PATCH 08/48] Add year data tests --- .../calendar/data/DayOfWeekTests.kt | 2 +- .../com/kizitonwose/calendar/data/Utils.kt | 7 ++ .../calendar/data/YearDataTests.kt | 89 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 data/src/test/java/com/kizitonwose/calendar/data/Utils.kt create mode 100644 data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt diff --git a/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt index 0b57ed5c..d1ec782d 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt @@ -20,7 +20,7 @@ class DayOfWeekTests { @Test fun `first day of the week works as expected`() { - DayOfWeek.values().forEach { dayOfWeek -> + DayOfWeek.entries.forEach { dayOfWeek -> assertEquals(dayOfWeek, daysOfWeek(firstDayOfWeek = dayOfWeek).first()) } } diff --git a/data/src/test/java/com/kizitonwose/calendar/data/Utils.kt b/data/src/test/java/com/kizitonwose/calendar/data/Utils.kt new file mode 100644 index 00000000..5138abb7 --- /dev/null +++ b/data/src/test/java/com/kizitonwose/calendar/data/Utils.kt @@ -0,0 +1,7 @@ +package com.kizitonwose.calendar.data + +import java.time.DayOfWeek +import java.time.YearMonth +import java.time.temporal.WeekFields + +fun YearMonth.weeksInMonth(firstDayOfWeek: DayOfWeek) = atEndOfMonth().get(WeekFields.of(firstDayOfWeek, 1).weekOfMonth()) diff --git a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt new file mode 100644 index 00000000..52cb17ce --- /dev/null +++ b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt @@ -0,0 +1,89 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.OutDateStyle +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.DayOfWeek +import java.time.Month +import java.time.Month.JANUARY +import java.time.Year +import java.time.YearMonth + +class YearDataTests { + + @Test + fun `year data is accurate with non-leap year`() { + val year = Year.of(2019) + val firstDayOfWeek = DayOfWeek.MONDAY + val outDateStyle = OutDateStyle.EndOfRow + val yearData = getCalendarYearData(year, 0, firstDayOfWeek, outDateStyle) + val months = yearData.months + val days = yearData.months.flatMap { it.weekDays }.flatten() + yearData.months.forEachIndexed { index, month -> + val monthData = getCalendarMonthData( + startMonth = YearMonth.of(year.value, JANUARY), + offset = month.yearMonth.month.ordinal, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ) + assertEquals(monthData.calendarMonth, month) + assertEquals(Month.entries[JANUARY.ordinal + index], month.yearMonth.month) + assertEquals(month.yearMonth.weeksInMonth(firstDayOfWeek), month.weekDays.count()) + } + assertEquals(12, months.count()) + assertEquals(year, yearData.year) + assertEquals(36, days.count { it.position == DayPosition.InDate }) + assertEquals(33, days.count { it.position == DayPosition.OutDate }) + assertEquals(365, days.count { it.position == DayPosition.MonthDate }) + } + + @Test + fun `year data is accurate with leap year`() { + val year = Year.of(2020) + val firstDayOfWeek = DayOfWeek.SUNDAY + val outDateStyle = OutDateStyle.EndOfGrid + val yearData = getCalendarYearData(year, 0, firstDayOfWeek, outDateStyle) + val months = yearData.months + val days = yearData.months.flatMap { it.weekDays }.flatten() + yearData.months.forEachIndexed { index, month -> + val monthData = getCalendarMonthData( + startMonth = YearMonth.of(year.value, JANUARY), + offset = month.yearMonth.month.ordinal, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ) + assertEquals(monthData.calendarMonth, month) + assertEquals(Month.entries[JANUARY.ordinal + index], month.yearMonth.month) + assertEquals(6, month.weekDays.count()) + val weeksWithoutGridOutDates = month.weekDays.filterNot { week -> week.all { it.position == DayPosition.OutDate } } + assertEquals(month.yearMonth.weeksInMonth(firstDayOfWeek), weeksWithoutGridOutDates.count()) + } + assertEquals(12, months.count()) + assertEquals(year, yearData.year) + assertEquals(35, days.count { it.position == DayPosition.InDate }) + assertEquals(103, days.count { it.position == DayPosition.OutDate }) + assertEquals(366, days.count { it.position == DayPosition.MonthDate }) + } + + @Test + fun `generated year is at the correct offset`() { + val yearData = getCalendarYearData(Year.of(2020), 6, DayOfWeek.SUNDAY, OutDateStyle.EndOfGrid) + + assertEquals(yearData.year, Year.of(2026)) + } + + @Test + fun `year index calculation works as expected`() { + val index = getYearIndex(startYear = Year.of(2020), targetYear = Year.of(2030)) + + assertEquals(10, index) + } + + @Test + fun `index indices count calculation works as expected`() { + val count = getYearIndicesCount(startYear = Year.of(2020), endYear = Year.of(2040)) + + assertEquals(21, count) + } +} From 7b452811fab92714f10898a28e1ab28817142b94 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 22 Jul 2024 11:18:21 +0200 Subject: [PATCH 09/48] Use JUnit5 --- build.gradle.kts | 2 +- compose/build.gradle.kts | 3 ++- .../com/kizitonwose/calendar/compose/CalendarStateTests.kt | 4 ++-- .../calendar/compose/HeatMapCalendarStateTests.kt | 4 ++-- .../com/kizitonwose/calendar/compose/StateSaverTests.kt | 4 ++-- .../kizitonwose/calendar/compose/WeekCalendarStateTests.kt | 6 +++--- data/build.gradle.kts | 3 ++- .../java/com/kizitonwose/calendar/data/DayOfWeekTests.kt | 4 ++-- .../java/com/kizitonwose/calendar/data/HeatMapDataTests.kt | 6 +++--- .../java/com/kizitonwose/calendar/data/MonthDataTests.kt | 6 +++--- .../java/com/kizitonwose/calendar/data/WeekDataTests.kt | 6 +++--- .../java/com/kizitonwose/calendar/data/YearDataTests.kt | 4 ++-- gradle/libs.versions.toml | 6 +++++- sample/build.gradle.kts | 2 +- 14 files changed, 33 insertions(+), 27 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index dd8a9e78..e6cd5c3d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,8 +29,8 @@ allprojects { } } } - tasks.withType().configureEach { + useJUnitPlatform() // https://docs.gradle.org/8.8/userguide/performance.html#execute_tests_in_parallel maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) } diff --git a/compose/build.gradle.kts b/compose/build.gradle.kts index bd55c5ec..7529aa2e 100644 --- a/compose/build.gradle.kts +++ b/compose/build.gradle.kts @@ -41,7 +41,8 @@ dependencies { implementation(libs.compose.foundation) implementation(libs.compose.runtime) - testImplementation(libs.test.junit) + testImplementation(libs.test.junit5.api) + testRuntimeOnly(libs.test.junit5.engine) } mavenPublishing { diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt index 7c7eb0aa..83c711f8 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt @@ -2,8 +2,8 @@ package com.kizitonwose.calendar.compose import com.kizitonwose.calendar.core.OutDateStyle import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import junit.framework.TestCase.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt index 9c6367a9..e6ea9061 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt @@ -2,8 +2,8 @@ package com.kizitonwose.calendar.compose import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import junit.framework.TestCase.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt index 02417ca3..9ec1495f 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt @@ -6,8 +6,8 @@ import androidx.compose.runtime.saveable.listSaver import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState import com.kizitonwose.calendar.core.OutDateStyle -import org.junit.Assert.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt index 15bf885a..a0236f0b 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt @@ -2,9 +2,9 @@ package com.kizitonwose.calendar.compose import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertTrue -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate diff --git a/data/build.gradle.kts b/data/build.gradle.kts index c3150500..2f500ddf 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -23,7 +23,8 @@ dependencies { implementation(project(":core")) implementation(libs.kotlin.stdlib) - testImplementation(libs.test.junit) + testImplementation(libs.test.junit5.api) + testRuntimeOnly(libs.test.junit5.engine) } mavenPublishing { diff --git a/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt index d1ec782d..b4d847f1 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt @@ -1,8 +1,8 @@ package com.kizitonwose.calendar.data import com.kizitonwose.calendar.core.daysOfWeek -import org.junit.Assert.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import java.time.DayOfWeek class DayOfWeekTests { diff --git a/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt index 408a0d10..b9a4e6eb 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt @@ -5,9 +5,9 @@ import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.core.nextMonth import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.core.yearMonth -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.Month.DECEMBER import java.time.Month.NOVEMBER diff --git a/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt index 71a0e1f8..21e09393 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt @@ -6,9 +6,9 @@ import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.core.nextMonth import com.kizitonwose.calendar.core.previousMonth import com.kizitonwose.calendar.core.yearMonth -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.Month.MAY import java.time.Month.NOVEMBER diff --git a/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt index deea9e77..435a733c 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt @@ -2,9 +2,9 @@ package com.kizitonwose.calendar.data import com.kizitonwose.calendar.core.WeekDayPosition import com.kizitonwose.calendar.core.daysOfWeek -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate import java.time.Month.APRIL diff --git a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt index 52cb17ce..9f3ec65c 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt @@ -2,8 +2,8 @@ package com.kizitonwose.calendar.data import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.OutDateStyle -import org.junit.Assert.assertEquals -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.Month import java.time.Month.JANUARY diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df226c6e..6d24d95e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ agp = "8.5.1" kotlin = "2.0.0" compose = "1.7.0-beta05" espresso = "3.6.1" +junit5 = "5.10.3" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } @@ -21,7 +22,10 @@ androidx-test-espresso-contrib = { module = "androidx.test.espresso:espresso-con androidx-test-runner = { module = "androidx.test:runner", version = "1.6.1" } androidx-test-rules = { module = "androidx.test:rules", version = "1.6.1" } androidx-test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } -test-junit = { module = "junit:junit", version = "4.13.2" } +test-junit4 = { module = "junit:junit", version = "4.13.2" } +test-junit5-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } +test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } +test-junit5-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index ee357143..f425f849 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -63,7 +63,7 @@ dependencies { implementation(libs.compose.activity) implementation(libs.compose.navigation) - testImplementation(libs.test.junit) + testImplementation(libs.test.junit4) androidTestImplementation(libs.androidx.test.espresso.core) androidTestImplementation(libs.androidx.test.espresso.contrib) // RecyclerView actions. From 651aca43006d8fa9a83ab5f423933a4504b325d4 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 22 Jul 2024 15:22:41 +0200 Subject: [PATCH 10/48] Add year calendar state tests --- .../kizitonwose/calendar/compose/Calendar.kt | 1 - .../compose/yearcalendar/YearCalendarState.kt | 1 - .../calendar/compose/StateSaverTests.kt | 31 +++++- .../compose/YearCalendarStateTests.kt | 103 ++++++++++++++++++ .../calendar/sample/compose/Example10Page.kt | 4 +- .../calendar/sample/compose/Utils.kt | 3 - 6 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt index 1baf8df2..81a50ddc 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/Calendar.kt @@ -508,7 +508,6 @@ public fun VerticalYearCalendar( yearContainer = yearContainer, ) -@ExperimentalCalendarApi @Composable private fun YearCalendar( modifier: Modifier, diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt index 263f59fb..e8043213 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -81,7 +81,6 @@ public fun rememberYearCalendarState( * @param firstVisibleYear the initial value for [YearCalendarState.firstVisibleYear] * @param outDateStyle the preferred style for out date generation. */ -@ExperimentalCalendarApi @Stable public class YearCalendarState internal constructor( startYear: Year, diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt index 9ec1495f..fb43a0d0 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt @@ -5,11 +5,13 @@ import androidx.compose.runtime.saveable.SaverScope import androidx.compose.runtime.saveable.listSaver import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState import com.kizitonwose.calendar.core.OutDateStyle import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate +import java.time.Year import java.time.YearMonth /** @@ -22,12 +24,13 @@ class StateSaverTests { @Test fun `month calendar state can be restored`() { val now = YearMonth.now() - val firstDayOfWeek = DayOfWeek.values().random() + val firstDayOfWeek = DayOfWeek.entries.random() + val outDateStyle = OutDateStyle.entries.random() val state = CalendarState( startMonth = now, endMonth = now, firstVisibleMonth = now, - outDateStyle = OutDateStyle.EndOfRow, + outDateStyle = outDateStyle, firstDayOfWeek = firstDayOfWeek, visibleItemState = VisibleItemState(), ) @@ -42,7 +45,7 @@ class StateSaverTests { @Test fun `week calendar state can be restored`() { val now = LocalDate.now() - val firstDayOfWeek = DayOfWeek.values().random() + val firstDayOfWeek = DayOfWeek.entries.random() val state = WeekCalendarState( startDate = now, endDate = now, @@ -60,7 +63,7 @@ class StateSaverTests { @Test fun `heatmap calendar state can be restored`() { val now = YearMonth.now() - val firstDayOfWeek = DayOfWeek.values().random() + val firstDayOfWeek = DayOfWeek.entries.random() val state = HeatMapCalendarState( startMonth = now, endMonth = now, @@ -75,6 +78,26 @@ class StateSaverTests { assertEquals(state.firstDayOfWeek, restored.firstDayOfWeek) } + @Test + fun `year calendar state can be restored`() { + val now = Year.now() + val firstDayOfWeek = DayOfWeek.entries.random() + val outDateStyle = OutDateStyle.entries.random() + val state = YearCalendarState( + startYear = now, + endYear = now, + firstVisibleYear = now, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + visibleItemState = VisibleItemState(), + ) + val restored = restore(state, YearCalendarState.Saver) + assertEquals(state.startYear, restored.startYear) + assertEquals(state.endYear, restored.endYear) + assertEquals(state.firstVisibleYear, restored.firstVisibleYear) + assertEquals(state.firstDayOfWeek, restored.firstDayOfWeek) + } + private fun restore(value: State, saver: Saver): State { with(saver) { val saved = SaverScope { true }.save(value)!! diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt new file mode 100644 index 00000000..5a26da44 --- /dev/null +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt @@ -0,0 +1,103 @@ +package com.kizitonwose.calendar.compose + +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.Month +import java.time.Year + +class YearCalendarStateTests { + @Test + fun `start year update is reflected in the state`() { + val now = Year.now() + val updatedStartYear = now.minusYears(8) + val state = createState( + startYear = now, + endYear = now, + ) + + assertEquals(state.store[0].year, now) + + state.startYear = updatedStartYear + + assertEquals(state.store[0].year, updatedStartYear) + } + + @Test + fun `end year update is reflected in the state`() { + val now = Year.now() + val updatedEndMonth = now.plusYears(8) + val state = createState( + startYear = now, + endYear = now, + ) + + assertEquals(state.store[0].year, now) + + state.endYear = updatedEndMonth + + assertEquals(state.store[8].year, updatedEndMonth) + } + + @Test + fun `first day of the week update is reflected in the state`() { + val firstDayOfWeek = LocalDate.now().dayOfWeek + + val state = createState(firstDayOfWeek = firstDayOfWeek) + + state.store[0].months + .flatMap { month -> month.weekDays } + .forEach { week -> + assertEquals(week.first().date.dayOfWeek, firstDayOfWeek) + } + do { + val updatedFirstDayOfWeek = state.firstDayOfWeek.plus(1) + state.firstDayOfWeek = updatedFirstDayOfWeek + + state.store[0].months + .flatMap { month -> month.weekDays } + .forEach { week -> + assertEquals(week.first().date.dayOfWeek, updatedFirstDayOfWeek) + } + } while (firstDayOfWeek != state.firstDayOfWeek) + } + + @Test + fun `out date style update is reflected in the state`() { + val outDateStyle = OutDateStyle.EndOfRow + // Nov 2022 has 5 weeks when Sun is the first day. + val startYear = Year.of(2022) + val state = createState( + startYear = startYear, + endYear = startYear, + outDateStyle = outDateStyle, + firstDayOfWeek = DayOfWeek.SUNDAY, + ) + + assertEquals(state.store[0].months[Month.NOVEMBER.ordinal].weekDays.count(), 5) + + state.outDateStyle = OutDateStyle.EndOfGrid + + assertEquals(state.store[0].months[Month.NOVEMBER.ordinal].weekDays.count(), 6) + } + + private fun createState( + startYear: Year = Year.now(), + endYear: Year = startYear, + firstVisibleYear: Year = startYear, + outDateStyle: OutDateStyle = OutDateStyle.EndOfRow, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + visibleItemState: VisibleItemState = VisibleItemState(), + ) = YearCalendarState( + startYear = startYear, + endYear = endYear, + firstVisibleYear = firstVisibleYear, + outDateStyle = outDateStyle, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = visibleItemState, + ) +} diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index 830b5ffa..902b3d3f 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -50,15 +50,13 @@ import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.sample.shared.displayText import kotlinx.coroutines.launch import java.time.Year -import java.time.YearMonth import java.time.temporal.ChronoUnit import kotlin.math.abs @OptIn(ExperimentalCalendarApi::class) @Composable fun Example10Page(adjacentYears: Long = 50) { - val currentMonth = remember { YearMonth.now() } - val currentYear = remember { Year.of(currentMonth.year) } + val currentYear = remember { Year.now() } val startYear = remember { currentYear.minusYears(adjacentYears) } val endYear = remember { currentYear.plusYears(adjacentYears) } val selections = remember { mutableStateListOf() } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index 73b9025b..fa711fed 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -36,7 +36,6 @@ import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarLayoutInfo import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.CalendarYear -import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.sample.shared.StatusBarColorLifecycleObserver import com.kizitonwose.calendar.sample.shared.findActivity @@ -167,7 +166,6 @@ fun rememberFirstMostVisibleMonth( * * @see [rememberFirstVisibleYearAfterScroll] */ -@OptIn(ExperimentalCalendarApi::class) @Composable fun rememberFirstMostVisibleYear( state: YearCalendarState, @@ -187,7 +185,6 @@ fun rememberFirstMostVisibleYear( * * @see [rememberFirstMostVisibleYear] */ -@OptIn(ExperimentalCalendarApi::class) @Composable fun rememberFirstVisibleYearAfterScroll(state: YearCalendarState): CalendarYear { val visibleYear = remember(state) { mutableStateOf(state.firstVisibleYear) } From 0d47caf25ffb551b725b07906b9f6aa49fc7ec55 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Mon, 22 Jul 2024 17:49:09 +0200 Subject: [PATCH 11/48] Add year calendar compose docs --- docs/Compose.md | 112 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 108 insertions(+), 4 deletions(-) diff --git a/docs/Compose.md b/docs/Compose.md index 682cae2c..463b2a18 100644 --- a/docs/Compose.md +++ b/docs/Compose.md @@ -4,7 +4,7 @@ - [Quick links](#quick-links) - [Compose Multiplatform Information](#compose-multiplatform-information) -- [Compose UI version compatibility](#compose-ui-versions) +- [Compose UI version compatibility](#compose-ui-version-compatibility) - [Calendar Composables](#calendar-composables) - [Usage](#usage) * [Calendar state](#usage) @@ -19,6 +19,7 @@ * [Disabling dates](#disabling-dates) - [Week calendar](#week-calendar) - [HeatMap calendar](#heatmap-calendar) +- [Year calendar](#year-calendar) ## Quick links @@ -57,7 +58,7 @@ Ensure that you are using the library version that matches the Compose UI versio ## Calendar Composables -The library can be used via four composables: +The library can be used via six composables: `HorizontalCalendar()`: Horizontally scrolling month-based calendar. @@ -67,9 +68,21 @@ The library can be used via four composables: `HeatMapCalendar()`: Horizontally scrolling heatmap calendar, useful for showing how data changes over time. A popular example is the user contribution chart on GitHub. +`HorizontalYearCalendar()`: Horizontally scrolling year-based calendar. + +`VerticalYearCalendar()`: Vertically scrolling year-based calendar. + All composables are based on LazyRow/LazyColumn for efficiency. -In the examples below, we will mostly use the month-based `HorizontalCalendar` and `VerticalCalendar` composables since all the calendar composables share the same basic concept. If you want a week-based calendar, use the `WeekCalendar` composable instead. Most state properties/methods with the name prefix/suffix `month` (e.g `firstVisibleMonth`) in the month-base calendar will have an equivalent with the name prefix/suffix `week` (e.g `firstVisibleWeek`) in the week-based calendar. +In the examples below, we will mostly use the month-based `HorizontalCalendar` +and `VerticalCalendar` composables since all the calendar composables share the same basic concept. +If you need a week-based calendar, use the `WeekCalendar` composable instead. If you need a +year-based calendar, use the `HorizontalYearCalendar` and `VerticalYearCalendar` composables. + +Most state properties/methods with the name prefix/suffix `month` (e.g `firstVisibleMonth`) in the +month-base calendar will have an equivalent with the name prefix/suffix `week` ( +e.g `firstVisibleWeek`) in the week-based calendar and `year` (e.g `firstVisibleYear`) in the +year-based calendar. ## Usage @@ -508,7 +521,9 @@ See the sample project for some complex implementations. ## Week calendar -As discussed previously, the library provides `HorizontalCalendar`, `VerticalCalendar`, and `WeekCalendar` composables. The `WeekCalendar` is a week-based calendar. Almost all topics covered above for the month calendar will apply to the week calendar. The main difference is that state properties/methods will have a slightly different name, typically with a `week` prefix/suffix instead of `month`. +The `WeekCalendar` is a week-based calendar. Almost all topics covered above for the month calendar +will apply to the week calendar. The main difference is that state properties/methods will have a +slightly different name, typically with a `week` prefix/suffix instead of `month`. For example: `firstVisibleMonth` => `firstVisibleWeek`, `scrollToMonth()` => `scrollToWeek()` and many others, but you get the idea. @@ -573,6 +588,95 @@ fun MainScreen() { Please see the `HeatMapCalendar` composable for the full documentation. There are also examples in the sample app. +## Year calendar + +The year-based calendar is best suited for large screens and can be used via +the `HorizontalYearCalendar` and `VerticalYearCalendar` composables. All topics covered above for +the month calendar will apply to the year calendar. The main difference is that state +properties/methods will have a slightly different name, typically with a `year` prefix/suffix +instead of `month`. + +For example: `firstVisibleMonth` => `firstVisibleYear`, `scrollToMonth()` => `scrollToYear()` and +many others, but you get the idea. + +The `monthHeader` and `monthFooter` parameters are available in both the month and year calendars +and serve the same purpose in both cases. The year calendar additionally provides the `yearHeader` +and `yearFooter` parameters to add a header or footer to each year on the calendar. + +Basic year calendar usage: + +```kotlin +@Composable +fun MainScreen() { + val currentYear = remember { Year.now() } + val startYear = remember { currentYear.minusYears(100) } // Adjust as needed + val endYear = remember { currentYear.plusYears(100) } // Adjust as needed + val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } // Available from the library + + val state = rememberYearCalendarState( + startYear = startYear, + endYear = endYear, + firstVisibleYear = currentYear, + firstDayOfWeek = firstDayOfWeek, + ) + HorizontalYearCalendar( + state = state, + dayContent = { Day(it) }, + yearHeader = { YearHeader(it) }, + monthHeader = { MonthHeader(it) }, + ) + +// If you need a vertical year calendar. +// VerticalYearCalendar( +// state = state, +// dayContent = { Day(it) } +// ) +} +``` + +There is an additional `outDateStyle` parameter that can be provided when creating the state +via `rememberYearCalendarState`. This determines how the out-dates are generated. See +the [properties](#state-properties) section to understand this parameter. + +A year calendar implementation from the sample app: + +Year calendar + +The year calendar composables also provide a parameter `isMonthVisible` which determines if a month +is added to the calendar year grid. For example, if you want a calendar that starts in the year 2024 +and ends in the year 2054, but only shows months from from July 2024, the logic would look like +this: + +```kotlin +@Composable +fun MainScreen() { + val july2024 = remember { YearMonth.of(2024, Month.JULY) } + val startYear = remember { Year.of(2024) } + val endYear = remember { Year.of(2054) } + val firstDayOfWeek = remember { firstDayOfWeekFromLocale() } + + val state = rememberYearCalendarState( + startYear = startYear, + endYear = endYear, + firstVisibleYear = startYear, + firstDayOfWeek = firstDayOfWeek, + ) + HorizontalYearCalendar( + state = state, + dayContent = { Day(it) }, + yearHeader = { YearHeader(it) }, + monthHeader = { MonthHeader(it) }, + isMonthVisible = { month -> + month.yearMonth >= july2024 + } + ) +} +``` + +The logic above will produce this result: + +Year calendar + Remember that all the screenshots shown so far are just examples of what you can achieve with the library and you can absolutely build your calendar to look however you want. **Made a cool calendar with this library? Share an image [here](https://github.com/kizitonwose/Calendar/issues/1).** From 45089a44d4e035d1d0a41ea78b2b45471c20a94a Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 10:38:14 +0200 Subject: [PATCH 12/48] Unify range checks --- .../kotlin/com/kizitonwose/calendar/compose/CalendarState.kt | 4 ++-- .../calendar/compose/heatmapcalendar/HeatMapCalendarState.kt | 4 ++-- .../calendar/compose/weekcalendar/WeekCalendarState.kt | 2 ++ .../calendar/compose/weekcalendar/WeekCalendarState.kt | 2 ++ 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt index ac4f221a..04b3cb16 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt @@ -20,7 +20,7 @@ import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState -import com.kizitonwose.calendar.data.checkDateRange +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.data.getCalendarMonthData import com.kizitonwose.calendar.data.getMonthIndex import com.kizitonwose.calendar.data.getMonthIndicesCount @@ -206,7 +206,7 @@ public class CalendarState internal constructor( private fun monthDataChanged() { store.clear() - checkDateRange(startMonth, endMonth) + checkRange(startMonth, endMonth) // Read the firstDayOfWeek and outDateStyle properties to ensure recomposition // even though they are unused in the CalendarInfo. Alternatively, we could use // mutableStateMapOf() as the backing store for DataStore() to ensure recomposition diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt index c40b75c3..546050a3 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt @@ -21,7 +21,7 @@ import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState -import com.kizitonwose.calendar.data.checkDateRange +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.data.getHeatMapCalendarMonthData import com.kizitonwose.calendar.data.getMonthIndex import com.kizitonwose.calendar.data.getMonthIndicesCount @@ -185,7 +185,7 @@ public class HeatMapCalendarState internal constructor( private fun monthDataChanged() { store.clear() - checkDateRange(startMonth, endMonth) + checkRange(startMonth, endMonth) calendarInfo = CalendarInfo( indexCount = getMonthIndicesCount(startMonth, endMonth), firstDayOfWeek = firstDayOfWeek, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt index 22016034..cef45046 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt @@ -27,6 +27,7 @@ import com.kizitonwose.calendar.core.toJvmSerializableLocalDate import com.kizitonwose.calendar.core.toLocalDate import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.data.getWeekCalendarAdjustedRange import com.kizitonwose.calendar.data.getWeekCalendarData import com.kizitonwose.calendar.data.getWeekIndex @@ -208,6 +209,7 @@ public class WeekCalendarState internal constructor( } private fun adjustDateRange() { + checkRange(startDate, endDate) val data = getWeekCalendarAdjustedRange(startDate, endDate, firstDayOfWeek) startDateAdjusted = data.startDateAdjusted endDateAdjusted = data.endDateAdjusted diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt index 7cc76c4f..26441b6f 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt @@ -22,6 +22,7 @@ import com.kizitonwose.calendar.core.WeekDayPosition import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.data.DataStore +import com.kizitonwose.calendar.data.checkRange import com.kizitonwose.calendar.data.getWeekCalendarAdjustedRange import com.kizitonwose.calendar.data.getWeekCalendarData import com.kizitonwose.calendar.data.getWeekIndex @@ -204,6 +205,7 @@ public class WeekCalendarState internal constructor( } private fun adjustDateRange() { + checkRange(startDate, endDate) val data = getWeekCalendarAdjustedRange(startDate, endDate, firstDayOfWeek) startDateAdjusted = data.startDateAdjusted endDateAdjusted = data.endDateAdjusted From 45126156a66eaf7b5d7cdbc15651d1bd59fa7f78 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 10:40:50 +0200 Subject: [PATCH 13/48] Add multiplatform core tests --- .../library/build.gradle.kts | 3 + .../kizitonwose/calendar/core/Extensions.kt | 5 + .../com/kizitonwose/calendar/core/Year.kt | 105 +++++++++++ .../kizitonwose/calendar/core/YearMonth.kt | 14 +- .../com/kizitonwose/calendar/data/Utils.kt | 15 +- .../com/kizitonwose/calendar/core/Utils.kt | 3 + .../calendar/core/YearMonthTest.kt | 175 ++++++++++++++++++ .../com/kizitonwose/calendar/core/YearTest.kt | 41 ++++ gradle/libs.versions.toml | 2 + 9 files changed, 347 insertions(+), 16 deletions(-) create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt diff --git a/compose-multiplatform/library/build.gradle.kts b/compose-multiplatform/library/build.gradle.kts index a13f535f..30bbbc83 100644 --- a/compose-multiplatform/library/build.gradle.kts +++ b/compose-multiplatform/library/build.gradle.kts @@ -70,6 +70,9 @@ kotlin { desktopMain.dependencies { implementation(compose.desktop.currentOs) } + commonTest.dependencies { + implementation(libs.kotlin.test) + } } @OptIn(ExperimentalKotlinGradlePluginApi::class) compilerOptions { diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt index 71fcd83f..60134c0c 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt @@ -5,6 +5,7 @@ import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month import kotlinx.datetime.TimeZone import kotlinx.datetime.isoDayNumber import kotlinx.datetime.minus @@ -46,6 +47,10 @@ public fun LocalDate.Companion.now( public val LocalDate.yearMonth: YearMonth get() = YearMonth(year, month) +public fun Month.atYear(year: Year): YearMonth = YearMonth(year.value, this) + +public fun Month.atYear(year: Int): YearMonth = YearMonth(year, this) + internal fun YearMonth.plusMonths(value: Int): YearMonth = plus(value, DateTimeUnit.MONTH) internal fun YearMonth.minusMonths(value: Int): YearMonth = minus(value, DateTimeUnit.MONTH) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt new file mode 100644 index 00000000..acb88b3c --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -0,0 +1,105 @@ +package com.kizitonwose.calendar.core + +import androidx.compose.runtime.Immutable +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeArithmeticException +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.TimeZone +import kotlinx.datetime.monthsUntil + +@Immutable +public data class Year(val value: Int) : Comparable, JvmSerializable { + internal val year = value + + init { + try { + atMonth(Month.JANUARY).atStartOfMonth() + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Year value $value is out of range", e) + } + } + + /** + * Same as java.time.Year.compareTo() + */ + override fun compareTo(other: Year): Int { + return value - other.value + } + + public companion object { + /** + * Obtains the current [Year] from the specified [clock] and [timeZone]. + * + * Using this method allows the use of an alternate clock or timezone for testing. + */ + public fun now( + clock: Clock = Clock.System, + timeZone: TimeZone = TimeZone.currentSystemDefault(), + ): Year = Year(LocalDate.now(clock, timeZone).year) + + public fun isLeap(year: Int): Boolean { + val prolepticYear: Long = year.toLong() + return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L) + } + } +} + + +public fun Year.atDay(dayOfYear: Int): LocalDate { + for (month in Month.entries) { + val yearMonth = atMonth(month) + if (yearMonth.atEndOfMonth().dayOfYear >= dayOfYear) { + return yearMonth.atDay((dayOfYear - yearMonth.atStartOfMonth().dayOfYear) + 1) + } + } + throw IllegalArgumentException("Invalid dayOfYear value '$dayOfYear' for year '$year") +} + +public fun Year.isLeap(): Boolean = Year.isLeap(year) + +public fun Year.length(): Int = if (isLeap()) 366 else 365 + +public fun Year.atMonthDay(month: Int, day: Int): LocalDate = LocalDate(year, month, day) + +public fun Year.atMonthDay(month: Month, day: Int): LocalDate = LocalDate(year, month, day) + +public fun Year.atMonth(month: Month): YearMonth = YearMonth(year, month) + +public fun Year.atMonth(month: Int): YearMonth = YearMonth(year, month) + +/** + * Returns the number of whole months between two year-month values. + * + * The value is rounded toward zero. + * + * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a + * positive result or [Int.MIN_VALUE] for a negative result. + * + * @see LocalDate.monthsUntil + */ +public fun Year.yearsUntil(other: Year): Int = other.year - year + +/** + * Returns a [Year] that results from adding the [value] number of the + * specified [unit] to this year-month. + * + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries + * of [Year] which is essentially the [LocalDate] boundaries. + */ +public fun Year.plusYears(value: Int): Year = Year(year + value) + +/** + * Returns a [Year] that results from subtracting the [value] number of the + * specified [unit] from this year-month. + * + * If the [value] is positive, the returned year-month is earlier than this year-month. + * If the [value] is negative, the returned year-month is later than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries + * of [Year] which is essentially the [LocalDate] boundaries. + */ +public fun Year.minusYears(value: Int): Year = Year(year - value) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt index a1ca4ba9..6bb96077 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt @@ -7,7 +7,6 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.Month import kotlinx.datetime.TimeZone -import kotlinx.datetime.daysUntil import kotlinx.datetime.minus import kotlinx.datetime.monthsUntil import kotlinx.datetime.number @@ -75,9 +74,16 @@ public fun YearMonth.atDay(day: Int): LocalDate = LocalDate(year, month, day) * For example, a date in February would return 29 in a leap year and 28 otherwise. */ public fun YearMonth.lengthOfMonth(): Int { - val thisMonthStart = atStartOfMonth() - val nextMonthStart = thisMonthStart.plusMonths(1) - return thisMonthStart.daysUntil(nextMonthStart) + return when (month) { + Month.FEBRUARY -> if (Year.isLeap(year)) 29 else 28 + Month.APRIL, + Month.JUNE, + Month.SEPTEMBER, + Month.NOVEMBER, + -> 30 + + else -> 31 + } } /** diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/Utils.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/Utils.kt index 0b36ece9..2c31f7dd 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/Utils.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/Utils.kt @@ -1,16 +1,7 @@ package com.kizitonwose.calendar.data -import com.kizitonwose.calendar.core.YearMonth -import kotlinx.datetime.LocalDate - -internal fun checkDateRange(startMonth: YearMonth, endMonth: YearMonth) { - check(endMonth >= startMonth) { - "startMonth: $startMonth is greater than endMonth: $endMonth" - } -} - -internal fun checkDateRange(startDate: LocalDate, endDate: LocalDate) { - check(endDate >= startDate) { - "startDate: $startDate is greater than endDate: $endDate" +internal fun > checkRange(start: T, end: T) { + check(end >= start) { + "start: $start is greater than end: $end" } } diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt new file mode 100644 index 00000000..ca53bb6c --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt @@ -0,0 +1,3 @@ +package com.kizitonwose.calendar.core + +internal infix fun Pair.toResult(that: C): Triple = Triple(first, second, that) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt new file mode 100644 index 00000000..97722098 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -0,0 +1,175 @@ +package com.kizitonwose.calendar.core + +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlin.test.Test +import kotlin.test.assertEquals + +class YearMonthTest { + @Test + fun lengthOfMonth() { + val leapYearValues = Month.entries.map { it.atYear(2024) } + val nonLeapYearValues = Month.entries.map { it.atYear(2023) } + + for (value in leapYearValues) { + val expectedLength = when (value.month) { + Month.FEBRUARY -> 29 + Month.APRIL, + Month.JUNE, + Month.SEPTEMBER, + Month.NOVEMBER, + -> 30 + + else -> 31 + } + assertEquals(expectedLength, value.lengthOfMonth()) + } + + for (value in nonLeapYearValues) { + val expectedLength = when (value.month) { + Month.FEBRUARY -> 28 + Month.APRIL, + Month.JUNE, + Month.SEPTEMBER, + Month.NOVEMBER, + -> 30 + + else -> 31 + } + assertEquals(expectedLength, value.lengthOfMonth()) + } + } + + @Test + fun atStartOfMonth() { + val values = listOf( + YearMonth(2025, Month.JANUARY) to LocalDate(2025, Month.JANUARY, 1), + YearMonth(2020, Month.JUNE) to LocalDate(2020, Month.JUNE, 1), + ) + + for ((yearMonth, firstDay) in values) { + assertEquals(yearMonth.atStartOfMonth(), firstDay) + } + } + + @Test + fun atEndOfMonth() { + val values = listOf( + YearMonth(2025, Month.JANUARY) to LocalDate(2025, Month.JANUARY, 31), + YearMonth(2024, Month.JUNE) to LocalDate(2024, Month.JUNE, 30), + YearMonth(2025, Month.FEBRUARY) to LocalDate(2025, Month.FEBRUARY, 28), + YearMonth(2024, Month.FEBRUARY) to LocalDate(2024, Month.FEBRUARY, 29), + ) + + for ((yearMonth, lastDay) in values) { + assertEquals(yearMonth.atEndOfMonth(), lastDay) + } + } + + @Test + fun atDay() { + val yearMonthValues = Month.entries.map { it.atYear(2024) } + + for (yearMonth in yearMonthValues) { + for (day in 1..yearMonth.lengthOfMonth()) { + assertEquals(LocalDate(yearMonth.year, yearMonth.month, day), yearMonth.atDay(day)) + } + } + } + + @Test + fun monthsUntil() { + val values = listOf( + YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.NOVEMBER) toResult 10, + YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.DECEMBER) toResult 11, + YearMonth(2026, Month.MARCH) to YearMonth(2028, Month.FEBRUARY) toResult 23, + YearMonth(2047, Month.OCTOBER) to YearMonth(2051, Month.APRIL) toResult 42, + YearMonth(2065, Month.JUNE) to YearMonth(2071, Month.JUNE) toResult 72, + YearMonth(2020, Month.MAY) to YearMonth(2023, Month.JULY) toResult 38, + YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.AUGUST) toResult 0, + YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.SEPTEMBER) toResult 1, + ) + + for ((start, end, result) in values) { + assertEquals(result, start.monthsUntil(end)) + assertEquals(-result, end.monthsUntil(start)) + } + } + + @Test + fun plus() { + val plusMonth = listOf( + YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2024, Month.NOVEMBER), + YearMonth(2020, Month.MAY) to 38 toResult YearMonth(2023, Month.JULY), + YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.AUGUST) to 1 toResult YearMonth(2022, Month.SEPTEMBER), + ) + val plusYear = listOf( + YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2034, Month.JANUARY), + YearMonth(2020, Month.MAY) to 38 toResult YearMonth(2058, Month.MAY), + YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.SEPTEMBER) to 1 toResult YearMonth(2023, Month.SEPTEMBER), + ) + + for ((start, value, result) in plusMonth) { + assertEquals(result, start.plus(value, DateTimeUnit.MONTH)) + assertEquals(start, result.plus(-value, DateTimeUnit.MONTH)) + } + + for ((start, value, result) in plusYear) { + assertEquals(result, start.plus(value, DateTimeUnit.YEAR)) + assertEquals(start, result.plus(-value, DateTimeUnit.YEAR)) + } + } + + @Test + fun minus() { + val minusMonth = listOf( + YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2023, Month.MARCH), + YearMonth(2020, Month.MAY) to 38 toResult YearMonth(2017, Month.MARCH), + YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.AUGUST) to 1 toResult YearMonth(2022, Month.JULY), + ) + val minusYear = listOf( + YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2014, Month.JANUARY), + YearMonth(2020, Month.MAY) to 38 toResult YearMonth(1982, Month.MAY), + YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.SEPTEMBER) to 1 toResult YearMonth(2021, Month.SEPTEMBER), + ) + + for ((start, value, result) in minusMonth) { + assertEquals(result, start.minus(value, DateTimeUnit.MONTH)) + assertEquals(start, result.minus(-value, DateTimeUnit.MONTH)) + } + + for ((start, value, result) in minusYear) { + assertEquals(result, start.minus(value, DateTimeUnit.YEAR)) + assertEquals(start, result.minus(-value, DateTimeUnit.YEAR)) + } + } + + @Test + fun next() { + val values = listOf( + YearMonth(2024, Month.DECEMBER) to YearMonth(2025, Month.JANUARY), + YearMonth(2020, Month.MAY) to YearMonth(2020, Month.JUNE), + ) + + for ((start, next) in values) { + assertEquals(next, start.next) + } + } + + @Test + fun previous() { + val values = listOf( + YearMonth(2025, Month.JANUARY) to YearMonth(2024, Month.DECEMBER), + YearMonth(2020, Month.JUNE) to YearMonth(2020, Month.MAY), + ) + + for ((start, previous) in values) { + assertEquals(previous, start.previous) + } + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt new file mode 100644 index 00000000..d37d4478 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt @@ -0,0 +1,41 @@ +package com.kizitonwose.calendar.core + +import kotlin.test.Test +import kotlin.test.assertEquals + +class YearTest { + @Test + fun isLeap() { + assertEquals(false, Year.isLeap(1999)) + assertEquals(true, Year.isLeap(2000)) + assertEquals(false, Year.isLeap(2001)) + assertEquals(false, Year.isLeap(2007)) + assertEquals(true, Year.isLeap(2008)) + assertEquals(false, Year.isLeap(2009)) + assertEquals(false, Year.isLeap(2010)) + assertEquals(false, Year.isLeap(2011)) + assertEquals(true, Year.isLeap(2012)) + assertEquals(false, Year.isLeap(2095)) + assertEquals(true, Year.isLeap(2096)) + assertEquals(false, Year.isLeap(2097)) + assertEquals(false, Year.isLeap(2098)) + assertEquals(false, Year.isLeap(2099)) + assertEquals(false, Year.isLeap(2100)) + assertEquals(false, Year.isLeap(2101)) + assertEquals(false, Year.isLeap(2102)) + assertEquals(false, Year.isLeap(2103)) + assertEquals(true, Year.isLeap(2104)) + assertEquals(false, Year.isLeap(2105)) + assertEquals(false, Year.isLeap(-500)) + assertEquals(true, Year.isLeap(-400)) + assertEquals(false, Year.isLeap(-300)) + assertEquals(false, Year.isLeap(-200)) + assertEquals(false, Year.isLeap(-100)) + assertEquals(true, Year.isLeap(0)) + assertEquals(false, Year.isLeap(100)) + assertEquals(false, Year.isLeap(200)) + assertEquals(false, Year.isLeap(300)) + assertEquals(true, Year.isLeap(400)) + assertEquals(false, Year.isLeap(500)) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d24d95e..a01f180d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,8 @@ junit5 = "5.10.3" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } From c78a7a34f68ea93b56952fa6c6398f3faf33ffaf Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 14:18:12 +0200 Subject: [PATCH 14/48] Add multiplatform Year and YearMonth tests --- .../com/kizitonwose/calendar/core/Year.kt | 4 +- .../calendar/core/ExtensionTest.kt | 80 +++++++++ .../com/kizitonwose/calendar/core/Utils.kt | 2 +- .../calendar/core/YearMonthTest.kt | 48 +++--- .../com/kizitonwose/calendar/core/YearTest.kt | 163 ++++++++++++++++++ .../calendar/data/YearDataTests.kt | 8 +- 6 files changed, 277 insertions(+), 28 deletions(-) create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt index acb88b3c..812e32f1 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -60,13 +60,13 @@ public fun Year.isLeap(): Boolean = Year.isLeap(year) public fun Year.length(): Int = if (isLeap()) 366 else 365 -public fun Year.atMonthDay(month: Int, day: Int): LocalDate = LocalDate(year, month, day) +public fun Year.atMonthDay(monthNumber: Int, day: Int): LocalDate = LocalDate(year, monthNumber, day) public fun Year.atMonthDay(month: Month, day: Int): LocalDate = LocalDate(year, month, day) public fun Year.atMonth(month: Month): YearMonth = YearMonth(year, month) -public fun Year.atMonth(month: Int): YearMonth = YearMonth(year, month) +public fun Year.atMonth(monthNumber: Int): YearMonth = YearMonth(year, monthNumber) /** * Returns the number of whole months between two year-month values. diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt new file mode 100644 index 00000000..c1dd31c6 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt @@ -0,0 +1,80 @@ +package com.kizitonwose.calendar.core + +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlin.test.Test +import kotlin.test.assertEquals + +class ExtensionTest { + @Test + fun generalExtensions() { + assertEquals( + YearMonth(2024, Month.JUNE), + Month.JUNE.atYear(2024), + ) + assertEquals( + YearMonth(2024, Month.JUNE), + Month.JUNE.atYear(Year(2024)), + ) + assertEquals( + YearMonth(2024, Month.JULY), + YearMonth(2024, Month.JUNE).plusMonths(1), + ) + assertEquals( + YearMonth(2024, Month.MAY), + YearMonth(2024, Month.JUNE).minusMonths(1), + ) + assertEquals( + LocalDate(2024, Month.MAY, 2), + LocalDate(2024, Month.MAY, 1).plusDays(1), + ) + assertEquals( + LocalDate(2024, Month.APRIL, 30), + LocalDate(2024, Month.MAY, 1).minusDays(1), + ) + assertEquals( + LocalDate(2024, Month.JUNE, 2), + LocalDate(2024, Month.MAY, 2).plusMonths(1), + ) + assertEquals( + LocalDate(2024, Month.FEBRUARY, 29), + LocalDate(2024, Month.MARCH, 30).minusMonths(1), + ) + assertEquals( + LocalDate(2024, Month.MAY, 9), + LocalDate(2024, Month.MAY, 2).plusWeeks(1), + ) + assertEquals( + LocalDate(2024, Month.MARCH, 23), + LocalDate(2024, Month.MARCH, 30).minusWeeks(1), + ) + assertEquals( + 4, + LocalDate(2024, Month.MARCH, 1).weeksUntil(LocalDate(2024, Month.MARCH, 30)), + ) + assertEquals( + YearMonth(2024, Month.MARCH), + LocalDate(2024, Month.MARCH, 1).yearMonth, + ) + } + + @Test + fun daysUntil() { + assertEquals(5, DayOfWeek.FRIDAY.daysUntil(DayOfWeek.WEDNESDAY)) + assertEquals(2, DayOfWeek.TUESDAY.daysUntil(DayOfWeek.THURSDAY)) + assertEquals(0, DayOfWeek.SUNDAY.daysUntil(DayOfWeek.SUNDAY)) + assertEquals(3, DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY)) + assertEquals(5, DayOfWeek.WEDNESDAY.daysUntil(DayOfWeek.MONDAY)) + assertEquals(1, DayOfWeek.THURSDAY.daysUntil(DayOfWeek.FRIDAY)) + assertEquals(6, DayOfWeek.MONDAY.daysUntil(DayOfWeek.SUNDAY)) + assertEquals(6, DayOfWeek.SUNDAY.daysUntil(DayOfWeek.SATURDAY)) + } + + @Test + fun daysOfWeek() { + DayOfWeek.entries.forEach { dayOfWeek -> + assertEquals(dayOfWeek, daysOfWeek(firstDayOfWeek = dayOfWeek).first()) + } + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt index ca53bb6c..a0810c90 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt @@ -1,3 +1,3 @@ package com.kizitonwose.calendar.core -internal infix fun Pair.toResult(that: C): Triple = Triple(first, second, that) +internal infix fun Pair.toTriple(that: C): Triple = Triple(first, second, that) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt index 97722098..7ea0a4e3 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -81,14 +81,14 @@ class YearMonthTest { @Test fun monthsUntil() { val values = listOf( - YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.NOVEMBER) toResult 10, - YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.DECEMBER) toResult 11, - YearMonth(2026, Month.MARCH) to YearMonth(2028, Month.FEBRUARY) toResult 23, - YearMonth(2047, Month.OCTOBER) to YearMonth(2051, Month.APRIL) toResult 42, - YearMonth(2065, Month.JUNE) to YearMonth(2071, Month.JUNE) toResult 72, - YearMonth(2020, Month.MAY) to YearMonth(2023, Month.JULY) toResult 38, - YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.AUGUST) toResult 0, - YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.SEPTEMBER) toResult 1, + YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.NOVEMBER) toTriple 10, + YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.DECEMBER) toTriple 11, + YearMonth(2026, Month.MARCH) to YearMonth(2028, Month.FEBRUARY) toTriple 23, + YearMonth(2047, Month.OCTOBER) to YearMonth(2051, Month.APRIL) toTriple 42, + YearMonth(2065, Month.JUNE) to YearMonth(2071, Month.JUNE) toTriple 72, + YearMonth(2020, Month.MAY) to YearMonth(2023, Month.JULY) toTriple 38, + YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.AUGUST) toTriple 0, + YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.SEPTEMBER) toTriple 1, ) for ((start, end, result) in values) { @@ -100,16 +100,16 @@ class YearMonthTest { @Test fun plus() { val plusMonth = listOf( - YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2024, Month.NOVEMBER), - YearMonth(2020, Month.MAY) to 38 toResult YearMonth(2023, Month.JULY), - YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), - YearMonth(2022, Month.AUGUST) to 1 toResult YearMonth(2022, Month.SEPTEMBER), + YearMonth(2024, Month.JANUARY) to 10 toTriple YearMonth(2024, Month.NOVEMBER), + YearMonth(2020, Month.MAY) to 38 toTriple YearMonth(2023, Month.JULY), + YearMonth(2022, Month.AUGUST) to 0 toTriple YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.AUGUST) to 1 toTriple YearMonth(2022, Month.SEPTEMBER), ) val plusYear = listOf( - YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2034, Month.JANUARY), - YearMonth(2020, Month.MAY) to 38 toResult YearMonth(2058, Month.MAY), - YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), - YearMonth(2022, Month.SEPTEMBER) to 1 toResult YearMonth(2023, Month.SEPTEMBER), + YearMonth(2024, Month.JANUARY) to 10 toTriple YearMonth(2034, Month.JANUARY), + YearMonth(2020, Month.MAY) to 38 toTriple YearMonth(2058, Month.MAY), + YearMonth(2022, Month.AUGUST) to 0 toTriple YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.SEPTEMBER) to 1 toTriple YearMonth(2023, Month.SEPTEMBER), ) for ((start, value, result) in plusMonth) { @@ -126,16 +126,16 @@ class YearMonthTest { @Test fun minus() { val minusMonth = listOf( - YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2023, Month.MARCH), - YearMonth(2020, Month.MAY) to 38 toResult YearMonth(2017, Month.MARCH), - YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), - YearMonth(2022, Month.AUGUST) to 1 toResult YearMonth(2022, Month.JULY), + YearMonth(2024, Month.JANUARY) to 10 toTriple YearMonth(2023, Month.MARCH), + YearMonth(2020, Month.MAY) to 38 toTriple YearMonth(2017, Month.MARCH), + YearMonth(2022, Month.AUGUST) to 0 toTriple YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.AUGUST) to 1 toTriple YearMonth(2022, Month.JULY), ) val minusYear = listOf( - YearMonth(2024, Month.JANUARY) to 10 toResult YearMonth(2014, Month.JANUARY), - YearMonth(2020, Month.MAY) to 38 toResult YearMonth(1982, Month.MAY), - YearMonth(2022, Month.AUGUST) to 0 toResult YearMonth(2022, Month.AUGUST), - YearMonth(2022, Month.SEPTEMBER) to 1 toResult YearMonth(2021, Month.SEPTEMBER), + YearMonth(2024, Month.JANUARY) to 10 toTriple YearMonth(2014, Month.JANUARY), + YearMonth(2020, Month.MAY) to 38 toTriple YearMonth(1982, Month.MAY), + YearMonth(2022, Month.AUGUST) to 0 toTriple YearMonth(2022, Month.AUGUST), + YearMonth(2022, Month.SEPTEMBER) to 1 toTriple YearMonth(2021, Month.SEPTEMBER), ) for ((start, value, result) in minusMonth) { diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt index d37d4478..b954195f 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt @@ -1,7 +1,11 @@ package com.kizitonwose.calendar.core +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlinx.datetime.number import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class YearTest { @Test @@ -38,4 +42,163 @@ class YearTest { assertEquals(true, Year.isLeap(400)) assertEquals(false, Year.isLeap(500)) } + + @Test + fun length() { + val leapYears = listOf(2000, 2008, 2012, 2096, 0, -400) + val nonLeapYears = listOf(2001, 2011, 2095, 500, 1, -500) + + for (year in leapYears) { + assertEquals(366, Year(year).length()) + } + + for (year in nonLeapYears) { + assertEquals(365, Year(year).length()) + } + } + + @Test + fun atMonth() { + val years = listOf(0, -400, 2024, 1, 1999) + + for (year in years) { + for (month in Month.entries) { + assertEquals(YearMonth(year, month), Year(year).atMonth(month)) + } + } + } + + @Test + fun atMonthNumber() { + val years = listOf(0, -400, 2024, 1, 1999) + + for (year in years) { + for (month in Month.entries) { + assertEquals(YearMonth(year, month.number), Year(year).atMonth(month)) + } + } + } + + @Test + fun atMonthDay() { + val validDays = listOf( + 2024 to Month.FEBRUARY toTriple 29, + 1999 to Month.JUNE toTriple 30, + 2030 to Month.DECEMBER toTriple 31, + 1866 to Month.DECEMBER toTriple 1, + ) + val invalidDays = listOf( + 2023 to Month.FEBRUARY toTriple 29, + 1999 to Month.JUNE toTriple 31, + 2030 to Month.DECEMBER toTriple -1, + 1866 to Month.DECEMBER toTriple 0, + ) + + for ((year, month, day) in validDays) { + assertEquals(LocalDate(year, month, day), Year(year).atMonthDay(month, day)) + } + + for ((year, month, day) in invalidDays) { + assertFailsWith(IllegalArgumentException::class) { + Year(year).atMonthDay(month, day) + } + } + } + + @Test + fun atMonthNumberDay() { + val validDays = listOf( + 2024 to 1 toTriple 29, + 1999 to 6 toTriple 30, + 2030 to 12 toTriple 31, + 1866 to 12 toTriple 1, + ) + val invalidDays = listOf( + 2023 to 0 toTriple 29, + 1999 to 13 toTriple 31, + 2030 to 6 toTriple -1, + 1866 to 1 toTriple 0, + ) + + for ((year, monthNumber, day) in validDays) { + assertEquals(LocalDate(year, monthNumber, day), Year(year).atMonthDay(monthNumber, day)) + } + + for ((year, monthNumber, day) in invalidDays) { + assertFailsWith(IllegalArgumentException::class) { + Year(year).atMonthDay(monthNumber, day) + } + } + } + + @Test + fun atDay() { + val validDays = listOf( + 2024 to 366 toTriple LocalDate(2024, Month.DECEMBER, 31), + 2039 to 365 toTriple LocalDate(2039, Month.DECEMBER, 31), + 1999 to 30 toTriple LocalDate(1999, Month.JANUARY, 30), + 1866 to 59 toTriple LocalDate(1866, Month.FEBRUARY, 28), + ) + val invalidDays = listOf( + 2034 to 367, + 2023 to 366, + 2030 to -1, + 2030 to 0, + ) + + for ((year, dayOfYear, date) in validDays) { + assertEquals(date, Year(year).atDay(dayOfYear)) + } + + for ((year, dayOfYear) in invalidDays) { + assertFailsWith(IllegalArgumentException::class) { + Year(year).atDay(dayOfYear) + } + } + } + + @Test + fun yearsUntil() { + val values = listOf( + 2020 to 2024 toTriple 4, + 2024 to 2030 toTriple 6, + 1999 to 2028 toTriple 29, + 1300 to 1365 toTriple 65, + ) + + for ((start, end, result) in values) { + assertEquals(result, Year(start).yearsUntil(Year(end))) + assertEquals(-result, Year(end).yearsUntil(Year(start))) + } + } + + @Test + fun plus() { + val values = listOf( + 2020 to 4 toTriple 2024, + 2024 to 6 toTriple 2030, + 1999 to 29 toTriple 2028, + 1300 to 65 toTriple 1365, + ) + + for ((start, value, result) in values) { + assertEquals(Year(result), Year(start).plusYears(value)) + assertEquals(Year(start), Year(result).plusYears(-value)) + } + } + + @Test + fun minus() { + val values = listOf( + 2020 to 4 toTriple 2016, + 2024 to 6 toTriple 2018, + 1999 to 29 toTriple 1970, + 1300 to 65 toTriple 1235, + ) + + for ((start, value, result) in values) { + assertEquals(Year(result), Year(start).minusYears(value)) + assertEquals(Year(start), Year(result).minusYears(-value)) + } + } } diff --git a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt index 9f3ec65c..3cdac7ae 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt @@ -69,21 +69,27 @@ class YearDataTests { @Test fun `generated year is at the correct offset`() { val yearData = getCalendarYearData(Year.of(2020), 6, DayOfWeek.SUNDAY, OutDateStyle.EndOfGrid) + val yearData2 = getCalendarYearData(Year.of(2021), 0, DayOfWeek.SUNDAY, OutDateStyle.EndOfRow) assertEquals(yearData.year, Year.of(2026)) + assertEquals(yearData2.year, Year.of(2021)) } @Test fun `year index calculation works as expected`() { val index = getYearIndex(startYear = Year.of(2020), targetYear = Year.of(2030)) + val index2 = getYearIndex(startYear = Year.of(2052), targetYear = Year.of(2052)) assertEquals(10, index) + assertEquals(0, index2) } @Test - fun `index indices count calculation works as expected`() { + fun `year indices count calculation works as expected`() { val count = getYearIndicesCount(startYear = Year.of(2020), endYear = Year.of(2040)) + val count2 = getYearIndicesCount(startYear = Year.of(2052), endYear = Year.of(2052)) assertEquals(21, count) + assertEquals(1, count2) } } From 87ffc6357f230d693b3f7648ee5dcba78d85327d Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 15:35:46 +0200 Subject: [PATCH 15/48] Add Year model docs --- .../kizitonwose/calendar/core/Extensions.kt | 5 - .../com/kizitonwose/calendar/core/Year.kt | 102 ++++++++++++------ .../calendar/core/ExtensionTest.kt | 8 -- .../calendar/core/YearMonthTest.kt | 6 +- 4 files changed, 75 insertions(+), 46 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt index 60134c0c..71fcd83f 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt @@ -5,7 +5,6 @@ import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate -import kotlinx.datetime.Month import kotlinx.datetime.TimeZone import kotlinx.datetime.isoDayNumber import kotlinx.datetime.minus @@ -47,10 +46,6 @@ public fun LocalDate.Companion.now( public val LocalDate.yearMonth: YearMonth get() = YearMonth(year, month) -public fun Month.atYear(year: Year): YearMonth = YearMonth(year.value, this) - -public fun Month.atYear(year: Int): YearMonth = YearMonth(year, this) - internal fun YearMonth.plusMonths(value: Int): YearMonth = plus(value, DateTimeUnit.MONTH) internal fun YearMonth.minusMonths(value: Int): YearMonth = minus(value, DateTimeUnit.MONTH) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt index 812e32f1..a08bb959 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -2,11 +2,9 @@ package com.kizitonwose.calendar.core import androidx.compose.runtime.Immutable import kotlinx.datetime.Clock -import kotlinx.datetime.DateTimeArithmeticException import kotlinx.datetime.LocalDate import kotlinx.datetime.Month import kotlinx.datetime.TimeZone -import kotlinx.datetime.monthsUntil @Immutable public data class Year(val value: Int) : Comparable, JvmSerializable { @@ -38,6 +36,20 @@ public data class Year(val value: Int) : Comparable, JvmSerializable { timeZone: TimeZone = TimeZone.currentSystemDefault(), ): Year = Year(LocalDate.now(clock, timeZone).year) + /** + * Checks if the year is a leap year, according to the ISO proleptic calendar system rules. + * + * This method applies the current rules for leap years across the whole time-line. + * In general, a year is a leap year if it is divisible by four without remainder. + * However, years divisible by 100, are not leap years, with the exception of years + * divisible by 400 which are. + * + * For example, 1904 was a leap year it is divisible by 4. 1900 was not a leap year + * as it is divisible by 100, however 2000 was a leap year as it is divisible by 400. + * + * The calculation is proleptic - applying the same rules into the far future and far past. + * This is historically inaccurate, but is correct for the ISO-8601 standard. + */ public fun isLeap(year: Int): Boolean { val prolepticYear: Long = year.toLong() return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L) @@ -45,8 +57,44 @@ public data class Year(val value: Int) : Comparable, JvmSerializable { } } +/** + * Checks if the year is a leap year, according to the ISO proleptic calendar system rules. + * + * This method applies the current rules for leap years across the whole time-line. + * In general, a year is a leap year if it is divisible by four without remainder. + * However, years divisible by 100, are not leap years, with the exception of years + * divisible by 400 which are. + * + * For example, 1904 was a leap year it is divisible by 4. 1900 was not a leap year + * as it is divisible by 100, however 2000 was a leap year as it is divisible by 400. + * + * The calculation is proleptic - applying the same rules into the far future and far past. + * This is historically inaccurate, but is correct for the ISO-8601 standard. + */ +public fun Year.isLeap(): Boolean = Year.isLeap(year) + +/** + * Returns the number of days in this year. + * + * The result is 366 if this is a leap year and 365 otherwise. + */ +public fun Year.length(): Int = if (isLeap()) 366 else 365 + +/** + * Returns the [LocalDate] at the specified [dayOfYear] in this year. + * + * The day-of-year value 366 is only valid in a leap year + * + * @throws IllegalArgumentException if [dayOfYear] value is invalid in this year. + */ public fun Year.atDay(dayOfYear: Int): LocalDate { + require( + dayOfYear >= 1 && + (dayOfYear <= 365 || isLeap() && dayOfYear <= 365), + ) { + "Invalid dayOfYear value '$dayOfYear' for year '$year" + } for (month in Month.entries) { val yearMonth = atMonth(month) if (yearMonth.atEndOfMonth().dayOfYear >= dayOfYear) { @@ -56,50 +104,44 @@ public fun Year.atDay(dayOfYear: Int): LocalDate { throw IllegalArgumentException("Invalid dayOfYear value '$dayOfYear' for year '$year") } -public fun Year.isLeap(): Boolean = Year.isLeap(year) - -public fun Year.length(): Int = if (isLeap()) 366 else 365 - +/** + * Returns the [LocalDate] at the specified [monthNumber] and [day] in this year. + * + * @throws IllegalArgumentException if either [monthNumber] is invalid or the [day] value + * is invalid in the resolved calendar [Month]. + */ public fun Year.atMonthDay(monthNumber: Int, day: Int): LocalDate = LocalDate(year, monthNumber, day) +/** + * Returns the [LocalDate] at the specified [month] and [day] in this year. + * + * @throws IllegalArgumentException if the [day] value is invalid in the resolved calendar [Month]. + */ public fun Year.atMonthDay(month: Month, day: Int): LocalDate = LocalDate(year, month, day) +/** + * Returns the [YearMonth] at the specified [month] in this year. + */ public fun Year.atMonth(month: Month): YearMonth = YearMonth(year, month) +/** + * Returns the [YearMonth] at the specified [monthNumber] in this year. + * + * @throws IllegalArgumentException if either [monthNumber] is invalid. + */ public fun Year.atMonth(monthNumber: Int): YearMonth = YearMonth(year, monthNumber) /** - * Returns the number of whole months between two year-month values. - * - * The value is rounded toward zero. - * - * If the result does not fit in [Int], returns [Int.MAX_VALUE] for a - * positive result or [Int.MIN_VALUE] for a negative result. - * - * @see LocalDate.monthsUntil + * Returns the number of whole years between two year values. */ public fun Year.yearsUntil(other: Year): Int = other.year - year /** - * Returns a [Year] that results from adding the [value] number of the - * specified [unit] to this year-month. - * - * If the [value] is positive, the returned year-month is later than this year-month. - * If the [value] is negative, the returned year-month is earlier than this year-month. - * - * @throws DateTimeArithmeticException if the result exceeds the boundaries - * of [Year] which is essentially the [LocalDate] boundaries. + * Returns a [Year] that results from adding the [value] number of years to this year. */ public fun Year.plusYears(value: Int): Year = Year(year + value) /** - * Returns a [Year] that results from subtracting the [value] number of the - * specified [unit] from this year-month. - * - * If the [value] is positive, the returned year-month is earlier than this year-month. - * If the [value] is negative, the returned year-month is later than this year-month. - * - * @throws DateTimeArithmeticException if the result exceeds the boundaries - * of [Year] which is essentially the [LocalDate] boundaries. + * Returns a [Year] that results from subtracting the [value] number of years to this year. */ public fun Year.minusYears(value: Int): Year = Year(year - value) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt index c1dd31c6..71a34618 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt @@ -9,14 +9,6 @@ import kotlin.test.assertEquals class ExtensionTest { @Test fun generalExtensions() { - assertEquals( - YearMonth(2024, Month.JUNE), - Month.JUNE.atYear(2024), - ) - assertEquals( - YearMonth(2024, Month.JUNE), - Month.JUNE.atYear(Year(2024)), - ) assertEquals( YearMonth(2024, Month.JULY), YearMonth(2024, Month.JUNE).plusMonths(1), diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt index 7ea0a4e3..77480819 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -9,8 +9,8 @@ import kotlin.test.assertEquals class YearMonthTest { @Test fun lengthOfMonth() { - val leapYearValues = Month.entries.map { it.atYear(2024) } - val nonLeapYearValues = Month.entries.map { it.atYear(2023) } + val leapYearValues = Month.entries.map { Year(2024).atMonth(it) } + val nonLeapYearValues = Month.entries.map { Year(2023).atMonth(it) } for (value in leapYearValues) { val expectedLength = when (value.month) { @@ -69,7 +69,7 @@ class YearMonthTest { @Test fun atDay() { - val yearMonthValues = Month.entries.map { it.atYear(2024) } + val yearMonthValues = Month.entries.map { Year(2024).atMonth(it) } for (yearMonth in yearMonthValues) { for (day in 1..yearMonth.lengthOfMonth()) { From ac8152b82beb95e55fa49dee4b5eea652560481c Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 16:30:43 +0200 Subject: [PATCH 16/48] Add jvm year converters --- .../kotlin/com/kizitonwose/calendar/core/Converters.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt b/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt index c78263e5..d5a7d763 100644 --- a/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt +++ b/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt @@ -1,7 +1,12 @@ package com.kizitonwose.calendar.core +import java.time.Year as jtYear import java.time.YearMonth as jtYearMonth public fun YearMonth.toJavaYearMonth(): jtYearMonth = jtYearMonth.of(year, month) public fun jtYearMonth.toKotlinYearMonth(): YearMonth = YearMonth(year, month) + +public fun Year.toJavaYear(): jtYear = jtYear.of(year) + +public fun jtYear.toKotlinYear(): Year = Year(value) From 99178e95c8b1aeb04207c73bc25f9531d6730d2d Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 20:16:29 +0200 Subject: [PATCH 17/48] Add YearMonth/Year serializers and ISO 8601 toString() implementations. --- build.gradle.kts | 1 + .../library/build.gradle.kts | 3 + .../compose/weekcalendar/WeekCalendar.kt | 2 +- .../compose/weekcalendar/WeekCalendarState.kt | 6 +- .../com/kizitonwose/calendar/core/Year.kt | 11 ++- .../kizitonwose/calendar/core/YearMonth.kt | 16 ++++ .../calendar/core/format/Format.kt | 20 +++++ .../JvmSerializableLocalDate.kt | 3 +- .../core/serializers/YearMonthSerializers.kt | 79 +++++++++++++++++++ .../core/serializers/YearSerializers.kt | 73 +++++++++++++++++ .../core/YearMonthSerializationTest.kt | 68 ++++++++++++++++ .../calendar/core/YearMonthTest.kt | 26 ++++++ .../calendar/core/YearSerializationTest.kt | 59 ++++++++++++++ .../com/kizitonwose/calendar/core/YearTest.kt | 14 ++++ core/build.gradle.kts | 2 +- gradle/libs.versions.toml | 5 +- 16 files changed, 380 insertions(+), 8 deletions(-) create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt rename compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/{ => internal}/JvmSerializableLocalDate.kt (82%) create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthSerializationTest.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearSerializationTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index e6cd5c3d..624bb8de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,6 +15,7 @@ plugins { alias(libs.plugins.mavenPublish) apply false alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.jetbrainsCompose) apply false + alias(libs.plugins.kotlinSerialization) apply false alias(libs.plugins.versionCheck) alias(libs.plugins.bcv) } diff --git a/compose-multiplatform/library/build.gradle.kts b/compose-multiplatform/library/build.gradle.kts index 30bbbc83..97b379a0 100644 --- a/compose-multiplatform/library/build.gradle.kts +++ b/compose-multiplatform/library/build.gradle.kts @@ -9,6 +9,7 @@ plugins { alias(libs.plugins.androidLibrary) alias(libs.plugins.jetbrainsCompose) alias(libs.plugins.composeCompiler) + alias(libs.plugins.kotlinSerialization) alias(libs.plugins.mavenPublish) } @@ -58,6 +59,7 @@ kotlin { implementation(compose.ui) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) + compileOnly(libs.kotlinx.serialization.core) api(libs.kotlinx.datetime) } val nonJvmMain by creating { @@ -72,6 +74,7 @@ kotlin { } commonTest.dependencies { implementation(libs.kotlin.test) + implementation(libs.kotlinx.serialization.json) } } @OptIn(ExperimentalKotlinGradlePluginApi::class) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt index 63f57649..11f5cb87 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.draw.clipToBounds import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay -import com.kizitonwose.calendar.core.toJvmSerializableLocalDate +import com.kizitonwose.calendar.core.internal.toJvmSerializableLocalDate @Composable internal fun WeekCalendarImpl( diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt index cef45046..04ba530a 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt @@ -15,16 +15,16 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import com.kizitonwose.calendar.core.JvmSerializableLocalDate import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDayPosition import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.atEndOfMonth import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.internal.JvmSerializableLocalDate +import com.kizitonwose.calendar.core.internal.toJvmSerializableLocalDate +import com.kizitonwose.calendar.core.internal.toLocalDate import com.kizitonwose.calendar.core.now -import com.kizitonwose.calendar.core.toJvmSerializableLocalDate -import com.kizitonwose.calendar.core.toLocalDate import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState import com.kizitonwose.calendar.data.checkRange diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt index a08bb959..fcac2382 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -1,12 +1,16 @@ package com.kizitonwose.calendar.core import androidx.compose.runtime.Immutable +import com.kizitonwose.calendar.core.format.toIso8601String +import com.kizitonwose.calendar.core.serializers.YearIso8601Serializer import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate import kotlinx.datetime.Month import kotlinx.datetime.TimeZone +import kotlinx.serialization.Serializable @Immutable +@Serializable(with = YearIso8601Serializer::class) public data class Year(val value: Int) : Comparable, JvmSerializable { internal val year = value @@ -25,6 +29,11 @@ public data class Year(val value: Int) : Comparable, JvmSerializable { return value - other.value } + /** + * Converts this year to the ISO 8601 string representation. + */ + override fun toString(): String = toIso8601String() + public companion object { /** * Obtains the current [Year] from the specified [clock] and [timeZone]. @@ -91,7 +100,7 @@ public fun Year.length(): Int = if (isLeap()) 366 else 365 public fun Year.atDay(dayOfYear: Int): LocalDate { require( dayOfYear >= 1 && - (dayOfYear <= 365 || isLeap() && dayOfYear <= 365), + (dayOfYear <= 365 || isLeap() && dayOfYear <= 366), ) { "Invalid dayOfYear value '$dayOfYear' for year '$year" } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt index 6bb96077..2f1b7985 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt @@ -1,6 +1,8 @@ package com.kizitonwose.calendar.core import androidx.compose.runtime.Immutable +import com.kizitonwose.calendar.core.format.toIso8601String +import com.kizitonwose.calendar.core.serializers.YearMonthIso8601Serializer import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeArithmeticException import kotlinx.datetime.DateTimeUnit @@ -11,12 +13,21 @@ import kotlinx.datetime.minus import kotlinx.datetime.monthsUntil import kotlinx.datetime.number import kotlinx.datetime.plus +import kotlinx.serialization.Serializable @Immutable +@Serializable(with = YearMonthIso8601Serializer::class) public data class YearMonth(val year: Int, val month: Month) : Comparable, JvmSerializable { public constructor(year: Int, monthNumber: Int) : this(year = year, month = Month(monthNumber)) + /** + * Returns the number-of-the-month (1..12) component of the year-month. + * + * Shortcut for `month.number`. + */ + public val monthNumber: Int get() = month.number + init { try { atStartOfMonth() @@ -47,6 +58,11 @@ public data class YearMonth(val year: Int, val month: Month) : Comparable { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("com.kizitonwose.calendar.core.YearMonth", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): YearMonth = + LocalDate.parse("${decoder.decodeString()}-01", LocalDate.Formats.ISO).yearMonth + + override fun serialize(encoder: Encoder, value: YearMonth) { + encoder.encodeString(value.toIso8601String()) + } +} + +/** + * A serializer for [YearMonth] that represents a value as its components. + * + * JSON example: `{"year":2020,"month":12}` + */ +public object YearMonthComponentSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("com.kizitonwose.calendar.core.YearMonth") { + element("year") + element("month") + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): YearMonth = + decoder.decodeStructure(descriptor) { + var year: Int? = null + var month: Short? = null + loop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> year = decodeIntElement(descriptor, 0) + 1 -> month = decodeShortElement(descriptor, 1) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw SerializationException("Unexpected index: $index") + } + } + if (year == null) throw MissingFieldException(missingField = "year", serialName = descriptor.serialName) + if (month == null) throw MissingFieldException(missingField = "month", serialName = descriptor.serialName) + YearMonth(year, month.toInt()) + } + + override fun serialize(encoder: Encoder, value: YearMonth) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.year) + encodeShortElement(descriptor, 1, value.monthNumber.toShort()) + } + } +} diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt new file mode 100644 index 00000000..f521df78 --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt @@ -0,0 +1,73 @@ +package com.kizitonwose.calendar.core.serializers + +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.format.toIso8601String +import kotlinx.datetime.LocalDate +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.MissingFieldException +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure + +/** + * A serializer for [Year] that uses the ISO 8601 representation. + * + * JSON example: `"2020"` + * + * @see Year.toString + */ +public object YearIso8601Serializer : KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("com.kizitonwose.calendar.core.Year", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Year = + Year(LocalDate.parse("${decoder.decodeString()}-01-01", LocalDate.Formats.ISO).year) + + override fun serialize(encoder: Encoder, value: Year) { + encoder.encodeString(value.toIso8601String()) + } +} + +/** + * A serializer for [Year] that represents a value as its components. + * + * JSON example: `{"year":2020}` + */ +public object YearComponentSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("com.kizitonwose.calendar.core.Year") { + element("year") + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): Year = + decoder.decodeStructure(descriptor) { + var year: Int? = null + loop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> year = decodeIntElement(descriptor, 0) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw SerializationException("Unexpected index: $index") + } + } + if (year == null) throw MissingFieldException(missingField = "year", serialName = descriptor.serialName) + Year(year) + } + + override fun serialize(encoder: Encoder, value: Year) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.year) + } + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthSerializationTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthSerializationTest.kt new file mode 100644 index 00000000..ad9c70f5 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthSerializationTest.kt @@ -0,0 +1,68 @@ +package com.kizitonwose.calendar.core + +import com.kizitonwose.calendar.core.serializers.YearMonthComponentSerializer +import com.kizitonwose.calendar.core.serializers.YearMonthIso8601Serializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class YearMonthSerializationTest { + private fun iso8601Serialization(serializer: KSerializer) { + for ((localDate, json) in listOf( + Pair(YearMonth(2020, 12), "\"2020-12\""), + Pair(YearMonth(-2020, 1), "\"-2020-01\""), + Pair(YearMonth(2019, 10), "\"2019-10\""), + )) { + assertEquals(json, Json.encodeToString(serializer, localDate)) + assertEquals(localDate, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((localDate, json) in listOf( + Pair(YearMonth(2020, 12), "{\"year\":2020,\"month\":12}"), + Pair(YearMonth(-2020, 1), "{\"year\":-2020,\"month\":1}"), + Pair(YearMonth(2019, 10), "{\"year\":2019,\"month\":10}"), + )) { + assertEquals(json, Json.encodeToString(serializer, localDate)) + assertEquals(localDate, Json.decodeFromString(serializer, json)) + } + // all components must be present + assertFailsWith { + Json.decodeFromString(serializer, "{}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":3}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"month\":3}") + } + // invalid values must fail to construct + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":1000000000000,\"month\":3}") + } + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":2020,\"month\":30}") + } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(YearMonthIso8601Serializer) + } + + @Test + fun testComponentSerialization() { + componentSerialization(YearMonthComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO 8601 + iso8601Serialization(Json.serializersModule.serializer()) + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt index 77480819..c72a5b64 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -172,4 +172,30 @@ class YearMonthTest { assertEquals(previous, start.previous) } } + + @Test + fun monthNumber() { + val values = listOf( + YearMonth(2025, Month.JANUARY) to 1, + YearMonth(1999, Month.JUNE) to 6, + ) + + for ((value, result) in values) { + assertEquals(result, value.monthNumber) + } + } + + @Test + fun toIso8601String() { + val values = listOf( + YearMonth(2025, Month.JANUARY) to "2025-01", + YearMonth(-1999, Month.JUNE) to "-1999-06", + YearMonth(1, Month.AUGUST) to "0001-08", + YearMonth(0, Month.MARCH) to "0000-03", + ) + + for ((value, result) in values) { + assertEquals(result, value.toString()) + } + } } diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearSerializationTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearSerializationTest.kt new file mode 100644 index 00000000..a1107458 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearSerializationTest.kt @@ -0,0 +1,59 @@ +package com.kizitonwose.calendar.core + +import com.kizitonwose.calendar.core.serializers.YearComponentSerializer +import com.kizitonwose.calendar.core.serializers.YearIso8601Serializer +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class YearSerializationTest { + private fun iso8601Serialization(serializer: KSerializer) { + for ((localDate, json) in listOf( + Pair(Year(2020), "\"2020\""), + Pair(Year(-2020), "\"-2020\""), + Pair(Year(2019), "\"2019\""), + )) { + assertEquals(json, Json.encodeToString(serializer, localDate)) + assertEquals(localDate, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((localDate, json) in listOf( + Pair(Year(2020), "{\"year\":2020}"), + Pair(Year(-2020), "{\"year\":-2020}"), + Pair(Year(2019), "{\"year\":2019}"), + )) { + assertEquals(json, Json.encodeToString(serializer, localDate)) + assertEquals(localDate, Json.decodeFromString(serializer, json)) + } + // all components must be present + assertFailsWith { + Json.decodeFromString(serializer, "{}") + } + // invalid values must fail to construct + assertFailsWith { + Json.decodeFromString(serializer, "{\"year\":1000000000000}") + } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(YearIso8601Serializer) + } + + @Test + fun testComponentSerialization() { + componentSerialization(YearComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO 8601 + iso8601Serialization(Json.serializersModule.serializer()) + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt index b954195f..7797cb28 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt @@ -201,4 +201,18 @@ class YearTest { assertEquals(Year(start), Year(result).minusYears(-value)) } } + + @Test + fun toIso8601String() { + val values = listOf( + 2024 to "2024", + -1999 to "-1999", + 1 to "0001", + 0 to "0000", + ) + + for ((value, result) in values) { + assertEquals(result, Year(value).toString()) + } + } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index c3daf731..9690897d 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,7 +19,7 @@ kotlin { } dependencies { - implementation(libs.compose.runtime) // Only needed for @Immutable annotation. + compileOnly(libs.compose.runtime) // Only needed for @Immutable annotation. } mavenPublishing { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a01f180d..6b9b83a9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,11 +4,13 @@ kotlin = "2.0.0" compose = "1.7.0-beta05" espresso = "3.6.1" junit5 = "5.10.3" +kotlinxSerializationCore = "1.7.1" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlin" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } @@ -56,3 +58,4 @@ bcv = "org.jetbrains.kotlinx.binary-compatibility-validator:0.15.1" # KMM kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } jetbrainsCompose = { id = "org.jetbrains.compose", version = "1.7.0-alpha01" } +kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } From b53548d61316770e9e316ba509ffa16b86219aaa Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 20:24:18 +0200 Subject: [PATCH 18/48] Remove unnecessary `JvmSerializableLocalDate` class. --- .../compose/weekcalendar/WeekCalendar.kt | 3 +-- .../compose/weekcalendar/WeekCalendarState.kt | 15 +++++-------- .../core/internal/JvmSerializableLocalDate.kt | 22 ------------------- 3 files changed, 7 insertions(+), 33 deletions(-) delete mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/internal/JvmSerializableLocalDate.kt diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt index 11f5cb87..a2430d94 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.draw.clipToBounds import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay -import com.kizitonwose.calendar.core.internal.toJvmSerializableLocalDate @Composable internal fun WeekCalendarImpl( @@ -39,7 +38,7 @@ internal fun WeekCalendarImpl( ) { items( count = state.weekIndexCount, - key = { offset -> state.store[offset].days.first().date.toJvmSerializableLocalDate() }, + key = { offset -> state.store[offset].days.first().date.toString() }, ) { offset -> val week = state.store[offset] Column( diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt index 04ba530a..8c780d1c 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt @@ -21,9 +21,6 @@ import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.atEndOfMonth import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import com.kizitonwose.calendar.core.internal.JvmSerializableLocalDate -import com.kizitonwose.calendar.core.internal.toJvmSerializableLocalDate -import com.kizitonwose.calendar.core.internal.toLocalDate import com.kizitonwose.calendar.core.now import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState @@ -270,9 +267,9 @@ public class WeekCalendarState internal constructor( internal val Saver: Saver = listSaver( save = { listOf( - it.startDate.toJvmSerializableLocalDate(), - it.endDate.toJvmSerializableLocalDate(), - it.firstVisibleWeek.days.first().date.toJvmSerializableLocalDate(), + it.startDate.toString(), + it.endDate.toString(), + it.firstVisibleWeek.days.first().date.toString(), it.firstDayOfWeek, it.listState.firstVisibleItemIndex, it.listState.firstVisibleItemScrollOffset, @@ -280,9 +277,9 @@ public class WeekCalendarState internal constructor( }, restore = { WeekCalendarState( - startDate = (it[0] as JvmSerializableLocalDate).toLocalDate(), - endDate = (it[1] as JvmSerializableLocalDate).toLocalDate(), - firstVisibleWeekDate = (it[2] as JvmSerializableLocalDate).toLocalDate(), + startDate = LocalDate.parse((it[0] as String)), + endDate = LocalDate.parse((it[1] as String)), + firstVisibleWeekDate = LocalDate.parse((it[2] as String)), firstDayOfWeek = it[3] as DayOfWeek, visibleItemState = VisibleItemState( firstVisibleItemIndex = it[4] as Int, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/internal/JvmSerializableLocalDate.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/internal/JvmSerializableLocalDate.kt deleted file mode 100644 index 0d74b643..00000000 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/internal/JvmSerializableLocalDate.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.kizitonwose.calendar.core.internal - -import com.kizitonwose.calendar.core.JvmSerializable -import kotlinx.datetime.LocalDate - -internal data class JvmSerializableLocalDate( - val year: Int, - val monthNumber: Int, - val dayOfMonth: Int, -) : JvmSerializable - -internal fun JvmSerializableLocalDate.toLocalDate() = LocalDate( - year = year, - monthNumber = monthNumber, - dayOfMonth = dayOfMonth, -) - -internal fun LocalDate.toJvmSerializableLocalDate() = JvmSerializableLocalDate( - year = year, - monthNumber = monthNumber, - dayOfMonth = dayOfMonth, -) From fc46d795909463f8adb0a749c016b7b359dc41a9 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 20:36:52 +0200 Subject: [PATCH 19/48] Use iso strings as state keys. --- .../kizitonwose/calendar/compose/CalendarMonths.kt | 3 ++- .../kizitonwose/calendar/compose/CalendarState.kt | 14 ++++++++------ .../compose/heatmapcalendar/HeatMapCalendar.kt | 3 ++- .../heatmapcalendar/HeatMapCalendarState.kt | 14 ++++++++------ .../calendar/compose/weekcalendar/WeekCalendar.kt | 3 ++- .../compose/weekcalendar/WeekCalendarState.kt | 14 ++++++++------ .../com/kizitonwose/calendar/core/format/Format.kt | 14 +++++++++++++- 7 files changed, 43 insertions(+), 22 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt index 1b246f67..0b69bc0a 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.format.toIso8601String @Suppress("FunctionName") internal fun LazyListScope.CalendarMonths( @@ -28,7 +29,7 @@ internal fun LazyListScope.CalendarMonths( ) { items( count = monthCount, - key = { offset -> monthData(offset).yearMonth }, + key = { offset -> monthData(offset).yearMonth.toIso8601String() }, ) { offset -> val month = monthData(offset) val fillHeight = when (contentHeightMode) { diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt index 04b3cb16..b734d45f 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarState.kt @@ -18,6 +18,8 @@ import androidx.compose.runtime.setValue import com.kizitonwose.calendar.core.OutDateStyle import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.format.fromIso8601YearMonth +import com.kizitonwose.calendar.core.format.toIso8601String import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState import com.kizitonwose.calendar.data.checkRange @@ -266,9 +268,9 @@ public class CalendarState internal constructor( internal val Saver: Saver = listSaver( save = { listOf( - it.startMonth, - it.endMonth, - it.firstVisibleMonth.yearMonth, + it.startMonth.toIso8601String(), + it.endMonth.toIso8601String(), + it.firstVisibleMonth.yearMonth.toIso8601String(), it.firstDayOfWeek, it.outDateStyle, it.listState.firstVisibleItemIndex, @@ -277,9 +279,9 @@ public class CalendarState internal constructor( }, restore = { CalendarState( - startMonth = it[0] as YearMonth, - endMonth = it[1] as YearMonth, - firstVisibleMonth = it[2] as YearMonth, + startMonth = (it[0] as String).fromIso8601YearMonth(), + endMonth = (it[1] as String).fromIso8601YearMonth(), + firstVisibleMonth = (it[2] as String).fromIso8601YearMonth(), firstDayOfWeek = it[3] as DayOfWeek, outDateStyle = it[4] as OutDateStyle, visibleItemState = VisibleItemState( diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendar.kt index 875aeb46..a8d95521 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendar.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.format.toIso8601String import kotlinx.datetime.DayOfWeek @Composable @@ -46,7 +47,7 @@ internal fun HeatMapCalendarImpl( ) { items( count = state.calendarInfo.indexCount, - key = { offset -> state.store[offset].yearMonth }, + key = { offset -> state.store[offset].yearMonth.toIso8601String() }, ) { offset -> val calendarMonth = state.store[offset] Column(modifier = Modifier.width(IntrinsicSize.Max)) { diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt index 546050a3..34c33426 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState.kt @@ -19,6 +19,8 @@ import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.format.fromIso8601YearMonth +import com.kizitonwose.calendar.core.format.toIso8601String import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState import com.kizitonwose.calendar.data.checkRange @@ -239,9 +241,9 @@ public class HeatMapCalendarState internal constructor( internal val Saver: Saver = listSaver( save = { listOf( - it.startMonth, - it.endMonth, - it.firstVisibleMonth.yearMonth, + it.startMonth.toIso8601String(), + it.endMonth.toIso8601String(), + it.firstVisibleMonth.yearMonth.toIso8601String(), it.firstDayOfWeek, it.listState.firstVisibleItemIndex, it.listState.firstVisibleItemScrollOffset, @@ -249,9 +251,9 @@ public class HeatMapCalendarState internal constructor( }, restore = { HeatMapCalendarState( - startMonth = it[0] as YearMonth, - endMonth = it[1] as YearMonth, - firstVisibleMonth = it[2] as YearMonth, + startMonth = (it[0] as String).fromIso8601YearMonth(), + endMonth = (it[1] as String).fromIso8601YearMonth(), + firstVisibleMonth = (it[2] as String).fromIso8601YearMonth(), firstDayOfWeek = it[3] as DayOfWeek, visibleItemState = VisibleItemState( firstVisibleItemIndex = it[4] as Int, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt index a2430d94..91a71113 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendar.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.draw.clipToBounds import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay +import com.kizitonwose.calendar.core.format.toIso8601String @Composable internal fun WeekCalendarImpl( @@ -38,7 +39,7 @@ internal fun WeekCalendarImpl( ) { items( count = state.weekIndexCount, - key = { offset -> state.store[offset].days.first().date.toString() }, + key = { offset -> state.store[offset].days.first().date.toIso8601String() }, ) { offset -> val week = state.store[offset] Column( diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt index 8c780d1c..8b4d56dc 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState.kt @@ -21,6 +21,8 @@ import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.atEndOfMonth import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.format.fromIso8601LocalDate +import com.kizitonwose.calendar.core.format.toIso8601String import com.kizitonwose.calendar.core.now import com.kizitonwose.calendar.data.DataStore import com.kizitonwose.calendar.data.VisibleItemState @@ -267,9 +269,9 @@ public class WeekCalendarState internal constructor( internal val Saver: Saver = listSaver( save = { listOf( - it.startDate.toString(), - it.endDate.toString(), - it.firstVisibleWeek.days.first().date.toString(), + it.startDate.toIso8601String(), + it.endDate.toIso8601String(), + it.firstVisibleWeek.days.first().date.toIso8601String(), it.firstDayOfWeek, it.listState.firstVisibleItemIndex, it.listState.firstVisibleItemScrollOffset, @@ -277,9 +279,9 @@ public class WeekCalendarState internal constructor( }, restore = { WeekCalendarState( - startDate = LocalDate.parse((it[0] as String)), - endDate = LocalDate.parse((it[1] as String)), - firstVisibleWeekDate = LocalDate.parse((it[2] as String)), + startDate = (it[0] as String).fromIso8601LocalDate(), + endDate = (it[1] as String).fromIso8601LocalDate(), + firstVisibleWeekDate = (it[2] as String).fromIso8601LocalDate(), firstDayOfWeek = it[3] as DayOfWeek, visibleItemState = VisibleItemState( firstVisibleItemIndex = it[4] as Int, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt index 705a92c3..d63a5641 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt @@ -4,6 +4,7 @@ import com.kizitonwose.calendar.core.Year import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.atMonth import com.kizitonwose.calendar.core.atStartOfMonth +import com.kizitonwose.calendar.core.yearMonth import kotlinx.datetime.LocalDate import kotlinx.datetime.format.char @@ -15,6 +16,17 @@ internal val ISO_YEAR by lazy { LocalDate.Format { year() } } -internal fun Year.toIso8601String() = ISO_YEAR.format(atMonth(1).atStartOfMonth()) +internal fun LocalDate.toIso8601String() = LocalDate.Formats.ISO.format(this) internal fun YearMonth.toIso8601String() = ISO_YEAR_MONTH.format(atStartOfMonth()) + +internal fun Year.toIso8601String() = ISO_YEAR.format(atMonth(1).atStartOfMonth()) + +internal fun String.fromIso8601LocalDate(): LocalDate = + LocalDate.parse(this, LocalDate.Formats.ISO) + +internal fun String.fromIso8601YearMonth(): YearMonth = + LocalDate.parse("$this-01", LocalDate.Formats.ISO).yearMonth + +internal fun String.fromIso8601Year(): Year = + Year(LocalDate.parse("$this-01-01", LocalDate.Formats.ISO).year) From 1a54da294687615fd3f1f711e7b6fdca8265a23d Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 20:38:59 +0200 Subject: [PATCH 20/48] Remove JvmSerializable --- .../kotlin/com/kizitonwose/calendar/core/JvmSerializable.kt | 3 --- .../commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt | 2 +- .../kotlin/com/kizitonwose/calendar/core/YearMonth.kt | 2 +- .../com/kizitonwose/calendar/core/JvmSerializable.jvm.kt | 3 --- .../com/kizitonwose/calendar/core/JvmSerializable.nonJvm.kt | 3 --- 5 files changed, 2 insertions(+), 11 deletions(-) delete mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.kt delete mode 100644 compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.jvm.kt delete mode 100644 compose-multiplatform/library/src/nonJvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.nonJvm.kt diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.kt deleted file mode 100644 index dba7ae32..00000000 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kizitonwose.calendar.core - -public expect interface JvmSerializable diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt index fcac2382..935c574c 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -11,7 +11,7 @@ import kotlinx.serialization.Serializable @Immutable @Serializable(with = YearIso8601Serializer::class) -public data class Year(val value: Int) : Comparable, JvmSerializable { +public data class Year(val value: Int) : Comparable { internal val year = value init { diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt index 2f1b7985..437fa3cd 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt @@ -17,7 +17,7 @@ import kotlinx.serialization.Serializable @Immutable @Serializable(with = YearMonthIso8601Serializer::class) -public data class YearMonth(val year: Int, val month: Month) : Comparable, JvmSerializable { +public data class YearMonth(val year: Int, val month: Month) : Comparable { public constructor(year: Int, monthNumber: Int) : this(year = year, month = Month(monthNumber)) diff --git a/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.jvm.kt b/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.jvm.kt deleted file mode 100644 index 199695a3..00000000 --- a/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.jvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kizitonwose.calendar.core - -public actual typealias JvmSerializable = java.io.Serializable diff --git a/compose-multiplatform/library/src/nonJvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.nonJvm.kt b/compose-multiplatform/library/src/nonJvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.nonJvm.kt deleted file mode 100644 index 7147a4bd..00000000 --- a/compose-multiplatform/library/src/nonJvmMain/kotlin/com/kizitonwose/calendar/core/JvmSerializable.nonJvm.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kizitonwose.calendar.core - -public actual interface JvmSerializable From 8b48ee1a74cd72942499ec4fa5087a58ae8d3db8 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Tue, 23 Jul 2024 21:03:05 +0200 Subject: [PATCH 21/48] Simplify serializers --- .../kizitonwose/calendar/core/format/Format.kt | 16 ++++++++++------ .../core/serializers/YearMonthSerializers.kt | 5 ++--- .../calendar/core/serializers/YearSerializers.kt | 4 ++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt index d63a5641..9910ea67 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt @@ -8,25 +8,29 @@ import com.kizitonwose.calendar.core.yearMonth import kotlinx.datetime.LocalDate import kotlinx.datetime.format.char -internal val ISO_YEAR_MONTH by lazy { +private val ISO_YEAR_MONTH by lazy { LocalDate.Format { year(); char('-'); monthNumber() } } -internal val ISO_YEAR by lazy { +private val ISO_YEAR by lazy { LocalDate.Format { year() } } -internal fun LocalDate.toIso8601String() = LocalDate.Formats.ISO.format(this) +private val ISO_LOCAL_DATE by lazy { + LocalDate.Formats.ISO +} + +internal fun LocalDate.toIso8601String() = ISO_LOCAL_DATE.format(this) internal fun YearMonth.toIso8601String() = ISO_YEAR_MONTH.format(atStartOfMonth()) internal fun Year.toIso8601String() = ISO_YEAR.format(atMonth(1).atStartOfMonth()) internal fun String.fromIso8601LocalDate(): LocalDate = - LocalDate.parse(this, LocalDate.Formats.ISO) + LocalDate.parse(this, ISO_LOCAL_DATE) internal fun String.fromIso8601YearMonth(): YearMonth = - LocalDate.parse("$this-01", LocalDate.Formats.ISO).yearMonth + LocalDate.parse("$this-01", ISO_LOCAL_DATE).yearMonth internal fun String.fromIso8601Year(): Year = - Year(LocalDate.parse("$this-01-01", LocalDate.Formats.ISO).year) + Year(LocalDate.parse("$this-01-01", ISO_LOCAL_DATE).year) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt index ccd0b8f0..b4025eeb 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt @@ -1,9 +1,8 @@ package com.kizitonwose.calendar.core.serializers import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.format.fromIso8601YearMonth import com.kizitonwose.calendar.core.format.toIso8601String -import com.kizitonwose.calendar.core.yearMonth -import kotlinx.datetime.LocalDate import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.MissingFieldException @@ -32,7 +31,7 @@ public object YearMonthIso8601Serializer : KSerializer { PrimitiveSerialDescriptor("com.kizitonwose.calendar.core.YearMonth", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): YearMonth = - LocalDate.parse("${decoder.decodeString()}-01", LocalDate.Formats.ISO).yearMonth + decoder.decodeString().fromIso8601YearMonth() override fun serialize(encoder: Encoder, value: YearMonth) { encoder.encodeString(value.toIso8601String()) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt index f521df78..a54fb9f8 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt @@ -1,8 +1,8 @@ package com.kizitonwose.calendar.core.serializers import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.format.fromIso8601Year import com.kizitonwose.calendar.core.format.toIso8601String -import kotlinx.datetime.LocalDate import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.MissingFieldException @@ -31,7 +31,7 @@ public object YearIso8601Serializer : KSerializer { PrimitiveSerialDescriptor("com.kizitonwose.calendar.core.Year", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): Year = - Year(LocalDate.parse("${decoder.decodeString()}-01-01", LocalDate.Formats.ISO).year) + decoder.decodeString().fromIso8601Year() override fun serialize(encoder: Encoder, value: Year) { encoder.encodeString(value.toIso8601String()) From 62df3926b755b1f411d3d6ace8a00bc86b53f573 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 09:10:29 +0200 Subject: [PATCH 22/48] Allow external string to Year/YearMonth conversion. --- .../com/kizitonwose/calendar/core/Year.kt | 18 +++++++++++++++++ .../kizitonwose/calendar/core/YearMonth.kt | 20 +++++++++++++++++++ .../calendar/core/YearMonthTest.kt | 19 ++++++++++++++++++ .../com/kizitonwose/calendar/core/YearTest.kt | 18 +++++++++++++++++ 4 files changed, 75 insertions(+) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt index 935c574c..f6a43968 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -1,6 +1,7 @@ package com.kizitonwose.calendar.core import androidx.compose.runtime.Immutable +import com.kizitonwose.calendar.core.format.fromIso8601Year import com.kizitonwose.calendar.core.format.toIso8601String import com.kizitonwose.calendar.core.serializers.YearIso8601Serializer import kotlinx.datetime.Clock @@ -63,6 +64,23 @@ public data class Year(val value: Int) : Comparable { val prolepticYear: Long = year.toLong() return prolepticYear and 3 == 0L && (prolepticYear % 100 != 0L || prolepticYear % 400 == 0L) } + + /** + * Obtains an instance of [Year] from a text string such as `2020`. + * + * The string format must be `yyyy`, ideally obtained from calling [Year.toString]. + * + * @throws IllegalArgumentException if the text cannot be parsed or the boundaries of [Year] are exceeded. + * + * @see Year.toString + */ + public fun parseIso8601(string: String): Year { + return try { + string.fromIso8601Year() + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid Year value $string", e) + } + } } } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt index 437fa3cd..fd51f06e 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt @@ -1,6 +1,7 @@ package com.kizitonwose.calendar.core import androidx.compose.runtime.Immutable +import com.kizitonwose.calendar.core.format.fromIso8601YearMonth import com.kizitonwose.calendar.core.format.toIso8601String import com.kizitonwose.calendar.core.serializers.YearMonthIso8601Serializer import kotlinx.datetime.Clock @@ -57,10 +58,29 @@ public data class YearMonth(val year: Int, val month: Month) : Comparable Date: Wed, 24 Jul 2024 10:40:20 +0200 Subject: [PATCH 23/48] Expose basic arithmetic utilities. --- .../kizitonwose/calendar/core/Extensions.kt | 74 ++++++++++++++++--- .../kizitonwose/calendar/core/YearMonth.kt | 42 +++++++++-- .../calendar/core/ExtensionTest.kt | 16 ++++ .../calendar/core/YearMonthTest.kt | 24 ------ .../kotlin/ContinuousSelectionHelper.kt | 2 - .../src/commonMain/kotlin/Example1Page.kt | 4 +- .../src/commonMain/kotlin/Example2Page.kt | 1 + .../src/commonMain/kotlin/Example3Page.kt | 4 +- .../src/commonMain/kotlin/Example4Page.kt | 3 +- .../src/commonMain/kotlin/Example5Page.kt | 2 + .../src/commonMain/kotlin/Example6Page.kt | 1 + .../src/commonMain/kotlin/Example7Page.kt | 3 +- .../src/commonMain/kotlin/Example9Page.kt | 6 +- .../kotlin/Example9PageAnimatedVisibility.kt | 2 + .../sample/src/commonMain/kotlin/Flight.kt | 4 +- .../sample/src/commonMain/kotlin/Utils.kt | 10 +-- docs/Compose.md | 9 --- 17 files changed, 138 insertions(+), 69 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt index 71fcd83f..ee33b71c 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt @@ -2,6 +2,7 @@ package com.kizitonwose.calendar.core import androidx.compose.ui.text.intl.Locale import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeArithmeticException import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate @@ -46,24 +47,77 @@ public fun LocalDate.Companion.now( public val LocalDate.yearMonth: YearMonth get() = YearMonth(year, month) -internal fun YearMonth.plusMonths(value: Int): YearMonth = plus(value, DateTimeUnit.MONTH) +/** + * Returns a [LocalDate] that results from adding the [value] number of + * days to this date. + * + * If the [value] is positive, the returned date is later than this date. + * If the [value] is negative, the returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.plusDays(value: Int): LocalDate = plus(value, DateTimeUnit.DAY) + +/** + * Returns a [LocalDate] that results from subtracting the [value] number of + * days from this date. + * + * If the [value] is positive, the returned date is later than this date. + * If the [value] is negative, the returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.minusDays(value: Int): LocalDate = minus(value, DateTimeUnit.DAY) + +/** + * Returns a [LocalDate] that results from adding the [value] number of + * months to this date. + * + * If the [value] is positive, the returned date is later than this date. + * If the [value] is negative, the returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.plusMonths(value: Int): LocalDate = plus(value, DateTimeUnit.MONTH) -internal fun YearMonth.minusMonths(value: Int): YearMonth = minus(value, DateTimeUnit.MONTH) +/** + * Returns a [LocalDate] that results from subtracting the [value] number of + * months from this date. + * + * If the [value] is positive, the returned date is later than this date. + * If the [value] is negative, the returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.minusMonths(value: Int): LocalDate = minus(value, DateTimeUnit.MONTH) -internal fun LocalDate.plusDays(value: Int): LocalDate = plus(value, DateTimeUnit.DAY) +/** + * Returns a [LocalDate] that results from adding the [value] number of + * years to this date. + * + * If the [value] is positive, the returned date is later than this date. + * If the [value] is negative, the returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.plusYears(value: Int): LocalDate = plus(value, DateTimeUnit.YEAR) -internal fun LocalDate.minusDays(value: Int): LocalDate = minus(value, DateTimeUnit.DAY) +/** + * Returns a [LocalDate] that results from subtracting the [value] number of + * years from this date. + * + * If the [value] is positive, the returned date is later than this date. + * If the [value] is negative, the returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.minusYears(value: Int): LocalDate = minus(value, DateTimeUnit.YEAR) internal fun LocalDate.plusWeeks(value: Int): LocalDate = plus(value, DateTimeUnit.WEEK) internal fun LocalDate.minusWeeks(value: Int): LocalDate = minus(value, DateTimeUnit.WEEK) -internal fun LocalDate.plusMonths(value: Int): LocalDate = plus(value, DateTimeUnit.MONTH) - -internal fun LocalDate.minusMonths(value: Int): LocalDate = minus(value, DateTimeUnit.MONTH) - -internal fun LocalDate.weeksUntil(other: LocalDate): Int = - until(other, DateTimeUnit.WEEK) +internal fun LocalDate.weeksUntil(other: LocalDate): Int = until(other, DateTimeUnit.WEEK) // E.g DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY) = 3 internal fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (other.isoDayNumber - isoDayNumber)) % 7 diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt index fd51f06e..b5856cf5 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/YearMonth.kt @@ -162,23 +162,49 @@ public fun YearMonth.minus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth atStartOfMonth().minus(value, unit).yearMonth /** - * Returns a [YearMonth] that results from adding the 1 month this year-month. + * Returns a [YearMonth] that results from adding the [value] number of months + * to this year-month. + * + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. * * @throws DateTimeArithmeticException if the result exceeds the boundaries * of [YearMonth] which is essentially the [LocalDate] boundaries. + */ +public fun YearMonth.plusMonths(value: Int): YearMonth = plus(value, DateTimeUnit.MONTH) + +/** + * Returns a [YearMonth] that results from subtracting the [value] number of months + * from this year-month. * - * @see YearMonth.plus + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries + * of [YearMonth] which is essentially the [LocalDate] boundaries. */ -public val YearMonth.next: YearMonth - get() = this.plus(1, DateTimeUnit.MONTH) +public fun YearMonth.minusMonths(value: Int): YearMonth = minus(value, DateTimeUnit.MONTH) /** - * Returns a [YearMonth] that results from subtracting the 1 month this year-month. + * Returns a [YearMonth] that results from adding the [value] number of years + * to this year-month. + * + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. * * @throws DateTimeArithmeticException if the result exceeds the boundaries * of [YearMonth] which is essentially the [LocalDate] boundaries. + */ +public fun YearMonth.plusYears(value: Int): YearMonth = plus(value, DateTimeUnit.YEAR) + +/** + * Returns a [YearMonth] that results from subtracting the [value] number of years + * from this year-month. * - * @see YearMonth.minus + * If the [value] is positive, the returned year-month is later than this year-month. + * If the [value] is negative, the returned year-month is earlier than this year-month. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries + * of [YearMonth] which is essentially the [LocalDate] boundaries. */ -public val YearMonth.previous: YearMonth - get() = this.minus(1, DateTimeUnit.MONTH) +public fun YearMonth.minusYears(value: Int): YearMonth = minus(value, DateTimeUnit.YEAR) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt index 71a34618..5acbd617 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt @@ -17,6 +17,14 @@ class ExtensionTest { YearMonth(2024, Month.MAY), YearMonth(2024, Month.JUNE).minusMonths(1), ) + assertEquals( + YearMonth(2025, Month.JUNE), + YearMonth(2024, Month.JUNE).plusYears(1), + ) + assertEquals( + YearMonth(2023, Month.MAY), + YearMonth(2024, Month.MAY).minusYears(1), + ) assertEquals( LocalDate(2024, Month.MAY, 2), LocalDate(2024, Month.MAY, 1).plusDays(1), @@ -33,6 +41,14 @@ class ExtensionTest { LocalDate(2024, Month.FEBRUARY, 29), LocalDate(2024, Month.MARCH, 30).minusMonths(1), ) + assertEquals( + LocalDate(2026, Month.JUNE, 2), + LocalDate(2025, Month.JUNE, 2).plusYears(1), + ) + assertEquals( + LocalDate(2023, Month.FEBRUARY, 28), + LocalDate(2024, Month.FEBRUARY, 29).minusYears(1), + ) assertEquals( LocalDate(2024, Month.MAY, 9), LocalDate(2024, Month.MAY, 2).plusWeeks(1), diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt index 08d2776d..2409cce1 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -150,30 +150,6 @@ class YearMonthTest { } } - @Test - fun next() { - val values = listOf( - YearMonth(2024, Month.DECEMBER) to YearMonth(2025, Month.JANUARY), - YearMonth(2020, Month.MAY) to YearMonth(2020, Month.JUNE), - ) - - for ((start, next) in values) { - assertEquals(next, start.next) - } - } - - @Test - fun previous() { - val values = listOf( - YearMonth(2025, Month.JANUARY) to YearMonth(2024, Month.DECEMBER), - YearMonth(2020, Month.JUNE) to YearMonth(2020, Month.MAY), - ) - - for ((start, previous) in values) { - assertEquals(previous, start.previous) - } - } - @Test fun monthNumber() { val values = listOf( diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/ContinuousSelectionHelper.kt b/compose-multiplatform/sample/src/commonMain/kotlin/ContinuousSelectionHelper.kt index 4604cd06..e8fdaabb 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/ContinuousSelectionHelper.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/ContinuousSelectionHelper.kt @@ -1,7 +1,5 @@ import com.kizitonwose.calendar.core.atEndOfMonth import com.kizitonwose.calendar.core.atStartOfMonth -import com.kizitonwose.calendar.core.next -import com.kizitonwose.calendar.core.previous import com.kizitonwose.calendar.core.yearMonth import kotlinx.datetime.LocalDate import kotlinx.datetime.daysUntil diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt index 316baaa2..f24ce49f 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt @@ -27,8 +27,8 @@ import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.daysOfWeek -import com.kizitonwose.calendar.core.next -import com.kizitonwose.calendar.core.previous +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.plusMonths import kotlinx.coroutines.launch import kotlinx.datetime.DayOfWeek import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example2Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example2Page.kt index c8356c50..a050dd56 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example2Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example2Page.kt @@ -46,6 +46,7 @@ import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusMonths import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt index 351205df..7e652980 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt @@ -48,8 +48,8 @@ import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.OutDateStyle import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.daysOfWeek -import com.kizitonwose.calendar.core.next -import com.kizitonwose.calendar.core.previous +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.plusMonths import kotlinx.coroutines.launch import kotlinx.datetime.DayOfWeek import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example4Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example4Page.kt index c57b5c97..e08e587e 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example4Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example4Page.kt @@ -1,3 +1,4 @@ + import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -33,7 +34,7 @@ import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale -import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusMonths import org.jetbrains.compose.ui.tooling.preview.Preview @Composable diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example5Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example5Page.kt index 6298e1a6..9a495f6d 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example5Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example5Page.kt @@ -23,7 +23,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kizitonwose.calendar.compose.WeekCalendar import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import com.kizitonwose.calendar.core.minusDays import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays import kotlinx.datetime.LocalDate import kotlinx.datetime.format.Padding import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt index 0350e8aa..2aecec71 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt @@ -41,6 +41,7 @@ import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays import com.kizitonwose.calendar.core.yearMonth import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt index aa93c795..eb45eca1 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt @@ -1,4 +1,3 @@ - import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -29,7 +28,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kizitonwose.calendar.compose.WeekCalendar import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import com.kizitonwose.calendar.core.minusDays import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays import kotlinx.datetime.LocalDate import kotlinx.datetime.format.Padding import org.jetbrains.compose.ui.tooling.preview.Preview diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt index d548d25b..1a842fa0 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt @@ -55,9 +55,11 @@ import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.atEndOfMonth import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.daysOfWeek -import com.kizitonwose.calendar.core.next +import com.kizitonwose.calendar.core.minusDays +import com.kizitonwose.calendar.core.minusMonths import com.kizitonwose.calendar.core.now -import com.kizitonwose.calendar.core.previous +import com.kizitonwose.calendar.core.plusDays +import com.kizitonwose.calendar.core.plusMonths import com.kizitonwose.calendar.core.yearMonth import kotlinx.coroutines.launch import kotlinx.datetime.DayOfWeek diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example9PageAnimatedVisibility.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example9PageAnimatedVisibility.kt index 078b5b74..16a36949 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example9PageAnimatedVisibility.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example9PageAnimatedVisibility.kt @@ -28,7 +28,9 @@ import com.kizitonwose.calendar.core.WeekDayPosition import com.kizitonwose.calendar.core.atEndOfMonth import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.minusMonths import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusMonths import com.kizitonwose.calendar.core.yearMonth import kotlinx.coroutines.launch import kotlinx.datetime.LocalDate diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Flight.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Flight.kt index 8e68fafe..2cbb4087 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Flight.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Flight.kt @@ -1,7 +1,9 @@ + import androidx.compose.ui.graphics.Color import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.atDay -import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.plusMonths import kotlinx.datetime.LocalDateTime import kotlinx.datetime.atTime import kotlinx.datetime.format.DayOfWeekNames diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt index 11e85a6f..02ced119 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt @@ -1,3 +1,4 @@ + import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -33,9 +34,6 @@ import com.kizitonwose.calendar.core.plus import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.datetime.DateTimeUnit -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus -import kotlinx.datetime.plus fun Modifier.clickable( enabled: Boolean = true, @@ -181,7 +179,5 @@ private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50 } } -internal fun LocalDate.plusDays(value: Int): LocalDate = plus(value, DateTimeUnit.DAY) -internal fun LocalDate.minusDays(value: Int): LocalDate = minus(value, DateTimeUnit.DAY) -internal fun YearMonth.plusMonths(value: Int): YearMonth = plus(value, DateTimeUnit.MONTH) -internal fun YearMonth.minusMonths(value: Int): YearMonth = minus(value, DateTimeUnit.MONTH) +val YearMonth.next: YearMonth get() = this.plus(1, DateTimeUnit.MONTH) +val YearMonth.previous: YearMonth get() = this.minus(1, DateTimeUnit.MONTH) diff --git a/docs/Compose.md b/docs/Compose.md index 463b2a18..ae77a96e 100644 --- a/docs/Compose.md +++ b/docs/Compose.md @@ -43,15 +43,6 @@ The APIs for the compose libraries for Android and Multiplatform projects have b Note that the `YearMonth` class does not yet exist in the `kotlinx-datetime` library, therefore the multiplatfrom calendar library includes a minimal `YearMonth` class implementation to bridge this gap until the class [is hopefully added](https://github.com/Kotlin/kotlinx-datetime/issues/168) to the `kotlinx-datetime` library. -The functions `plusMonths`, `minusMonths`, `plusDays` and `minusDays` used in the examples are provided out of the box by the `java.time` library, but can be easily added for multiplatform projects using the `kotlinx-datetime` library if needed: - -```kotlin -fun YearMonth.plusMonths(value: Int): YearMonth = plus(value, DateTimeUnit.MONTH) -fun YearMonth.minusMonths(value: Int): YearMonth = minus(value, DateTimeUnit.MONTH) -fun LocalDate.plusDays(value: Int): LocalDate = plus(value, DateTimeUnit.DAY) -fun LocalDate.minusDays(value: Int): LocalDate = minus(value, DateTimeUnit.DAY) -``` - ## Compose UI version compatibility Ensure that you are using the library version that matches the Compose UI version in your project. If you use a version of the library that has a higher version of Compose UI than the one in your project, gradle will upgrade the Compose UI version in your project via transitive dependency. See the compatibility table [here](https://github.com/kizitonwose/Calendar#compose-ui-version-compatibility). From 32eb5a4e9ba797d7197194f0555e348915a0e23e Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 10:49:40 +0200 Subject: [PATCH 24/48] Clean up tests --- .../calendar/core/YearMonthTest.kt | 34 ++++++------------- .../com/kizitonwose/calendar/core/YearTest.kt | 32 ++++++----------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt index 2409cce1..a456a3e4 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -44,35 +44,29 @@ class YearMonthTest { @Test fun atStartOfMonth() { - val values = listOf( + for ((yearMonth, firstDay) in listOf( YearMonth(2025, Month.JANUARY) to LocalDate(2025, Month.JANUARY, 1), YearMonth(2020, Month.JUNE) to LocalDate(2020, Month.JUNE, 1), - ) - - for ((yearMonth, firstDay) in values) { + )) { assertEquals(yearMonth.atStartOfMonth(), firstDay) } } @Test fun atEndOfMonth() { - val values = listOf( + for ((yearMonth, lastDay) in listOf( YearMonth(2025, Month.JANUARY) to LocalDate(2025, Month.JANUARY, 31), YearMonth(2024, Month.JUNE) to LocalDate(2024, Month.JUNE, 30), YearMonth(2025, Month.FEBRUARY) to LocalDate(2025, Month.FEBRUARY, 28), YearMonth(2024, Month.FEBRUARY) to LocalDate(2024, Month.FEBRUARY, 29), - ) - - for ((yearMonth, lastDay) in values) { + )) { assertEquals(yearMonth.atEndOfMonth(), lastDay) } } @Test fun atDay() { - val yearMonthValues = Month.entries.map { Year(2024).atMonth(it) } - - for (yearMonth in yearMonthValues) { + for (yearMonth in Month.entries.map { Year(2024).atMonth(it) }) { for (day in 1..yearMonth.lengthOfMonth()) { assertEquals(LocalDate(yearMonth.year, yearMonth.month, day), yearMonth.atDay(day)) } @@ -81,7 +75,7 @@ class YearMonthTest { @Test fun monthsUntil() { - val values = listOf( + for ((start, end, result) in listOf( YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.NOVEMBER) toTriple 10, YearMonth(2024, Month.JANUARY) to YearMonth(2024, Month.DECEMBER) toTriple 11, YearMonth(2026, Month.MARCH) to YearMonth(2028, Month.FEBRUARY) toTriple 23, @@ -90,9 +84,7 @@ class YearMonthTest { YearMonth(2020, Month.MAY) to YearMonth(2023, Month.JULY) toTriple 38, YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.AUGUST) toTriple 0, YearMonth(2022, Month.AUGUST) to YearMonth(2022, Month.SEPTEMBER) toTriple 1, - ) - - for ((start, end, result) in values) { + )) { assertEquals(result, start.monthsUntil(end)) assertEquals(-result, end.monthsUntil(start)) } @@ -152,26 +144,22 @@ class YearMonthTest { @Test fun monthNumber() { - val values = listOf( + for ((value, result) in listOf( YearMonth(2025, Month.JANUARY) to 1, YearMonth(1999, Month.JUNE) to 6, - ) - - for ((value, result) in values) { + )) { assertEquals(result, value.monthNumber) } } @Test fun toIso8601String() { - val values = listOf( + for ((value, result) in listOf( YearMonth(2025, Month.JANUARY) to "2025-01", YearMonth(-1999, Month.JUNE) to "-1999-06", YearMonth(1, Month.AUGUST) to "0001-08", YearMonth(0, Month.MARCH) to "0000-03", - ) - - for ((value, result) in values) { + )) { assertEquals(result, value.toString()) } } diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt index 8018178c..59a43a43 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt @@ -59,9 +59,7 @@ class YearTest { @Test fun atMonth() { - val years = listOf(0, -400, 2024, 1, 1999) - - for (year in years) { + for (year in listOf(0, -400, 2024, 1, 1999)) { for (month in Month.entries) { assertEquals(YearMonth(year, month), Year(year).atMonth(month)) } @@ -70,9 +68,7 @@ class YearTest { @Test fun atMonthNumber() { - val years = listOf(0, -400, 2024, 1, 1999) - - for (year in years) { + for (year in listOf(0, -400, 2024, 1, 1999)) { for (month in Month.entries) { assertEquals(YearMonth(year, month.number), Year(year).atMonth(month)) } @@ -159,14 +155,12 @@ class YearTest { @Test fun yearsUntil() { - val values = listOf( + for ((start, end, result) in listOf( 2020 to 2024 toTriple 4, 2024 to 2030 toTriple 6, 1999 to 2028 toTriple 29, 1300 to 1365 toTriple 65, - ) - - for ((start, end, result) in values) { + )) { assertEquals(result, Year(start).yearsUntil(Year(end))) assertEquals(-result, Year(end).yearsUntil(Year(start))) } @@ -174,14 +168,12 @@ class YearTest { @Test fun plus() { - val values = listOf( + for ((start, value, result) in listOf( 2020 to 4 toTriple 2024, 2024 to 6 toTriple 2030, 1999 to 29 toTriple 2028, 1300 to 65 toTriple 1365, - ) - - for ((start, value, result) in values) { + )) { assertEquals(Year(result), Year(start).plusYears(value)) assertEquals(Year(start), Year(result).plusYears(-value)) } @@ -189,14 +181,12 @@ class YearTest { @Test fun minus() { - val values = listOf( + for ((start, value, result) in listOf( 2020 to 4 toTriple 2016, 2024 to 6 toTriple 2018, 1999 to 29 toTriple 1970, 1300 to 65 toTriple 1235, - ) - - for ((start, value, result) in values) { + )) { assertEquals(Year(result), Year(start).minusYears(value)) assertEquals(Year(start), Year(result).minusYears(-value)) } @@ -204,14 +194,12 @@ class YearTest { @Test fun toIso8601String() { - val values = listOf( + for ((value, result) in listOf( 2024 to "2024", -1999 to "-1999", 1 to "0001", 0 to "0000", - ) - - for ((value, result) in values) { + )) { assertEquals(result, Year(value).toString()) } } From 7cbbcee0a6be2ab467eb0210a7f9f50f7f935e99 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 14:56:36 +0200 Subject: [PATCH 25/48] Expose native/wasm dependency --- compose-multiplatform/library/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compose-multiplatform/library/build.gradle.kts b/compose-multiplatform/library/build.gradle.kts index 97b379a0..3515d712 100644 --- a/compose-multiplatform/library/build.gradle.kts +++ b/compose-multiplatform/library/build.gradle.kts @@ -66,7 +66,9 @@ kotlin { dependsOn(commonMain) nativeMain.dependsOn(this) wasmJsMain.dependsOn(this) - dependencies {} + dependencies { + api(libs.kotlinx.serialization.core) + } } desktopMain.dependsOn(jvmMain) desktopMain.dependencies { From 07ce4115468b7d384262423a9ff9393be54e7fa8 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 15:42:20 +0200 Subject: [PATCH 26/48] Revert multiplatform library to compose 1.6.11 --- .../kizitonwose/calendar/compose/CalendarDefaults.kt | 10 +++++++--- gradle/libs.versions.toml | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt index 5bb39f4e..3856d891 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -22,7 +22,7 @@ internal object CalendarDefaults { @Composable private fun pagedFlingBehavior(state: LazyListState): FlingBehavior { val snappingLayout = remember(state) { - val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) + val provider = SnapLayoutInfoProvider(state, CalendarSnapPositionInLayout()) CalendarSnapLayoutInfoProvider(provider) } return rememberSnapFlingBehavior(snappingLayout) @@ -46,5 +46,9 @@ private fun CalendarSnapLayoutInfoProvider( * In compose 1.3, the default was single page snapping (zero), but this changed * in compose 1.4 to decayed page snapping which is not great for calendar usage. */ - override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f + override fun calculateApproachOffset(initialVelocity: Float): Float = 0f } + +@OptIn(ExperimentalFoundationApi::class) +@Suppress("FunctionName") +private fun CalendarSnapPositionInLayout() = SnapPositionInLayout { _, _, _, _, _ -> 0 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6b9b83a9..735d0f9f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ compose-navigation = { module = "androidx.navigation:navigation-compose", versio compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } -jetbrains-compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.8.0-alpha02" } +jetbrains-compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.7.0-alpha07" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -57,5 +57,5 @@ bcv = "org.jetbrains.kotlinx.binary-compatibility-validator:0.15.1" # KMM kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -jetbrainsCompose = { id = "org.jetbrains.compose", version = "1.7.0-alpha01" } +jetbrainsCompose = { id = "org.jetbrains.compose", version = "1.6.11" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } From 800ac7a2a88d8b0cbf2396a0eb21e67841e09edb Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 16:56:47 +0200 Subject: [PATCH 27/48] Add multiplatform year calendar --- .../kizitonwose/calendar/compose/Calendar.kt | 297 ++++++++++++++++++ .../calendar/compose/CalendarMonths.kt | 2 +- .../yearcalendar/YearCalendarLayoutInfo.kt | 37 +++ .../yearcalendar/YearCalendarMonths.kt | 194 ++++++++++++ .../compose/yearcalendar/YearCalendarState.kt | 296 +++++++++++++++++ .../yearcalendar/YearContentHeightMode.kt | 37 +++ .../kizitonwose/calendar/core/CalendarYear.kt | 43 +++ .../core/serializers/YearSerializers.kt | 2 +- .../com/kizitonwose/calendar/data/WeekData.kt | 12 +- .../com/kizitonwose/calendar/data/YearData.kt | 37 +++ .../kizitonwose/calendar/core/Converters.kt | 2 +- .../compose/yearcalendar/YearCalendarState.kt | 2 +- 12 files changed, 951 insertions(+), 10 deletions(-) create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarYear.kt create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/YearData.kt diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt index cf14adb4..2e88b9a1 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt @@ -7,7 +7,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.kizitonwose.calendar.compose.CalendarDefaults.flingBehavior import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarImpl @@ -18,8 +20,13 @@ import com.kizitonwose.calendar.compose.heatmapcalendar.rememberHeatMapCalendarS import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarImpl import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarMonths +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.CalendarYear import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay import kotlinx.datetime.DayOfWeek @@ -293,3 +300,293 @@ public fun HeatMapCalendar( monthHeader = monthHeader, contentPadding = contentPadding, ) + +/** + * A horizontally scrolling year calendar. + * + * @param modifier the modifier to apply to this calendar. + * @param state the state object to be used to control or observe the calendar's properties. + * Examples: `startYear`, `endYear`, `firstDayOfWeek`, `firstVisibleYear`, `outDateStyle`. + * @param columns the number of months columns in each year on the calendar. + * @param calendarScrollPaged the scrolling behavior of the calendar. When `true`, the calendar will + * snap to the nearest year after a scroll or swipe action. When `false`, the calendar scrolls normally. + * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions + * is allowed. You can still scroll programmatically using the state even when it is disabled. + * @param reverseLayout reverse the direction of scrolling and layout. When `true`, years will be + * composed from the end to the start and [YearCalendarState.startYear] will be located at the end. + * @param contentPadding a padding around the whole calendar. This will add padding for the + * content after it has been clipped, which is not possible via [modifier] param. You can use it + * to add a padding before the first year or after the last one. If you want to add a spacing + * between each year, use the [yearContainer] composable or the [yearBodyContentPadding] parameter. + * @param yearBodyContentPadding a padding around the year body content. Alternatively, you can + * also provide a [yearBody] with the desired padding to achieve the same result. + * @param monthVerticalSpacing the vertical spacing between month rows. + * @param monthHorizontalSpacing the horizontal spacing between month columns. + * @param contentHeightMode Determines how the height of the month and day content is calculated. + * @param isMonthVisible Determines if a month is shown on the calendar grid. For example, you can + * use this to hide all past months. + * @param dayContent a composable block which describes the day content. + * @param monthHeader a composable block which describes the month header content. The header is + * placed above each month on the calendar. + * @param monthBody a composable block which describes the month body content. This is the container + * where all the month days are placed, excluding the header and footer. This is useful if you + * want to customize the day container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param monthFooter a composable block which describes the month footer content. The footer is + * placed below each month on the calendar. + * @param monthContainer a composable block which describes the entire month content. This is the + * container where all the month contents are placed (header => days => footer). This is useful if + * you want to customize the month container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearHeader a composable block which describes the year header content. The header is + * placed above each year on the calendar. + * @param yearBody a composable block which describes the year body content. This is the container + * where all the months in the year are placed, excluding the year header and footer. This is useful + * if you want to customize the month container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearFooter a composable block which describes the year footer content. The footer is + * placed below each year on the calendar. + * @param yearContainer a composable block which describes the entire year content. This is the + * container where all the year contents are placed (header => months => footer). This is useful if + * you want to customize the year container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + */ +@Composable +public fun HorizontalYearCalendar( + modifier: Modifier = Modifier, + state: YearCalendarState = rememberYearCalendarState(), + columns: Int = 3, + calendarScrollPaged: Boolean = true, + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + contentPadding: PaddingValues = PaddingValues(0.dp), + yearBodyContentPadding: PaddingValues = PaddingValues(0.dp), + monthVerticalSpacing: Dp = 0.dp, + monthHorizontalSpacing: Dp = 0.dp, + contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, + isMonthVisible: (month: CalendarMonth) -> Boolean = remember { { true } }, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)? = null, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)? = null, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)? = null, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)? = null, +): Unit = YearCalendar( + modifier = modifier, + state = state, + columns = columns, + calendarScrollPaged = calendarScrollPaged, + userScrollEnabled = userScrollEnabled, + isHorizontal = true, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + yearBodyContentPadding = yearBodyContentPadding, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + contentHeightMode = contentHeightMode, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, +) + +/** + * A vertically scrolling year calendar. + * + * @param modifier the modifier to apply to this calendar. + * @param state the state object to be used to control or observe the calendar's properties. + * Examples: `startYear`, `endYear`, `firstDayOfWeek`, `firstVisibleYear`, `outDateStyle`. + * @param columns the number of months columns in each year on the calendar. + * @param calendarScrollPaged the scrolling behavior of the calendar. When `true`, the calendar will + * snap to the nearest year after a scroll or swipe action. When `false`, the calendar scrolls normally. + * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions + * is allowed. You can still scroll programmatically using the state even when it is disabled. + * @param reverseLayout reverse the direction of scrolling and layout. When `true`, years will be + * composed from the end to the start and [YearCalendarState.startYear] will be located at the end. + * @param contentPadding a padding around the whole calendar. This will add padding for the + * content after it has been clipped, which is not possible via [modifier] param. You can use it + * to add a padding before the first year or after the last one. If you want to add a spacing + * between each year, use the [yearContainer] composable or the [yearBodyContentPadding] parameter. + * @param yearBodyContentPadding a padding around the year body content. Alternatively, you can + * also provide a [yearBody] with the desired padding to achieve the same result. + * @param monthVerticalSpacing the vertical spacing between month rows. + * @param monthHorizontalSpacing the horizontal spacing between month columns. + * @param contentHeightMode Determines how the height of the month and day content is calculated. + * @param isMonthVisible Determines if a month is shown on the calendar grid. For example, you can + * use this to hide all past months. + * @param dayContent a composable block which describes the day content. + * @param monthHeader a composable block which describes the month header content. The header is + * placed above each month on the calendar. + * @param monthBody a composable block which describes the month body content. This is the container + * where all the month days are placed, excluding the header and footer. This is useful if you + * want to customize the day container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param monthFooter a composable block which describes the month footer content. The footer is + * placed below each month on the calendar. + * @param monthContainer a composable block which describes the entire month content. This is the + * container where all the month contents are placed (header => days => footer). This is useful if + * you want to customize the month container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearHeader a composable block which describes the year header content. The header is + * placed above each year on the calendar. + * @param yearBody a composable block which describes the year body content. This is the container + * where all the months in the year are placed, excluding the year header and footer. This is useful + * if you want to customize the month container, for example, with a background color or other effects. + * The actual body content is provided in the block and must be called after your desired + * customisations are rendered. + * @param yearFooter a composable block which describes the year footer content. The footer is + * placed below each year on the calendar. + * @param yearContainer a composable block which describes the entire year content. This is the + * container where all the year contents are placed (header => months => footer). This is useful if + * you want to customize the year container, for example, with a background color or other effects. + * The actual container content is provided in the block and must be called after your desired + * customisations are rendered. + */ +@Composable +public fun VerticalYearCalendar( + modifier: Modifier = Modifier, + state: YearCalendarState = rememberYearCalendarState(), + columns: Int = 3, + calendarScrollPaged: Boolean = true, + userScrollEnabled: Boolean = true, + reverseLayout: Boolean = false, + contentPadding: PaddingValues = PaddingValues(0.dp), + yearBodyContentPadding: PaddingValues = PaddingValues(0.dp), + monthVerticalSpacing: Dp = 0.dp, + monthHorizontalSpacing: Dp = 0.dp, + contentHeightMode: YearContentHeightMode = YearContentHeightMode.Wrap, + isMonthVisible: (month: CalendarMonth) -> Boolean = remember { { true } }, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)? = null, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)? = null, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)? = null, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)? = null, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)? = null, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)? = null, +): Unit = YearCalendar( + modifier = modifier, + state = state, + columns = columns, + calendarScrollPaged = calendarScrollPaged, + userScrollEnabled = userScrollEnabled, + isHorizontal = false, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + yearBodyContentPadding = yearBodyContentPadding, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + contentHeightMode = contentHeightMode, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, +) + +@Composable +private fun YearCalendar( + modifier: Modifier, + state: YearCalendarState, + columns: Int, + calendarScrollPaged: Boolean, + userScrollEnabled: Boolean, + isHorizontal: Boolean, + reverseLayout: Boolean, + contentPadding: PaddingValues, + contentHeightMode: YearContentHeightMode, + monthVerticalSpacing: Dp, + monthHorizontalSpacing: Dp, + yearBodyContentPadding: PaddingValues, + isMonthVisible: (month: CalendarMonth) -> Boolean, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)?, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)?, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)?, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)?, +) { + if (isHorizontal) { + LazyRow( + modifier = modifier, + state = state.listState, + flingBehavior = flingBehavior(calendarScrollPaged, state.listState), + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + ) { + YearCalendarMonths( + yearCount = state.calendarInfo.indexCount, + yearData = { offset -> state.store[offset] }, + columns = columns, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + yearBodyContentPadding = yearBodyContentPadding, + contentHeightMode = contentHeightMode, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, + ) + } + } else { + LazyColumn( + modifier = modifier, + state = state.listState, + flingBehavior = flingBehavior(calendarScrollPaged, state.listState), + userScrollEnabled = userScrollEnabled, + reverseLayout = reverseLayout, + contentPadding = contentPadding, + ) { + YearCalendarMonths( + yearCount = state.calendarInfo.indexCount, + yearData = { offset -> state.store[offset] }, + columns = columns, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + yearBodyContentPadding = yearBodyContentPadding, + contentHeightMode = contentHeightMode, + isMonthVisible = isMonthVisible, + dayContent = dayContent, + monthHeader = monthHeader, + monthBody = monthBody, + monthFooter = monthFooter, + monthContainer = monthContainer, + yearHeader = yearHeader, + yearBody = yearBody, + yearFooter = yearFooter, + yearContainer = yearContainer, + ) + } + } +} diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt index 0b69bc0a..388a0d0d 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarMonths.kt @@ -87,4 +87,4 @@ private val defaultMonthContainer: (@Composable LazyItemScope.(CalendarMonth, co private val defaultMonthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit) = { _, content -> content() } -private fun T?.or(default: T) = this ?: default +internal fun T?.or(default: T) = this ?: default diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt new file mode 100644 index 00000000..d8eda40a --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo.kt @@ -0,0 +1,37 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import com.kizitonwose.calendar.core.CalendarYear + +/** + * Contains useful information about the currently displayed layout state of the calendar. + * For example you can get the list of currently displayed years. + * + * Use [YearCalendarState.layoutInfo] to retrieve this. + * + * @see LazyListLayoutInfo + */ +public class YearCalendarLayoutInfo( + info: LazyListLayoutInfo, + private val getIndexData: (Int) -> CalendarYear, +) : LazyListLayoutInfo by info { + /** + * The list of [YearCalendarItemInfo] representing all the currently visible years. + */ + public val visibleYearsInfo: List + get() = visibleItemsInfo.map { info -> + YearCalendarItemInfo(info, getIndexData(info.index)) + } +} + +/** + * Contains useful information about an individual year on the calendar. + * + * @param year The year in the list. + + * @see YearCalendarLayoutInfo + * @see LazyListItemInfo + */ +public class YearCalendarItemInfo(info: LazyListItemInfo, public val year: CalendarYear) : + LazyListItemInfo by info diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt new file mode 100644 index 00000000..f2366b0b --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarMonths.kt @@ -0,0 +1,194 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.unit.Dp +import com.kizitonwose.calendar.compose.or +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.CalendarYear + +@Suppress("FunctionName") +internal fun LazyListScope.YearCalendarMonths( + yearCount: Int, + yearData: (offset: Int) -> CalendarYear, + columns: Int, + monthVerticalSpacing: Dp, + monthHorizontalSpacing: Dp, + yearBodyContentPadding: PaddingValues, + contentHeightMode: YearContentHeightMode, + isMonthVisible: (month: CalendarMonth) -> Boolean, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit)?, + monthFooter: (@Composable ColumnScope.(CalendarMonth) -> Unit)?, + monthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit)?, + yearHeader: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit)?, + yearFooter: (@Composable ColumnScope.(CalendarYear) -> Unit)?, + yearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit)?, +) { + items( + count = yearCount, + key = { offset -> yearData(offset).year.value }, + ) { yearOffset -> + val year = yearData(yearOffset) + val fillHeight = when (contentHeightMode) { + YearContentHeightMode.Wrap -> false + YearContentHeightMode.Fill, + YearContentHeightMode.Stretch, + -> true + } + val hasYearContainer = yearContainer != null + yearContainer.or(defaultYearContainer)(year) { + Column( + modifier = Modifier + .then(if (hasYearContainer) Modifier.fillMaxWidth() else Modifier.fillParentMaxWidth()) + .then( + if (fillHeight) { + if (hasYearContainer) Modifier.fillMaxHeight() else Modifier.fillParentMaxHeight() + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + val months = year.months.filter(isMonthVisible) + yearHeader?.invoke(this, year) + yearBody.or(defaultYearBody)(year) { + CalendarGrid( + modifier = Modifier + .fillMaxWidth() + .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()) + .padding(yearBodyContentPadding), + columns = columns, + itemCount = months.count(), + fillHeight = fillHeight, + monthVerticalSpacing = monthVerticalSpacing, + monthHorizontalSpacing = monthHorizontalSpacing, + ) { monthOffset -> + val month = months[monthOffset] + val hasContainer = monthContainer != null + monthContainer.or(defaultMonthContainer)(month) { + Column( + modifier = Modifier + .then(if (hasContainer) Modifier.fillMaxWidth() else Modifier) + .then( + if (fillHeight) { + if (hasContainer) Modifier.fillMaxHeight() else Modifier + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + monthHeader?.invoke(this, month) + monthBody.or(defaultMonthBody)(month) { + Column( + modifier = Modifier + .fillMaxWidth() + .then(if (fillHeight) Modifier.weight(1f) else Modifier.wrapContentHeight()), + ) { + for (week in month.weekDays) { + Row( + modifier = Modifier + .fillMaxWidth() + .then( + if (contentHeightMode == YearContentHeightMode.Stretch) { + Modifier.weight(1f) + } else { + Modifier.wrapContentHeight() + }, + ), + ) { + for (day in week) { + Box( + modifier = Modifier + .weight(1f) + .clipToBounds(), + ) { + dayContent(day) + } + } + } + } + } + } + monthFooter?.invoke(this, month) + } + } + } + } + yearFooter?.invoke(this, year) + } + } + } +} + +@Composable +private fun CalendarGrid( + columns: Int, + fillHeight: Boolean, + monthVerticalSpacing: Dp, + monthHorizontalSpacing: Dp, + itemCount: Int, + modifier: Modifier = Modifier, + content: @Composable BoxScope.(Int) -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(monthVerticalSpacing), + ) { + var rows = (itemCount / columns) + if (itemCount.mod(columns) > 0) { + rows += 1 + } + + for (rowId in 0 until rows) { + val firstIndex = rowId * columns + + Row( + modifier = Modifier.then( + if (fillHeight) Modifier.weight(1f) else Modifier, + ), + horizontalArrangement = Arrangement.spacedBy(monthHorizontalSpacing), + ) { + for (columnId in 0 until columns) { + val index = firstIndex + columnId + Box( + modifier = Modifier + .weight(1f), + ) { + if (index < itemCount) { + content(index) + } + } + } + } + } + } +} + +private val defaultYearContainer: (@Composable LazyItemScope.(CalendarYear, container: @Composable () -> Unit) -> Unit) = + { _, container -> container() } + +private val defaultYearBody: (@Composable ColumnScope.(CalendarYear, content: @Composable () -> Unit) -> Unit) = + { _, content -> content() } + +private val defaultMonthContainer: (@Composable BoxScope.(CalendarMonth, container: @Composable () -> Unit) -> Unit) = + { _, container -> container() } + +private val defaultMonthBody: (@Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit) = + { _, content -> content() } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt new file mode 100644 index 00000000..fefc904f --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -0,0 +1,296 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.lazy.LazyListLayoutInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.kizitonwose.calendar.compose.CalendarInfo +import com.kizitonwose.calendar.compose.CalendarLayoutInfo +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.data.DataStore +import com.kizitonwose.calendar.data.VisibleItemState +import com.kizitonwose.calendar.data.checkRange +import com.kizitonwose.calendar.data.getCalendarYearData +import com.kizitonwose.calendar.data.getYearIndex +import com.kizitonwose.calendar.data.getYearIndicesCount +import kotlinx.datetime.DayOfWeek + +/** + * Creates a [YearCalendarState] that is remembered across compositions. + * + * @param startYear the initial value for [YearCalendarState.startYear] + * @param endYear the initial value for [YearCalendarState.endYear] + * @param firstDayOfWeek the initial value for [YearCalendarState.firstDayOfWeek] + * @param firstVisibleYear the initial value for [YearCalendarState.firstVisibleYear] + * @param outDateStyle the initial value for [YearCalendarState.outDateStyle] + */ +@Composable +public fun rememberYearCalendarState( + startYear: Year = Year.now(), + endYear: Year = startYear, + firstVisibleYear: Year = startYear, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + outDateStyle: OutDateStyle = OutDateStyle.EndOfRow, +): YearCalendarState { + return rememberSaveable( + inputs = arrayOf( + startYear, + endYear, + firstVisibleYear, + firstDayOfWeek, + outDateStyle, + ), + saver = YearCalendarState.Saver, + ) { + YearCalendarState( + startYear = startYear, + endYear = endYear, + firstDayOfWeek = firstDayOfWeek, + firstVisibleYear = firstVisibleYear, + outDateStyle = outDateStyle, + visibleItemState = null, + ) + } +} + +/** + * A state object that can be hoisted to control and observe calendar properties. + * + * This should be created via [rememberYearCalendarState]. + * + * @param startYear the first month on the calendar. + * @param endYear the last month on the calendar. + * @param firstDayOfWeek the first day of week on the calendar. + * @param firstVisibleYear the initial value for [YearCalendarState.firstVisibleYear] + * @param outDateStyle the preferred style for out date generation. + */ +@Stable +public class YearCalendarState internal constructor( + startYear: Year, + endYear: Year, + firstDayOfWeek: DayOfWeek, + firstVisibleYear: Year, + outDateStyle: OutDateStyle, + visibleItemState: VisibleItemState?, +) : ScrollableState { + /** Backing state for [startYear] */ + private var _startYear by mutableStateOf(startYear) + + /** The first year on the calendar. */ + public var startYear: Year + get() = _startYear + set(value) { + if (value != startYear) { + _startYear = value + yearDataChanged() + } + } + + /** Backing state for [endYear] */ + private var _endYear by mutableStateOf(endYear) + + /** The last year on the calendar. */ + public var endYear: Year + get() = _endYear + set(value) { + if (value != endYear) { + _endYear = value + yearDataChanged() + } + } + + /** Backing state for [firstDayOfWeek] */ + private var _firstDayOfWeek by mutableStateOf(firstDayOfWeek) + + /** The first day of week on the calendar. */ + public var firstDayOfWeek: DayOfWeek + get() = _firstDayOfWeek + set(value) { + if (value != firstDayOfWeek) { + _firstDayOfWeek = value + yearDataChanged() + } + } + + /** Backing state for [outDateStyle] */ + private var _outDateStyle by mutableStateOf(outDateStyle) + + /** The preferred style for out date generation. */ + public var outDateStyle: OutDateStyle + get() = _outDateStyle + set(value) { + if (value != outDateStyle) { + _outDateStyle = value + yearDataChanged() + } + } + + /** + * The first year that is visible. + * + * @see [lastVisibleYear] + */ + public val firstVisibleYear: CalendarYear by derivedStateOf { + store[listState.firstVisibleItemIndex] + } + + /** + * The last year that is visible. + * + * @see [firstVisibleYear] + */ + public val lastVisibleYear: CalendarYear by derivedStateOf { + store[listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0] + } + + /** + * The object of [CalendarLayoutInfo] calculated during the last layout pass. For example, + * you can use it to calculate what items are currently visible. + * + * Note that this property is observable and is updated after every scroll or remeasure. + * If you use it in the composable function it will be recomposed on every change causing + * potential performance issues including infinity recomposition loop. + * Therefore, avoid using it in the composition. + * + * If you need to use it in the composition then consider wrapping the calculation into a + * derived state in order to only have recompositions when the derived value changes. + * See Example6Page in the sample app for usage. + * + * If you want to run some side effects like sending an analytics event or updating a state + * based on this value consider using "snapshotFlow". + * + * see [LazyListLayoutInfo] + */ + public val layoutInfo: YearCalendarLayoutInfo + get() = YearCalendarLayoutInfo(listState.layoutInfo) { index -> store[index] } + + /** + * [InteractionSource] that will be used to dispatch drag events when this + * calendar is being dragged. If you want to know whether the fling (or animated scroll) is in + * progress, use [isScrollInProgress]. + */ + public val interactionSource: InteractionSource + get() = listState.interactionSource + + internal val listState = LazyListState( + firstVisibleItemIndex = visibleItemState?.firstVisibleItemIndex + ?: getScrollIndex(firstVisibleYear) ?: 0, + firstVisibleItemScrollOffset = visibleItemState?.firstVisibleItemScrollOffset ?: 0, + ) + + internal var calendarInfo by mutableStateOf(CalendarInfo(indexCount = 0)) + + internal val store = DataStore { offset -> + getCalendarYearData( + startYear = this.startYear, + offset = offset, + firstDayOfWeek = this.firstDayOfWeek, + outDateStyle = this.outDateStyle, + ) + } + + init { + yearDataChanged() // Update indexCount initially. + } + + private fun yearDataChanged() { + store.clear() + checkRange(startYear, endYear) + // Read the firstDayOfWeek and outDateStyle properties to ensure recomposition + // even though they are unused in the CalendarInfo. Alternatively, we could use + // mutableStateMapOf() as the backing store for DataStore() to ensure recomposition + // but not sure how compose handles recomposition of a lazy list that reads from + // such map when an entry unrelated to the visible indices changes. + calendarInfo = CalendarInfo( + indexCount = getYearIndicesCount(startYear, endYear), + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ) + } + + /** + * Instantly brings the [year] to the top of the viewport. + * + * @param year the year to which to scroll. Must be within the + * range of [startYear] and [endYear]. + * + * @see [animateScrollToYear] + */ + public suspend fun scrollToYear(year: Year) { + listState.scrollToItem(getScrollIndex(year) ?: return) + } + + /** + * Animate (smooth scroll) to the given [year]. + * + * @param year the year to which to scroll. Must be within the + * range of [startYear] and [endYear]. + */ + public suspend fun animateScrollToYear(year: Year) { + listState.animateScrollToItem(getScrollIndex(year) ?: return) + } + + private fun getScrollIndex(year: Year): Int? { + if (year !in startYear..endYear) { + println("YearCalendarState - Attempting to scroll out of range: $year") + return null + } + return getYearIndex(startYear, year) + } + + /** + * Whether this [ScrollableState] is currently scrolling by gesture, fling or programmatically. + */ + override val isScrollInProgress: Boolean + get() = listState.isScrollInProgress + + override fun dispatchRawDelta(delta: Float): Float = listState.dispatchRawDelta(delta) + + override suspend fun scroll( + scrollPriority: MutatePriority, + block: suspend ScrollScope.() -> Unit, + ): Unit = listState.scroll(scrollPriority, block) + + public companion object { + internal val Saver: Saver = listSaver( + save = { + listOf( + it.startYear.value, + it.endYear.value, + it.firstVisibleYear.year.value, + it.firstDayOfWeek.ordinal, + it.outDateStyle.ordinal, + it.listState.firstVisibleItemIndex, + it.listState.firstVisibleItemScrollOffset, + ) + }, + restore = { + YearCalendarState( + startYear = Year(it[0]), + endYear = Year(it[1]), + firstVisibleYear = Year(it[2]), + firstDayOfWeek = DayOfWeek.entries[it[3]], + outDateStyle = OutDateStyle.entries[it[4]], + visibleItemState = VisibleItemState( + firstVisibleItemIndex = it[5], + firstVisibleItemScrollOffset = it[6], + ), + ) + }, + ) + } +} diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt new file mode 100644 index 00000000..227c72b3 --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode.kt @@ -0,0 +1,37 @@ +package com.kizitonwose.calendar.compose.yearcalendar + +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.ui.Modifier + +/** + * Determines how the height of the month content is calculated. + */ +public enum class YearContentHeightMode { + /** + * The calendar months and days will wrap content height. This allows + * you to use [Modifier.aspectRatio] if you want square day content + * or [Modifier.height] if you want a specific height value + * for the day content. + */ + Wrap, + + /** + * The calendar months will be distributed uniformly to fill the + * parent's height. However, the days within the calendar months will + * wrap content height. This allows you to spread the calendar months + * evenly across the screen while using [Modifier.aspectRatio] if you + * want square day content or [Modifier.height] if you want a specific + * height value for the day content. + */ + Fill, + + /** + * The calendar months and days will uniformly stretch to fill the + * parent's height. This allows you to use [Modifier.fillMaxHeight] for + * the day content height. With this option, your Calendar composable should + * also be created with [Modifier.fillMaxHeight] or [Modifier.height]. + */ + Stretch, +} diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarYear.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarYear.kt new file mode 100644 index 00000000..f8de0747 --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/CalendarYear.kt @@ -0,0 +1,43 @@ +package com.kizitonwose.calendar.core + +import androidx.compose.runtime.Immutable + +/** + * Represents a year on the calendar. + * + * @param year the calendar year value. + * @param months the months in this year. + */ +@Immutable +public data class CalendarYear( + val year: Year, + val months: List, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as CalendarYear + + if (year != other.year) return false + if (months.first() != other.months.first()) return false + if (months.last() != other.months.last()) return false + + return true + } + + override fun hashCode(): Int { + var result = year.hashCode() + result = 31 * result + months.first().hashCode() + result = 31 * result + months.last().hashCode() + return result + } + + override fun toString(): String { + return "CalendarYear { " + + "year = $year, " + + "firstMonth = ${months.first()}, " + + "lastMonth = ${months.last()} " + + "} " + } +} diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt index a54fb9f8..e3ec0844 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt @@ -67,7 +67,7 @@ public object YearComponentSerializer : KSerializer { override fun serialize(encoder: Encoder, value: Year) { encoder.encodeStructure(descriptor) { - encodeIntElement(descriptor, 0, value.year) + encodeIntElement(descriptor, 0, value.value) } } } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt index 6865c1d8..4ca68408 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt @@ -11,12 +11,12 @@ import com.kizitonwose.calendar.core.weeksUntil import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate -public data class WeekDateRange( +internal data class WeekDateRange( val startDateAdjusted: LocalDate, val endDateAdjusted: LocalDate, ) -public fun getWeekCalendarAdjustedRange( +internal fun getWeekCalendarAdjustedRange( startDate: LocalDate, endDate: LocalDate, firstDayOfWeek: DayOfWeek, @@ -28,7 +28,7 @@ public fun getWeekCalendarAdjustedRange( return WeekDateRange(startDateAdjusted = startDateAdjusted, endDateAdjusted = endDateAdjusted) } -public fun getWeekCalendarData( +internal fun getWeekCalendarData( startDateAdjusted: LocalDate, offset: Int, desiredStartDate: LocalDate, @@ -38,7 +38,7 @@ public fun getWeekCalendarData( return WeekData(firstDayInWeek, desiredStartDate, desiredEndDate) } -public data class WeekData internal constructor( +internal data class WeekData internal constructor( private val firstDayInWeek: LocalDate, private val desiredStartDate: LocalDate, private val desiredEndDate: LocalDate, @@ -56,11 +56,11 @@ public data class WeekData internal constructor( } } -public fun getWeekIndex(startDateAdjusted: LocalDate, date: LocalDate): Int { +internal fun getWeekIndex(startDateAdjusted: LocalDate, date: LocalDate): Int { return startDateAdjusted.weeksUntil(date) } -public fun getWeekIndicesCount(startDateAdjusted: LocalDate, endDateAdjusted: LocalDate): Int { +internal fun getWeekIndicesCount(startDateAdjusted: LocalDate, endDateAdjusted: LocalDate): Int { // Add one to include the start week itself! return getWeekIndex(startDateAdjusted, endDateAdjusted) + 1 } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/YearData.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/YearData.kt new file mode 100644 index 00000000..a39d89b4 --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/YearData.kt @@ -0,0 +1,37 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.atMonth +import com.kizitonwose.calendar.core.plusYears +import com.kizitonwose.calendar.core.yearsUntil +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.Month + +internal fun getCalendarYearData( + startYear: Year, + offset: Int, + firstDayOfWeek: DayOfWeek, + outDateStyle: OutDateStyle, +): CalendarYear { + val year = startYear.plusYears(offset) + val months = List(Month.entries.size) { index -> + getCalendarMonthData( + startMonth = year.atMonth(Month.JANUARY), + offset = index, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ).calendarMonth + } + return CalendarYear(year, months) +} + +internal fun getYearIndex(startYear: Year, targetYear: Year): Int { + return startYear.yearsUntil(targetYear) +} + +internal fun getYearIndicesCount(startYear: Year, endYear: Year): Int { + // Add one to include the start year itself! + return getYearIndex(startYear, endYear) + 1 +} diff --git a/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt b/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt index d5a7d763..09312a83 100644 --- a/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt +++ b/compose-multiplatform/library/src/jvmMain/kotlin/com/kizitonwose/calendar/core/Converters.kt @@ -7,6 +7,6 @@ public fun YearMonth.toJavaYearMonth(): jtYearMonth = jtYearMonth.of(year, month public fun jtYearMonth.toKotlinYearMonth(): YearMonth = YearMonth(year, month) -public fun Year.toJavaYear(): jtYear = jtYear.of(year) +public fun Year.toJavaYear(): jtYear = jtYear.of(value) public fun jtYear.toKotlinYear(): Year = Year(value) diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt index e8043213..72810054 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -249,7 +249,7 @@ public class YearCalendarState internal constructor( private fun getScrollIndex(year: Year): Int? { if (year !in startYear..endYear) { - Log.d("CalendarState", "Attempting to scroll out of range: $year") + Log.d("YearCalendarState", "Attempting to scroll out of range: $year") return null } return getYearIndex(startYear, year) From 46d4a3aecf817f580b1795329dd5935a63f581c2 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 17:36:57 +0200 Subject: [PATCH 28/48] Add multiplatform calendar state tests --- .../kizitonwose/calendar/core/Extensions.kt | 9 +- .../calendar/compose/CalendarStateTests.kt | 103 +++++++++++++++++ .../compose/HeatMapCalendarStateTests.kt | 82 +++++++++++++ .../calendar/compose/StateSaverTests.kt | 109 ++++++++++++++++++ .../compose/WeekCalendarStateTests.kt | 77 +++++++++++++ .../compose/YearCalendarStateTests.kt | 108 +++++++++++++++++ .../calendar/core/ExtensionTest.kt | 12 ++ .../kizitonwose/calendar/data/Extensions.kt | 2 +- 8 files changed, 499 insertions(+), 3 deletions(-) create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt index ee33b71c..3159e327 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Extensions.kt @@ -7,7 +7,6 @@ import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import kotlinx.datetime.TimeZone -import kotlinx.datetime.isoDayNumber import kotlinx.datetime.minus import kotlinx.datetime.plus import kotlinx.datetime.todayIn @@ -120,4 +119,10 @@ internal fun LocalDate.minusWeeks(value: Int): LocalDate = minus(value, DateTime internal fun LocalDate.weeksUntil(other: LocalDate): Int = until(other, DateTimeUnit.WEEK) // E.g DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY) = 3 -internal fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (other.isoDayNumber - isoDayNumber)) % 7 +internal fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (other.ordinal - ordinal)) % 7 + +// E.g DayOfWeek.SATURDAY.plusDays(3) = DayOfWeek.TUESDAY +internal fun DayOfWeek.plusDays(days: Int): DayOfWeek { + val amount = (days % 7) + return DayOfWeek.entries[(ordinal + (amount + 7)) % 7] +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt new file mode 100644 index 00000000..a205044d --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt @@ -0,0 +1,103 @@ +package com.kizitonwose.calendar.compose + +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays +import com.kizitonwose.calendar.core.plusMonths +import com.kizitonwose.calendar.data.VisibleItemState +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlin.test.Test +import kotlin.test.assertEquals + +class CalendarStateTests { + @Test + fun startMonthUpdateIsReflectedInTheState() { + val now = YearMonth.now() + val updatedStartMonth = now.minusMonths(4) + val state = createState( + startMonth = now, + endMonth = now, + ) + + assertEquals(state.store[0].yearMonth, now) + + state.startMonth = updatedStartMonth + + assertEquals(state.store[0].yearMonth, updatedStartMonth) + } + + @Test + fun endMonthUpdateIsReflectedInTheState() { + val now = YearMonth.now() + val updatedEndMonth = now.plusMonths(4) + val state = createState( + startMonth = now, + endMonth = now, + ) + + assertEquals(state.store[0].yearMonth, now) + + state.endMonth = updatedEndMonth + + assertEquals(state.store[4].yearMonth, updatedEndMonth) + } + + @Test + fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + val firstDayOfWeek = LocalDate.now().dayOfWeek + + val state = createState(firstDayOfWeek = firstDayOfWeek) + + state.store[0].weekDays.forEach { week -> + assertEquals(week.first().date.dayOfWeek, firstDayOfWeek) + } + + do { + val updatedFirstDayOfWeek = state.firstDayOfWeek.plusDays(1) + state.firstDayOfWeek = updatedFirstDayOfWeek + + state.store[0].weekDays.forEach { week -> + assertEquals(week.first().date.dayOfWeek, updatedFirstDayOfWeek) + } + } while (firstDayOfWeek != state.firstDayOfWeek) + } + + @Test + fun outDateStyleUpdateIsReflectedInTheState() { + val outDateStyle = OutDateStyle.EndOfRow + // Nov 2022 has 5 weeks when Sun is the first day. + val startMonth = YearMonth(2022, 11) + val state = createState( + startMonth = startMonth, + endMonth = startMonth, + outDateStyle = outDateStyle, + firstDayOfWeek = DayOfWeek.SUNDAY, + ) + + assertEquals(state.store[0].weekDays.count(), 5) + + state.outDateStyle = OutDateStyle.EndOfGrid + + assertEquals(state.store[0].weekDays.count(), 6) + } + + private fun createState( + startMonth: YearMonth = YearMonth.now(), + endMonth: YearMonth = startMonth, + firstVisibleMonth: YearMonth = startMonth, + outDateStyle: OutDateStyle = OutDateStyle.EndOfRow, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + visibleItemState: VisibleItemState = VisibleItemState(), + ) = CalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = firstVisibleMonth, + outDateStyle = outDateStyle, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = visibleItemState, + ) +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt new file mode 100644 index 00000000..77a3bcce --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt @@ -0,0 +1,82 @@ +package com.kizitonwose.calendar.compose + +import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays +import com.kizitonwose.calendar.core.plusMonths +import com.kizitonwose.calendar.data.VisibleItemState +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlin.test.Test +import kotlin.test.assertEquals + +class HeatMapCalendarStateTests { + @Test + fun startMonthUpdateIsReflectedInTheState() { + val now = YearMonth.now() + val updatedStartMonth = now.minusMonths(4) + val state = createState( + startMonth = now, + endMonth = now, + ) + + assertEquals(state.store[0].yearMonth, now) + + state.startMonth = updatedStartMonth + + assertEquals(state.store[0].yearMonth, updatedStartMonth) + } + + @Test + fun endMonthUpdateIsReflectedInTheState() { + val now = YearMonth.now() + val updatedEndMonth = now.plusMonths(4) + val state = createState( + startMonth = now, + endMonth = now, + ) + + assertEquals(state.store[0].yearMonth, now) + + state.endMonth = updatedEndMonth + + assertEquals(state.store[4].yearMonth, updatedEndMonth) + } + + @Test + fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + val firstDayOfWeek = LocalDate.now().dayOfWeek + + val state = createState(firstDayOfWeek = firstDayOfWeek) + + state.store[0].weekDays.forEach { week -> + assertEquals(week.first().date.dayOfWeek, firstDayOfWeek) + } + + do { + val updatedFirstDayOfWeek = state.firstDayOfWeek.plusDays(1) + state.firstDayOfWeek = updatedFirstDayOfWeek + + state.store[0].weekDays.forEach { week -> + assertEquals(week.first().date.dayOfWeek, updatedFirstDayOfWeek) + } + } while (firstDayOfWeek != state.firstDayOfWeek) + } + + private fun createState( + startMonth: YearMonth = YearMonth.now(), + endMonth: YearMonth = startMonth, + firstVisibleMonth: YearMonth = startMonth, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + visibleItemState: VisibleItemState = VisibleItemState(), + ) = HeatMapCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = firstVisibleMonth, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = visibleItemState, + ) +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt new file mode 100644 index 00000000..b6d6c47c --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt @@ -0,0 +1,109 @@ +package com.kizitonwose.calendar.compose + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import androidx.compose.runtime.saveable.listSaver +import com.kizitonwose.calendar.compose.heatmapcalendar.HeatMapCalendarState +import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.data.VisibleItemState +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * The states use the [listSaver] type so these tests should catch when we move + * things around without paying attention to the indices or the actual items + * being saved. Such issues are typically not caught during development since + * state restoration (e.g via rotation) will likely not happen often. + */ +class StateSaverTests { + @Test + fun monthCalendarStateCanBeRestored() { + val now = YearMonth.now() + val firstDayOfWeek = DayOfWeek.entries.random() + val outDateStyle = OutDateStyle.entries.random() + val state = CalendarState( + startMonth = now, + endMonth = now, + firstVisibleMonth = now, + outDateStyle = outDateStyle, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = VisibleItemState(), + ) + val restored = restore(state, CalendarState.Saver) + assertEquals(state.startMonth, restored.startMonth) + assertEquals(state.endMonth, restored.endMonth) + assertEquals(state.firstVisibleMonth, restored.firstVisibleMonth) + assertEquals(state.outDateStyle, restored.outDateStyle) + assertEquals(state.firstDayOfWeek, restored.firstDayOfWeek) + } + + @Test + fun weekCalendarStateCanBeRestored() { + val now = LocalDate.now() + val firstDayOfWeek = DayOfWeek.entries.random() + val state = WeekCalendarState( + startDate = now, + endDate = now, + firstVisibleWeekDate = now, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = VisibleItemState(), + ) + val restored = restore(state, WeekCalendarState.Saver) + assertEquals(state.startDate, restored.startDate) + assertEquals(state.endDate, restored.endDate) + assertEquals(state.firstVisibleWeek, restored.firstVisibleWeek) + assertEquals(state.firstDayOfWeek, restored.firstDayOfWeek) + } + + @Test + fun heatmapCalendarStateCanBeRestored() { + val now = YearMonth.now() + val firstDayOfWeek = DayOfWeek.entries.random() + val state = HeatMapCalendarState( + startMonth = now, + endMonth = now, + firstVisibleMonth = now, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = VisibleItemState(), + ) + val restored = restore(state, HeatMapCalendarState.Saver) + assertEquals(state.startMonth, restored.startMonth) + assertEquals(state.endMonth, restored.endMonth) + assertEquals(state.firstVisibleMonth, restored.firstVisibleMonth) + assertEquals(state.firstDayOfWeek, restored.firstDayOfWeek) + } + + @Test + fun yearCalendarStateCanBeRestored() { + val now = Year.now() + val firstDayOfWeek = DayOfWeek.entries.random() + val outDateStyle = OutDateStyle.entries.random() + val state = YearCalendarState( + startYear = now, + endYear = now, + firstVisibleYear = now, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + visibleItemState = VisibleItemState(), + ) + val restored = restore(state, YearCalendarState.Saver) + assertEquals(state.startYear, restored.startYear) + assertEquals(state.endYear, restored.endYear) + assertEquals(state.firstVisibleYear, restored.firstVisibleYear) + assertEquals(state.firstDayOfWeek, restored.firstDayOfWeek) + } + + private fun restore(value: State, saver: Saver): State { + with(saver) { + val saved = SaverScope { true }.save(value)!! + return restore(saved)!! + } + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt new file mode 100644 index 00000000..1df3074d --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt @@ -0,0 +1,77 @@ +package com.kizitonwose.calendar.compose + +import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.minusDays +import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays +import com.kizitonwose.calendar.data.VisibleItemState +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class WeekCalendarStateTests { + @Test + fun startDateUpdateIsReflectedInTheState() { + val now = LocalDate.now() + val updatedStartDate = now.minusDays(7) + val state = createState( + startDate = now, + endDate = now, + ) + + assertTrue(state.store[0].days.map { it.date }.contains(now)) + + state.startDate = updatedStartDate + + assertTrue(state.store[0].days.map { it.date }.contains(updatedStartDate)) + } + + @Test + fun endDateUpdateIsReflectedInTheState() { + val now = LocalDate.now() + val updatedEndDate = now.plusDays(7) + val state = createState( + startDate = now, + endDate = now, + ) + + assertTrue(state.store[0].days.map { it.date }.contains(now)) + + state.endDate = updatedEndDate + + assertTrue(state.store[1].days.map { it.date }.contains(updatedEndDate)) + } + + @Test + fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + val firstDayOfWeek = LocalDate.now().dayOfWeek + + val state = createState(firstDayOfWeek = firstDayOfWeek) + + assertEquals(state.store[0].days.first().date.dayOfWeek, firstDayOfWeek) + + do { + val updatedFirstDayOfWeek = state.firstDayOfWeek.plusDays(1) + state.firstDayOfWeek = updatedFirstDayOfWeek + + assertEquals(state.store[0].days.first().date.dayOfWeek, updatedFirstDayOfWeek) + } while (firstDayOfWeek != state.firstDayOfWeek) + } + + private fun createState( + startDate: LocalDate = LocalDate.now(), + endDate: LocalDate = startDate, + firstVisibleWeekDate: LocalDate = startDate, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + visibleItemState: VisibleItemState = VisibleItemState(), + ) = WeekCalendarState( + startDate = startDate, + endDate = endDate, + firstVisibleWeekDate = firstVisibleWeekDate, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = visibleItemState, + ) +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt new file mode 100644 index 00000000..24333e3f --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt @@ -0,0 +1,108 @@ +package com.kizitonwose.calendar.compose + +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale +import com.kizitonwose.calendar.core.minusYears +import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusDays +import com.kizitonwose.calendar.core.plusYears +import com.kizitonwose.calendar.data.VisibleItemState +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlin.test.Test +import kotlin.test.assertEquals + +class YearCalendarStateTests { + @Test + fun startYearUpdateIsReflectedInTheState() { + val now = Year.now() + val updatedStartYear = now.minusYears(8) + val state = createState( + startYear = now, + endYear = now, + ) + + assertEquals(state.store[0].year, now) + + state.startYear = updatedStartYear + + assertEquals(state.store[0].year, updatedStartYear) + } + + @Test + fun endYearUpdateIsReflectedInTheState() { + val now = Year.now() + val updatedEndMonth = now.plusYears(8) + val state = createState( + startYear = now, + endYear = now, + ) + + assertEquals(state.store[0].year, now) + + state.endYear = updatedEndMonth + + assertEquals(state.store[8].year, updatedEndMonth) + } + + @Test + fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + val firstDayOfWeek = LocalDate.now().dayOfWeek + + val state = createState(firstDayOfWeek = firstDayOfWeek) + + state.store[0].months + .flatMap { month -> month.weekDays } + .forEach { week -> + assertEquals(week.first().date.dayOfWeek, firstDayOfWeek) + } + do { + val updatedFirstDayOfWeek = state.firstDayOfWeek.plusDays(1) + state.firstDayOfWeek = updatedFirstDayOfWeek + + state.store[0].months + .flatMap { month -> month.weekDays } + .forEach { week -> + assertEquals(week.first().date.dayOfWeek, updatedFirstDayOfWeek) + } + } while (firstDayOfWeek != state.firstDayOfWeek) + } + + @Test + fun outDateStyleUpdateIsReflectedInTheState() { + val outDateStyle = OutDateStyle.EndOfRow + // Nov 2022 has 5 weeks when Sun is the first day. + val startYear = Year(2022) + val state = createState( + startYear = startYear, + endYear = startYear, + outDateStyle = outDateStyle, + firstDayOfWeek = DayOfWeek.SUNDAY, + ) + + assertEquals(state.store[0].months[Month.NOVEMBER.ordinal].weekDays.count(), 5) + + state.outDateStyle = OutDateStyle.EndOfGrid + + assertEquals(state.store[0].months[Month.NOVEMBER.ordinal].weekDays.count(), 6) + } + + private fun createState( + startYear: Year = Year.now(), + endYear: Year = startYear, + firstVisibleYear: Year = startYear, + outDateStyle: OutDateStyle = OutDateStyle.EndOfRow, + firstDayOfWeek: DayOfWeek = firstDayOfWeekFromLocale(), + visibleItemState: VisibleItemState = VisibleItemState(), + ) = YearCalendarState( + startYear = startYear, + endYear = endYear, + firstVisibleYear = firstVisibleYear, + outDateStyle = outDateStyle, + firstDayOfWeek = firstDayOfWeek, + visibleItemState = visibleItemState, + ) +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt index 5acbd617..d400b092 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/ExtensionTest.kt @@ -79,6 +79,18 @@ class ExtensionTest { assertEquals(6, DayOfWeek.SUNDAY.daysUntil(DayOfWeek.SATURDAY)) } + @Test + fun plusDays() { + assertEquals(DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY.plusDays(5)) + assertEquals(DayOfWeek.THURSDAY, DayOfWeek.TUESDAY.plusDays(2)) + assertEquals(DayOfWeek.SUNDAY, DayOfWeek.SUNDAY.plusDays(0)) + assertEquals(DayOfWeek.TUESDAY, DayOfWeek.SATURDAY.plusDays(3)) + assertEquals(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY.plusDays(5)) + assertEquals(DayOfWeek.FRIDAY, DayOfWeek.THURSDAY.plusDays(1)) + assertEquals(DayOfWeek.SUNDAY, DayOfWeek.MONDAY.plusDays(6)) + assertEquals(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY.plusDays(6)) + } + @Test fun daysOfWeek() { DayOfWeek.entries.forEach { dayOfWeek -> diff --git a/data/src/main/java/com/kizitonwose/calendar/data/Extensions.kt b/data/src/main/java/com/kizitonwose/calendar/data/Extensions.kt index eb53661f..90aa6038 100644 --- a/data/src/main/java/com/kizitonwose/calendar/data/Extensions.kt +++ b/data/src/main/java/com/kizitonwose/calendar/data/Extensions.kt @@ -3,4 +3,4 @@ package com.kizitonwose.calendar.data import java.time.DayOfWeek // E.g DayOfWeek.SATURDAY.daysUntil(DayOfWeek.TUESDAY) = 3 -public fun DayOfWeek.daysUntil(other: DayOfWeek): Int = (7 + (other.value - value)) % 7 +public fun DayOfWeek.daysUntil(other: DayOfWeek): Int = (7 + (other.ordinal - ordinal)) % 7 From 2c6347dce3a270d7f6b27dc554a6df2a3305708b Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Wed, 24 Jul 2024 18:23:48 +0200 Subject: [PATCH 29/48] Add multiplatform calendar data tests --- .../calendar/compose/CalendarStateTests.kt | 13 +- .../compose/HeatMapCalendarStateTests.kt | 10 +- .../calendar/compose/StateSaverTests.kt | 13 +- .../compose/WeekCalendarStateTests.kt | 10 +- .../compose/YearCalendarStateTests.kt | 13 +- .../com/kizitonwose/calendar/core/Utils.kt | 3 - .../calendar/core/YearMonthTest.kt | 1 + .../com/kizitonwose/calendar/core/YearTest.kt | 1 + .../calendar/data/HeatMapDataTests.kt | 154 +++++++++++++++ .../calendar/data/MonthDataTests.kt | 175 ++++++++++++++++++ .../calendar/data/WeekDataTests.kt | 138 ++++++++++++++ .../calendar/data/YearDataTests.kt | 101 ++++++++++ .../com/kizitonwose/calendar/utils/Utils.kt | 23 +++ .../calendar/data/HeatMapDataTests.kt | 10 +- .../calendar/data/MonthDataTests.kt | 7 +- .../calendar/data/WeekDataTests.kt | 16 +- .../calendar/data/YearDataTests.kt | 9 +- 17 files changed, 652 insertions(+), 45 deletions(-) delete mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt create mode 100644 compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/utils/Utils.kt diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt index a205044d..e842530c 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt @@ -10,12 +10,14 @@ import com.kizitonwose.calendar.core.plusMonths import com.kizitonwose.calendar.data.VisibleItemState import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate +import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals class CalendarStateTests { @Test - fun startMonthUpdateIsReflectedInTheState() { + @JsName("test1") + fun `start month update is reflected in the state`() { val now = YearMonth.now() val updatedStartMonth = now.minusMonths(4) val state = createState( @@ -31,7 +33,8 @@ class CalendarStateTests { } @Test - fun endMonthUpdateIsReflectedInTheState() { + @JsName("test2") + fun `end month update is reflected in the state`() { val now = YearMonth.now() val updatedEndMonth = now.plusMonths(4) val state = createState( @@ -47,7 +50,8 @@ class CalendarStateTests { } @Test - fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + @JsName("test3") + fun `first day of the week update is reflected in the state`() { val firstDayOfWeek = LocalDate.now().dayOfWeek val state = createState(firstDayOfWeek = firstDayOfWeek) @@ -67,7 +71,8 @@ class CalendarStateTests { } @Test - fun outDateStyleUpdateIsReflectedInTheState() { + @JsName("test4") + fun `out date style update is reflected in the state`() { val outDateStyle = OutDateStyle.EndOfRow // Nov 2022 has 5 weeks when Sun is the first day. val startMonth = YearMonth(2022, 11) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt index 77a3bcce..aa9054b6 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt @@ -10,12 +10,14 @@ import com.kizitonwose.calendar.core.plusMonths import com.kizitonwose.calendar.data.VisibleItemState import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate +import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals class HeatMapCalendarStateTests { @Test - fun startMonthUpdateIsReflectedInTheState() { + @JsName("test1") + fun `start month update is reflected in the state`() { val now = YearMonth.now() val updatedStartMonth = now.minusMonths(4) val state = createState( @@ -31,7 +33,8 @@ class HeatMapCalendarStateTests { } @Test - fun endMonthUpdateIsReflectedInTheState() { + @JsName("test2") + fun `end month update is reflected in the state`() { val now = YearMonth.now() val updatedEndMonth = now.plusMonths(4) val state = createState( @@ -47,7 +50,8 @@ class HeatMapCalendarStateTests { } @Test - fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + @JsName("test3") + fun `first day of the week update is reflected in the state`() { val firstDayOfWeek = LocalDate.now().dayOfWeek val state = createState(firstDayOfWeek = firstDayOfWeek) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt index b6d6c47c..e7f5f6bb 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt @@ -13,6 +13,7 @@ import com.kizitonwose.calendar.core.now import com.kizitonwose.calendar.data.VisibleItemState import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate +import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals @@ -24,7 +25,8 @@ import kotlin.test.assertEquals */ class StateSaverTests { @Test - fun monthCalendarStateCanBeRestored() { + @JsName("test1") + fun `month calendar state can be restored`() { val now = YearMonth.now() val firstDayOfWeek = DayOfWeek.entries.random() val outDateStyle = OutDateStyle.entries.random() @@ -45,7 +47,8 @@ class StateSaverTests { } @Test - fun weekCalendarStateCanBeRestored() { + @JsName("test2") + fun `week calendar state can be restored`() { val now = LocalDate.now() val firstDayOfWeek = DayOfWeek.entries.random() val state = WeekCalendarState( @@ -63,7 +66,8 @@ class StateSaverTests { } @Test - fun heatmapCalendarStateCanBeRestored() { + @JsName("test3") + fun `heatmap calendar state can be restored`() { val now = YearMonth.now() val firstDayOfWeek = DayOfWeek.entries.random() val state = HeatMapCalendarState( @@ -81,7 +85,8 @@ class StateSaverTests { } @Test - fun yearCalendarStateCanBeRestored() { + @JsName("test4") + fun `year calendar state can be restored`() { val now = Year.now() val firstDayOfWeek = DayOfWeek.entries.random() val outDateStyle = OutDateStyle.entries.random() diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt index 1df3074d..6c213280 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt @@ -8,13 +8,15 @@ import com.kizitonwose.calendar.core.plusDays import com.kizitonwose.calendar.data.VisibleItemState import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate +import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue class WeekCalendarStateTests { @Test - fun startDateUpdateIsReflectedInTheState() { + @JsName("test1") + fun `start date update is reflected in the state`() { val now = LocalDate.now() val updatedStartDate = now.minusDays(7) val state = createState( @@ -30,7 +32,8 @@ class WeekCalendarStateTests { } @Test - fun endDateUpdateIsReflectedInTheState() { + @JsName("test2") + fun `end date update is reflected in the state`() { val now = LocalDate.now() val updatedEndDate = now.plusDays(7) val state = createState( @@ -46,7 +49,8 @@ class WeekCalendarStateTests { } @Test - fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + @JsName("test3") + fun `first day of the week update is reflected in the state`() { val firstDayOfWeek = LocalDate.now().dayOfWeek val state = createState(firstDayOfWeek = firstDayOfWeek) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt index 24333e3f..dbc1b7e7 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt @@ -12,12 +12,14 @@ import com.kizitonwose.calendar.data.VisibleItemState import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import kotlinx.datetime.Month +import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals class YearCalendarStateTests { @Test - fun startYearUpdateIsReflectedInTheState() { + @JsName("test1") + fun `start year update is reflected in the state`() { val now = Year.now() val updatedStartYear = now.minusYears(8) val state = createState( @@ -33,7 +35,8 @@ class YearCalendarStateTests { } @Test - fun endYearUpdateIsReflectedInTheState() { + @JsName("test2") + fun `end year update is reflected in the state`() { val now = Year.now() val updatedEndMonth = now.plusYears(8) val state = createState( @@ -49,7 +52,8 @@ class YearCalendarStateTests { } @Test - fun firstDayOfTheWeekUpdateIsReflectedInTheState() { + @JsName("test3") + fun `first day of the week update is reflected in the state`() { val firstDayOfWeek = LocalDate.now().dayOfWeek val state = createState(firstDayOfWeek = firstDayOfWeek) @@ -72,7 +76,8 @@ class YearCalendarStateTests { } @Test - fun outDateStyleUpdateIsReflectedInTheState() { + @JsName("test4") + fun `out date style update is reflected in the state`() { val outDateStyle = OutDateStyle.EndOfRow // Nov 2022 has 5 weeks when Sun is the first day. val startYear = Year(2022) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt deleted file mode 100644 index a0810c90..00000000 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/Utils.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.kizitonwose.calendar.core - -internal infix fun Pair.toTriple(that: C): Triple = Triple(first, second, that) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt index a456a3e4..462c4013 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearMonthTest.kt @@ -1,5 +1,6 @@ package com.kizitonwose.calendar.core +import com.kizitonwose.calendar.utils.toTriple import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.LocalDate import kotlinx.datetime.Month diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt index 59a43a43..25b052e3 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/core/YearTest.kt @@ -1,5 +1,6 @@ package com.kizitonwose.calendar.core +import com.kizitonwose.calendar.utils.toTriple import kotlinx.datetime.LocalDate import kotlinx.datetime.Month import kotlinx.datetime.number diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt new file mode 100644 index 00000000..3cd97917 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt @@ -0,0 +1,154 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.yearMonth +import com.kizitonwose.calendar.utils.nextMonth +import com.kizitonwose.calendar.utils.previousMonth +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.Month +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class HeatMapDataTests { + private val october2022 = YearMonth(2022, Month.OCTOBER) + private val november2022 = YearMonth(2022, Month.NOVEMBER) + private val december2022 = YearMonth(2022, Month.DECEMBER) + private val firstDayOfWeek = DayOfWeek.MONDAY + + /** October, November and December 2022 + * with October as the start month and + * Monday as the first day of week. + * ┌──┬─────────────────┬───────────┬───────────┐ + * │ │Oct 2022 │Nov 2022 │Dec 2022 │ + * ├──┼──┬──┬──┬──┬──┬──┼──┬──┬──┬──┼──┬──┬──┬──┤ + * │Mo│26│03│10│17│24│31│07│14│21│28│05│12│19│26│ + * ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + * │Tu│27│04│11│18│25│01│08│15│22│29│06│13│20│27│ + * ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + * │We│28│05│12│19│26│02│09│16│23│30│07│14│21│28│ + * ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + * │Th│29│06│13│20│27│03│10│17│24│01│08│15│22│29│ + * ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + * │Fr│30│07│14│21│28│04│11│18│24│02│09│16│23│30│ + * ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + * │Sa│01│08│15│22│29│05│12│19│26│03│10│17│24│31│ + * ├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤ + * │Su│02│09│16│23│30│06│13│20│27│04│11│18│25│01│ + * └──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘ + **/ + + @Test + @JsName("test1") + fun `number of day positions are accurate`() { + val monthData = getHeatMapCalendarMonthData(october2022, 0, firstDayOfWeek) + val days = monthData.calendarMonth.weekDays.flatten() + + assertEquals(5, days.count { it.position == DayPosition.InDate }) + assertEquals(6, days.count { it.position == DayPosition.OutDate }) + assertEquals(31, days.count { it.position == DayPosition.MonthDate }) + assertEquals(42, days.count()) + assertEquals(6, monthData.calendarMonth.weekDays.count()) + monthData.calendarMonth.weekDays.forEach { weekDays -> + assertEquals(7, weekDays.count()) + } + } + + @Test + @JsName("test2") + fun `first date in the following month is accurate`() { + val novemberMonthData = getHeatMapCalendarMonthData(october2022, 1, firstDayOfWeek) + val days = novemberMonthData.calendarMonth.weekDays.flatten() + + assertEquals(7, days.first().date.dayOfMonth) + assertEquals(october2022.nextMonth, days.first().date.yearMonth) + assertEquals(DayPosition.MonthDate, days.first().position) + } + + @Test + @JsName("test3") + fun `dates in the following month are in the correct positions`() { + val novemberMonthData = getHeatMapCalendarMonthData(october2022, 1, firstDayOfWeek) + val days = novemberMonthData.calendarMonth.weekDays.flatten() + + val monthDates = days.take(24) + val outDates = days.takeLast(4) + + assertTrue(outDates.all { it.position == DayPosition.OutDate }) + assertTrue(monthDates.all { it.position == DayPosition.MonthDate }) + assertEquals(28, days.count()) + } + + @Test + @JsName("test4") + fun `dates in the following month have the correct month values`() { + val november2022 = october2022.nextMonth + val december2022 = november2022.nextMonth + val novemberMonthData = getHeatMapCalendarMonthData(october2022, 1, firstDayOfWeek) + val days = novemberMonthData.calendarMonth.weekDays.flatten() + + val monthDates = days.take(24) + val outDates = days.takeLast(4) + + assertTrue(outDates.all { it.date.yearMonth == december2022 }) + assertTrue(monthDates.all { it.date.yearMonth == november2022 }) + } + + @Test + @JsName("test5") + fun `dates in the first month are in the correct positions`() { + val monthData = getHeatMapCalendarMonthData(october2022, 0, firstDayOfWeek) + val days = monthData.calendarMonth.weekDays.flatten() + + val inDates = days.take(5) + val outDates = days.takeLast(6) + val monthDates = days.drop(5).dropLast(6) + + assertTrue(inDates.all { it.position == DayPosition.InDate }) + assertTrue(outDates.all { it.position == DayPosition.OutDate }) + assertTrue(monthDates.all { it.position == DayPosition.MonthDate }) + } + + @Test + @JsName("test6") + fun `dates in the first month have the correct month values`() { + val previousMonth = october2022.previousMonth + val nextMonth = october2022.nextMonth + val monthData = getHeatMapCalendarMonthData(october2022, 0, firstDayOfWeek) + val days = monthData.calendarMonth.weekDays.flatten() + + val inDates = days.take(5) + val outDates = days.takeLast(6) + val monthDates = days.drop(5).dropLast(6) + + assertTrue(inDates.all { it.date.yearMonth == previousMonth }) + assertTrue(outDates.all { it.date.yearMonth == nextMonth }) + assertTrue(monthDates.all { it.date.yearMonth == october2022 }) + } + + @Test + @JsName("test7") + fun `days are in the appropriate week columns`() { + val monthData = getHeatMapCalendarMonthData(october2022, 0, firstDayOfWeek) + val daysOfWeek = daysOfWeek(firstDayOfWeek) + + monthData.calendarMonth.weekDays.forEach { week -> + week.forEachIndexed { index, day -> + assertEquals(daysOfWeek[index], day.date.dayOfWeek) + } + } + } + + @Test + @JsName("test8") + fun `generated month is at the correct offset`() { + val novemberMonthData = getHeatMapCalendarMonthData(october2022, 1, firstDayOfWeek) + val decemberMonthData = getHeatMapCalendarMonthData(october2022, 2, firstDayOfWeek) + + assertEquals(november2022, novemberMonthData.calendarMonth.yearMonth) + assertEquals(december2022, decemberMonthData.calendarMonth.yearMonth) + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt new file mode 100644 index 00000000..0e6bad95 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt @@ -0,0 +1,175 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.yearMonth +import com.kizitonwose.calendar.utils.nextMonth +import com.kizitonwose.calendar.utils.previousMonth +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.Month +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MonthDataTests { + private val may2019 = YearMonth(2019, Month.MAY) + private val november2019 = YearMonth(2019, Month.NOVEMBER) + private val firstDayOfWeek = DayOfWeek.MONDAY + + /** May and November 2019 with Monday as the first day of week. + * ┌────────────────────┐ ┌────────────────────┐ + * │ May 2019 │ │ November 2019 │ + * ├──┬──┬──┬──┬──┬──┬──┤ ├──┬──┬──┬──┬──┬──┬──┤ + * │Mo│Tu│We│Th│Fr│Sa│Su│ │Mo│Tu│We│Th│Fr│Sa│Su│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │29│30│01│02│03│04│05│ │28│29│30│31│01│02│03│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │06│07│08│09│10│11│12│ │04│05│06│07│08│09│10│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │13│14│15│16│17│18│19│ │11│12│13│14│15│16│17│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │20│21│22│23│24│25│26│ │18│19│20│21│22│23│24│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │27│28│29│30│31│01│02│ │25│26│27│28│29│30│01│ + * └──┴──┴──┴──┴──┴──┴──┘ └──┴──┴──┴──┴──┴──┴──┘ + **/ + + @Test + @JsName("test1") + fun `number of day positions are accurate with EndOfRow OutDateStyle`() { + val monthData = getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfRow) + val days = monthData.calendarMonth.weekDays.flatten() + assertEquals(2, days.count { it.position == DayPosition.InDate }) + assertEquals(2, days.count { it.position == DayPosition.OutDate }) + assertEquals(31, days.count { it.position == DayPosition.MonthDate }) + assertEquals(35, days.count()) + assertEquals(5, monthData.calendarMonth.weekDays.count()) + monthData.calendarMonth.weekDays.forEach { weekDays -> + assertEquals(7, weekDays.count()) + } + } + + @Test + @JsName("test2") + fun `number of day positions are accurate with EndOfGrid OutDateStyle`() { + val monthData = getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfGrid) + val days = monthData.calendarMonth.weekDays.flatten() + assertEquals(2, days.count { it.position == DayPosition.InDate }) + assertEquals(9, days.count { it.position == DayPosition.OutDate }) + assertEquals(31, days.count { it.position == DayPosition.MonthDate }) + assertEquals(42, days.count()) + assertEquals(6, monthData.calendarMonth.weekDays.count()) + monthData.calendarMonth.weekDays.forEach { weekDays -> + assertEquals(7, weekDays.count()) + } + } + + @Test + @JsName("test3") + fun `dates are in the correct positions with EndOfRow OutDateStyle`() { + val monthData = getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfRow) + val days = monthData.calendarMonth.weekDays.flatten() + + val inDates = days.take(2) + val outDates = days.takeLast(2) + val monthDates = days.drop(2).dropLast(2) + + assertTrue(inDates.all { it.position == DayPosition.InDate }) + assertTrue(outDates.all { it.position == DayPosition.OutDate }) + assertTrue(monthDates.all { it.position == DayPosition.MonthDate }) + } + + @Test + @JsName("test14") + fun `dates are in the correct positions with EndOfGrid OutDateStyle`() { + val monthData = getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfGrid) + val days = monthData.calendarMonth.weekDays.flatten() + + val inDates = days.take(2) + val outDates = days.takeLast(2) + val monthDates = days.drop(2).dropLast(9) + + assertTrue(inDates.all { it.position == DayPosition.InDate }) + assertTrue(outDates.all { it.position == DayPosition.OutDate }) + assertTrue(monthDates.all { it.position == DayPosition.MonthDate }) + } + + @Test + @JsName("test5") + fun `dates have the correct month values`() { + val previousMonth = may2019.previousMonth + val nextMonth = may2019.nextMonth + val monthData = getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfRow) + val days = monthData.calendarMonth.weekDays.flatten() + + val inDates = days.take(2) + val outDates = days.takeLast(2) + val monthDates = days.drop(2).dropLast(2) + + assertTrue(inDates.all { it.date.yearMonth == previousMonth }) + assertTrue(outDates.all { it.date.yearMonth == nextMonth }) + assertTrue(monthDates.all { it.date.yearMonth == may2019 }) + } + + @Test + @JsName("test6") + fun `end of row out date style does not add a new row`() { + val endOfRowMonthData = + getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfRow) + + assertEquals(5, endOfRowMonthData.calendarMonth.weekDays.count()) + } + + @Test + @JsName("test7") + fun `end of grid out date style adds a new row`() { + val endOfGridMonthData = + getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfGrid) + + assertEquals(endOfGridMonthData.calendarMonth.weekDays.count(), 6) + endOfGridMonthData.calendarMonth.weekDays.last().forEach { day -> + assertEquals(DayPosition.OutDate, day.position) + assertEquals(may2019.nextMonth, day.date.yearMonth) + } + } + + @Test + @JsName("test8") + fun `days are in the appropriate week columns`() { + val monthData = getCalendarMonthData(may2019, 0, firstDayOfWeek, OutDateStyle.EndOfRow) + val daysOfWeek = daysOfWeek(firstDayOfWeek) + + monthData.calendarMonth.weekDays.forEach { week -> + week.forEachIndexed { index, day -> + assertEquals(daysOfWeek[index], day.date.dayOfWeek) + } + } + } + + @Test + @JsName("test9") + fun `generated month is at the correct offset`() { + val monthData = getCalendarMonthData(may2019, 6, firstDayOfWeek, OutDateStyle.EndOfRow) + + assertEquals(november2019, monthData.calendarMonth.yearMonth) + } + + @Test + @JsName("test10") + fun `month index calculation works as expected`() { + val index = getMonthIndex(startMonth = may2019, targetMonth = november2019) + + assertEquals(6, index) + } + + @Test + @JsName("test11") + fun `month indices count calculation works as expected`() { + val count = getMonthIndicesCount(startMonth = may2019, endMonth = november2019) + + assertEquals(7, count) + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt new file mode 100644 index 00000000..04027109 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt @@ -0,0 +1,138 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.WeekDayPosition +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.atDay +import com.kizitonwose.calendar.core.daysOfWeek +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class WeekDataTests { + private val may2019 = YearMonth(2019, Month.MAY) + private val november2019 = YearMonth(2019, Month.NOVEMBER) + private val firstDayOfWeek = DayOfWeek.MONDAY + + /** May and November 2019 with Monday as the first day of week. + * ┌────────────────────┐ ┌────────────────────┐ + * │ May 2019 │ │ November 2019 │ + * ├──┬──┬──┬──┬──┬──┬──┤ ├──┬──┬──┬──┬──┬──┬──┤ + * │Mo│Tu│We│Th│Fr│Sa│Su│ │Mo│Tu│We│Th│Fr│Sa│Su│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │29│30│01│02│03│04│05│ │28│29│30│31│01│02│03│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │06│07│08│09│10│11│12│ │04│05│06│07│08│09│10│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │13│14│15│16│17│18│19│ │11│12│13│14│15│16│17│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │20│21│22│23│24│25│26│ │18│19│20│21│22│23│24│ + * ├──┼──┼──┼──┼──┼──┼──┤ ├──┼──┼──┼──┼──┼──┼──┤ + * │27│28│29│30│31│01│02│ │25│26│27│28│29│30│01│ + * └──┴──┴──┴──┴──┴──┴──┘ └──┴──┴──┴──┴──┴──┴──┘ + **/ + + @Test + @JsName("test1") + fun `date range adjustment works as expected`() { + val may01 = may2019.atDay(1) + val nov01 = november2019.atDay(1) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, nov01, firstDayOfWeek) + + assertEquals(LocalDate(2019, Month.APRIL, 29), adjustedWeekRange.startDateAdjusted) + assertEquals(LocalDate(2019, Month.NOVEMBER, 3), adjustedWeekRange.endDateAdjusted) + } + + @Test + @JsName("test2") + fun `week data generation works as expected`() { + val may01 = may2019.atDay(1) + val nov01 = november2019.atDay(1) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, nov01, firstDayOfWeek) + val week = getWeekCalendarData(adjustedWeekRange.startDateAdjusted, 0, may01, nov01).week + + assertEquals(LocalDate(2019, Month.APRIL, 29), week.days.first().date) + assertEquals(LocalDate(2019, Month.MAY, 5), week.days.last().date) + } + + @Test + @JsName("test3") + fun `week in date generation works as expected`() { + val may01 = may2019.atDay(1) + val nov01 = november2019.atDay(1) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, nov01, firstDayOfWeek) + val week = getWeekCalendarData(adjustedWeekRange.startDateAdjusted, 0, may01, nov01).week + + val inDates = week.days.take(2) + val rangeDays = week.days.takeLast(5) + assertTrue(inDates.all { it.position == WeekDayPosition.InDate }) + assertTrue(rangeDays.all { it.position == WeekDayPosition.RangeDate }) + assertEquals(7, week.days.count()) + } + + @Test + @JsName("test4") + fun `week out date generation works as expected`() { + val may01 = may2019.atDay(1) + val may31 = may2019.atDay(31) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, may31, firstDayOfWeek) + val week = getWeekCalendarData(adjustedWeekRange.startDateAdjusted, 4, may01, may31).week + + val outDates = week.days.takeLast(2) + val rangeDays = week.days.take(5) + assertTrue(outDates.all { it.position == WeekDayPosition.OutDate }) + assertTrue(rangeDays.all { it.position == WeekDayPosition.RangeDate }) + assertEquals(7, week.days.count()) + } + + @Test + @JsName("test5") + fun `days are in the appropriate week columns`() { + val may01 = may2019.atDay(2) + val may31 = may2019.atDay(31) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, may31, firstDayOfWeek) + val week = getWeekCalendarData(adjustedWeekRange.startDateAdjusted, 0, may01, may31).week + + val daysOfWeek = daysOfWeek(firstDayOfWeek) + week.days.forEachIndexed { index, day -> + assertEquals(daysOfWeek[index], day.date.dayOfWeek) + } + } + + @Test + @JsName("test6") + fun `generated week is at the correct offset`() { + val may01 = may2019.atDay(2) + val may31 = may2019.atDay(31) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, may31, firstDayOfWeek) + val week = getWeekCalendarData(adjustedWeekRange.startDateAdjusted, 2, may01, may31).week + + assertEquals(may2019.atDay(13), week.days.first().date) + assertEquals(may2019.atDay(19), week.days.last().date) + } + + @Test + @JsName("test7") + fun `week index calculation works as expected`() { + val may01 = may2019.atDay(2) + val may31 = may2019.atDay(31) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, may31, firstDayOfWeek) + val index = getWeekIndex(adjustedWeekRange.startDateAdjusted, may31) + + assertEquals(4, index) + } + + @Test + @JsName("test8") + fun `week indices count calculation works as expected`() { + val may01 = may2019.atDay(2) + val may31 = may2019.atDay(31) + val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, may31, firstDayOfWeek) + val count = getWeekIndicesCount(adjustedWeekRange.startDateAdjusted, may31) + + assertEquals(5, count) + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt new file mode 100644 index 00000000..1b43a26e --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt @@ -0,0 +1,101 @@ +package com.kizitonwose.calendar.data + +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.utils.weeksInMonth +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.Month +import kotlin.js.JsName +import kotlin.test.Test +import kotlin.test.assertEquals + +class YearDataTests { + + @Test + @JsName("test1") + fun `year data is accurate with non-leap year`() { + val year = Year(2019) + val firstDayOfWeek = DayOfWeek.MONDAY + val outDateStyle = OutDateStyle.EndOfRow + val yearData = getCalendarYearData(year, 0, firstDayOfWeek, outDateStyle) + val months = yearData.months + val days = yearData.months.flatMap { it.weekDays }.flatten() + yearData.months.forEachIndexed { index, month -> + val monthData = getCalendarMonthData( + startMonth = YearMonth(year.value, Month.JANUARY), + offset = month.yearMonth.month.ordinal, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ) + assertEquals(monthData.calendarMonth, month) + assertEquals(Month.entries[Month.JANUARY.ordinal + index], month.yearMonth.month) + assertEquals(month.yearMonth.weeksInMonth(firstDayOfWeek), month.weekDays.count()) + } + assertEquals(12, months.count()) + assertEquals(year, yearData.year) + assertEquals(36, days.count { it.position == DayPosition.InDate }) + assertEquals(33, days.count { it.position == DayPosition.OutDate }) + assertEquals(365, days.count { it.position == DayPosition.MonthDate }) + } + + @Test + @JsName("test2") + fun `year data is accurate with leap year`() { + val year = Year(2020) + val firstDayOfWeek = DayOfWeek.SUNDAY + val outDateStyle = OutDateStyle.EndOfGrid + val yearData = getCalendarYearData(year, 0, firstDayOfWeek, outDateStyle) + val months = yearData.months + val days = yearData.months.flatMap { it.weekDays }.flatten() + yearData.months.forEachIndexed { index, month -> + val monthData = getCalendarMonthData( + startMonth = YearMonth(year.value, Month.JANUARY), + offset = month.yearMonth.month.ordinal, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = outDateStyle, + ) + assertEquals(monthData.calendarMonth, month) + assertEquals(Month.entries[Month.JANUARY.ordinal + index], month.yearMonth.month) + assertEquals(6, month.weekDays.count()) + val weeksWithoutGridOutDates = month.weekDays.filterNot { week -> week.all { it.position == DayPosition.OutDate } } + assertEquals(month.yearMonth.weeksInMonth(firstDayOfWeek), weeksWithoutGridOutDates.count()) + } + assertEquals(12, months.count()) + assertEquals(year, yearData.year) + assertEquals(35, days.count { it.position == DayPosition.InDate }) + assertEquals(103, days.count { it.position == DayPosition.OutDate }) + assertEquals(366, days.count { it.position == DayPosition.MonthDate }) + } + + @Test + @JsName("test3") + fun `generated year is at the correct offset`() { + val yearData = getCalendarYearData(Year(2020), 6, DayOfWeek.SUNDAY, OutDateStyle.EndOfGrid) + val yearData2 = getCalendarYearData(Year(2021), 0, DayOfWeek.SUNDAY, OutDateStyle.EndOfRow) + + assertEquals(yearData.year, Year(2026)) + assertEquals(yearData2.year, Year(2021)) + } + + @Test + @JsName("test4") + fun `year index calculation works as expected`() { + val index = getYearIndex(startYear = Year(2020), targetYear = Year(2030)) + val index2 = getYearIndex(startYear = Year(2052), targetYear = Year(2052)) + + assertEquals(10, index) + assertEquals(0, index2) + } + + @Test + @JsName("test5") + fun `year indices count calculation works as expected`() { + val count = getYearIndicesCount(startYear = Year(2020), endYear = Year(2040)) + val count2 = getYearIndicesCount(startYear = Year(2052), endYear = Year(2052)) + + assertEquals(21, count) + assertEquals(1, count2) + } +} diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/utils/Utils.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/utils/Utils.kt new file mode 100644 index 00000000..ed4f88c8 --- /dev/null +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/utils/Utils.kt @@ -0,0 +1,23 @@ +package com.kizitonwose.calendar.utils + +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.plusMonths +import com.kizitonwose.calendar.data.getCalendarMonthData +import kotlinx.datetime.DayOfWeek + +internal infix fun Pair.toTriple(that: C): Triple = Triple(first, second, that) + +internal fun YearMonth.weeksInMonth(firstDayOfWeek: DayOfWeek) = getCalendarMonthData( + startMonth = this, + offset = 0, + firstDayOfWeek = firstDayOfWeek, + outDateStyle = OutDateStyle.EndOfRow, +).calendarMonth.weekDays.count() + +internal val YearMonth.nextMonth: YearMonth + get() = this.plusMonths(1) + +internal val YearMonth.previousMonth: YearMonth + get() = this.minusMonths(1) diff --git a/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt index b9a4e6eb..1dd446c0 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt @@ -9,15 +9,13 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.time.DayOfWeek -import java.time.Month.DECEMBER -import java.time.Month.NOVEMBER -import java.time.Month.OCTOBER +import java.time.Month import java.time.YearMonth class HeatMapDataTests { - private val october2022 = YearMonth.of(2022, OCTOBER) - private val november2022 = YearMonth.of(2022, NOVEMBER) - private val december2022 = YearMonth.of(2022, DECEMBER) + private val october2022 = YearMonth.of(2022, Month.OCTOBER) + private val november2022 = YearMonth.of(2022, Month.NOVEMBER) + private val december2022 = YearMonth.of(2022, Month.DECEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY /** October, November and December 2022 diff --git a/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt index 21e09393..cbe3fbfa 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt @@ -10,13 +10,12 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.time.DayOfWeek -import java.time.Month.MAY -import java.time.Month.NOVEMBER +import java.time.Month import java.time.YearMonth class MonthDataTests { - private val may2019 = YearMonth.of(2019, MAY) - private val november2019 = YearMonth.of(2019, NOVEMBER) + private val may2019 = YearMonth.of(2019, Month.MAY) + private val november2019 = YearMonth.of(2019, Month.NOVEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY /** May and November 2019 with Monday as the first day of week. diff --git a/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt index 435a733c..70bdb9a9 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt @@ -7,14 +7,12 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate -import java.time.Month.APRIL -import java.time.Month.MAY -import java.time.Month.NOVEMBER +import java.time.Month import java.time.YearMonth class WeekDataTests { - private val may2019 = YearMonth.of(2019, MAY) - private val november2019 = YearMonth.of(2019, NOVEMBER) + private val may2019 = YearMonth.of(2019, Month.MAY) + private val november2019 = YearMonth.of(2019, Month.NOVEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY /** May and November 2019 with Monday as the first day of week. @@ -41,8 +39,8 @@ class WeekDataTests { val nov01 = november2019.atDay(1) val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, nov01, firstDayOfWeek) - assertEquals(LocalDate.of(2019, APRIL, 29), adjustedWeekRange.startDateAdjusted) - assertEquals(LocalDate.of(2019, NOVEMBER, 3), adjustedWeekRange.endDateAdjusted) + assertEquals(LocalDate.of(2019, Month.APRIL, 29), adjustedWeekRange.startDateAdjusted) + assertEquals(LocalDate.of(2019, Month.NOVEMBER, 3), adjustedWeekRange.endDateAdjusted) } @Test @@ -52,8 +50,8 @@ class WeekDataTests { val adjustedWeekRange = getWeekCalendarAdjustedRange(may01, nov01, firstDayOfWeek) val week = getWeekCalendarData(adjustedWeekRange.startDateAdjusted, 0, may01, nov01).week - assertEquals(LocalDate.of(2019, APRIL, 29), week.days.first().date) - assertEquals(LocalDate.of(2019, MAY, 5), week.days.last().date) + assertEquals(LocalDate.of(2019, Month.APRIL, 29), week.days.first().date) + assertEquals(LocalDate.of(2019, Month.MAY, 5), week.days.last().date) } @Test diff --git a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt index 3cdac7ae..423e921a 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.Month -import java.time.Month.JANUARY import java.time.Year import java.time.YearMonth @@ -22,13 +21,13 @@ class YearDataTests { val days = yearData.months.flatMap { it.weekDays }.flatten() yearData.months.forEachIndexed { index, month -> val monthData = getCalendarMonthData( - startMonth = YearMonth.of(year.value, JANUARY), + startMonth = YearMonth.of(year.value, Month.JANUARY), offset = month.yearMonth.month.ordinal, firstDayOfWeek = firstDayOfWeek, outDateStyle = outDateStyle, ) assertEquals(monthData.calendarMonth, month) - assertEquals(Month.entries[JANUARY.ordinal + index], month.yearMonth.month) + assertEquals(Month.entries[Month.JANUARY.ordinal + index], month.yearMonth.month) assertEquals(month.yearMonth.weeksInMonth(firstDayOfWeek), month.weekDays.count()) } assertEquals(12, months.count()) @@ -48,13 +47,13 @@ class YearDataTests { val days = yearData.months.flatMap { it.weekDays }.flatten() yearData.months.forEachIndexed { index, month -> val monthData = getCalendarMonthData( - startMonth = YearMonth.of(year.value, JANUARY), + startMonth = YearMonth.of(year.value, Month.JANUARY), offset = month.yearMonth.month.ordinal, firstDayOfWeek = firstDayOfWeek, outDateStyle = outDateStyle, ) assertEquals(monthData.calendarMonth, month) - assertEquals(Month.entries[JANUARY.ordinal + index], month.yearMonth.month) + assertEquals(Month.entries[Month.JANUARY.ordinal + index], month.yearMonth.month) assertEquals(6, month.weekDays.count()) val weeksWithoutGridOutDates = month.weekDays.filterNot { week -> week.all { it.position == DayPosition.OutDate } } assertEquals(month.yearMonth.weeksInMonth(firstDayOfWeek), weeksWithoutGridOutDates.count()) From 4ae762d5f897f986ce6e3a0cb3073fc5f6a89fc2 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 06:51:04 +0200 Subject: [PATCH 30/48] Clean up tests --- .../compose/{CalendarStateTests.kt => CalendarStateTest.kt} | 2 +- ...HeatMapCalendarStateTests.kt => HeatMapCalendarStateTest.kt} | 2 +- .../calendar/compose/{StateSaverTests.kt => StateSaverTest.kt} | 2 +- .../{WeekCalendarStateTests.kt => WeekCalendarStateTest.kt} | 2 +- .../{YearCalendarStateTests.kt => YearCalendarStateTest.kt} | 2 +- .../calendar/data/{HeatMapDataTests.kt => HeatMapDataTest.kt} | 2 +- .../calendar/data/{MonthDataTests.kt => MonthDataTest.kt} | 2 +- .../calendar/data/{WeekDataTests.kt => WeekDataTest.kt} | 2 +- .../calendar/data/{YearDataTests.kt => YearDataTest.kt} | 2 +- .../compose/{CalendarStateTests.kt => CalendarStateTest.kt} | 2 +- ...HeatMapCalendarStateTests.kt => HeatMapCalendarStateTest.kt} | 2 +- .../calendar/compose/{StateSaverTests.kt => StateSaverTest.kt} | 2 +- .../{WeekCalendarStateTests.kt => WeekCalendarStateTest.kt} | 2 +- .../{YearCalendarStateTests.kt => YearCalendarStateTest.kt} | 2 +- .../calendar/data/{DayOfWeekTests.kt => DayOfWeekTest.kt} | 2 +- .../calendar/data/{HeatMapDataTests.kt => HeatMapDataTest.kt} | 2 +- .../calendar/data/{MonthDataTests.kt => MonthDataTest.kt} | 2 +- .../calendar/data/{WeekDataTests.kt => WeekDataTest.kt} | 2 +- .../calendar/data/{YearDataTests.kt => YearDataTest.kt} | 2 +- .../sample/{CalendarComposeTests.kt => CalendarComposeTest.kt} | 2 +- .../sample/{CalendarViewTests.kt => CalendarViewTest.kt} | 2 +- .../{WeekCalendarViewTests.kt => WeekCalendarViewTest.kt} | 2 +- 22 files changed, 22 insertions(+), 22 deletions(-) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/{CalendarStateTests.kt => CalendarStateTest.kt} (99%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/{HeatMapCalendarStateTests.kt => HeatMapCalendarStateTest.kt} (98%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/{StateSaverTests.kt => StateSaverTest.kt} (99%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/{WeekCalendarStateTests.kt => WeekCalendarStateTest.kt} (98%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/{YearCalendarStateTests.kt => YearCalendarStateTest.kt} (99%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/{HeatMapDataTests.kt => HeatMapDataTest.kt} (99%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/{MonthDataTests.kt => MonthDataTest.kt} (99%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/{WeekDataTests.kt => WeekDataTest.kt} (99%) rename compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/{YearDataTests.kt => YearDataTest.kt} (99%) rename compose/src/test/java/com/kizitonwose/calendar/compose/{CalendarStateTests.kt => CalendarStateTest.kt} (99%) rename compose/src/test/java/com/kizitonwose/calendar/compose/{HeatMapCalendarStateTests.kt => HeatMapCalendarStateTest.kt} (98%) rename compose/src/test/java/com/kizitonwose/calendar/compose/{StateSaverTests.kt => StateSaverTest.kt} (99%) rename compose/src/test/java/com/kizitonwose/calendar/compose/{WeekCalendarStateTests.kt => WeekCalendarStateTest.kt} (98%) rename compose/src/test/java/com/kizitonwose/calendar/compose/{YearCalendarStateTests.kt => YearCalendarStateTest.kt} (99%) rename data/src/test/java/com/kizitonwose/calendar/data/{DayOfWeekTests.kt => DayOfWeekTest.kt} (97%) rename data/src/test/java/com/kizitonwose/calendar/data/{HeatMapDataTests.kt => HeatMapDataTest.kt} (99%) rename data/src/test/java/com/kizitonwose/calendar/data/{MonthDataTests.kt => MonthDataTest.kt} (99%) rename data/src/test/java/com/kizitonwose/calendar/data/{WeekDataTests.kt => WeekDataTest.kt} (99%) rename data/src/test/java/com/kizitonwose/calendar/data/{YearDataTests.kt => YearDataTest.kt} (99%) rename sample/src/androidTest/java/com/kizitonwose/calendar/sample/{CalendarComposeTests.kt => CalendarComposeTest.kt} (99%) rename sample/src/androidTest/java/com/kizitonwose/calendar/sample/{CalendarViewTests.kt => CalendarViewTest.kt} (99%) rename sample/src/androidTest/java/com/kizitonwose/calendar/sample/{WeekCalendarViewTests.kt => WeekCalendarViewTest.kt} (99%) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTest.kt index e842530c..e580b16d 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/CalendarStateTest.kt @@ -14,7 +14,7 @@ import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -class CalendarStateTests { +class CalendarStateTest { @Test @JsName("test1") fun `start month update is reflected in the state`() { diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt similarity index 98% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt index aa9054b6..6bba9952 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt @@ -14,7 +14,7 @@ import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -class HeatMapCalendarStateTests { +class HeatMapCalendarStateTest { @Test @JsName("test1") fun `start month update is reflected in the state`() { diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTest.kt index e7f5f6bb..e70ad3bd 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/StateSaverTest.kt @@ -23,7 +23,7 @@ import kotlin.test.assertEquals * being saved. Such issues are typically not caught during development since * state restoration (e.g via rotation) will likely not happen often. */ -class StateSaverTests { +class StateSaverTest { @Test @JsName("test1") fun `month calendar state can be restored`() { diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt similarity index 98% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt index 6c213280..391b3b14 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt @@ -13,7 +13,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class WeekCalendarStateTests { +class WeekCalendarStateTest { @Test @JsName("test1") fun `start date update is reflected in the state`() { diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTest.kt index dbc1b7e7..dfa79ae3 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/compose/YearCalendarStateTest.kt @@ -16,7 +16,7 @@ import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -class YearCalendarStateTests { +class YearCalendarStateTest { @Test @JsName("test1") fun `start year update is reflected in the state`() { diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTest.kt index 3cd97917..5cfb4928 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/HeatMapDataTest.kt @@ -13,7 +13,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class HeatMapDataTests { +class HeatMapDataTest { private val october2022 = YearMonth(2022, Month.OCTOBER) private val november2022 = YearMonth(2022, Month.NOVEMBER) private val december2022 = YearMonth(2022, Month.DECEMBER) diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTest.kt index 0e6bad95..0c53c23a 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/MonthDataTest.kt @@ -14,7 +14,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class MonthDataTests { +class MonthDataTest { private val may2019 = YearMonth(2019, Month.MAY) private val november2019 = YearMonth(2019, Month.NOVEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTest.kt index 04027109..bfa9b0c3 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/WeekDataTest.kt @@ -12,7 +12,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class WeekDataTests { +class WeekDataTest { private val may2019 = YearMonth(2019, Month.MAY) private val november2019 = YearMonth(2019, Month.NOVEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt similarity index 99% rename from compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt rename to compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt index 1b43a26e..1cb74a27 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTests.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt @@ -11,7 +11,7 @@ import kotlin.js.JsName import kotlin.test.Test import kotlin.test.assertEquals -class YearDataTests { +class YearDataTest { @Test @JsName("test1") diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTest.kt similarity index 99% rename from compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt rename to compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTest.kt index 83c711f8..2f515094 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/CalendarStateTest.kt @@ -8,7 +8,7 @@ import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth -class CalendarStateTests { +class CalendarStateTest { @Test fun `start month update is reflected in the state`() { val now = YearMonth.now() diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt similarity index 98% rename from compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt rename to compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt index e6ea9061..cabe5edb 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/HeatMapCalendarStateTest.kt @@ -8,7 +8,7 @@ import java.time.DayOfWeek import java.time.LocalDate import java.time.YearMonth -class HeatMapCalendarStateTests { +class HeatMapCalendarStateTest { @Test fun `start month update is reflected in the state`() { val now = YearMonth.now() diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTest.kt similarity index 99% rename from compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt rename to compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTest.kt index fb43a0d0..041146b0 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/StateSaverTest.kt @@ -20,7 +20,7 @@ import java.time.YearMonth * being saved. Such issues are typically not caught during development since * state restoration (e.g via rotation) will likely not happen often. */ -class StateSaverTests { +class StateSaverTest { @Test fun `month calendar state can be restored`() { val now = YearMonth.now() diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt similarity index 98% rename from compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt rename to compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt index a0236f0b..933d8beb 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/WeekCalendarStateTest.kt @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test import java.time.DayOfWeek import java.time.LocalDate -class WeekCalendarStateTests { +class WeekCalendarStateTest { @Test fun `start date update is reflected in the state`() { val now = LocalDate.now() diff --git a/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt b/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTest.kt similarity index 99% rename from compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt rename to compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTest.kt index 5a26da44..362def3b 100644 --- a/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTests.kt +++ b/compose/src/test/java/com/kizitonwose/calendar/compose/YearCalendarStateTest.kt @@ -10,7 +10,7 @@ import java.time.LocalDate import java.time.Month import java.time.Year -class YearCalendarStateTests { +class YearCalendarStateTest { @Test fun `start year update is reflected in the state`() { val now = Year.now() diff --git a/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTest.kt similarity index 97% rename from data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt rename to data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTest.kt index b4d847f1..e3ca9979 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/DayOfWeekTest.kt @@ -5,7 +5,7 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import java.time.DayOfWeek -class DayOfWeekTests { +class DayOfWeekTest { @Test fun `days until works as expected`() { assertEquals(5, DayOfWeek.FRIDAY.daysUntil(DayOfWeek.WEDNESDAY)) diff --git a/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTest.kt similarity index 99% rename from data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt rename to data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTest.kt index 1dd446c0..a5389248 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/HeatMapDataTest.kt @@ -12,7 +12,7 @@ import java.time.DayOfWeek import java.time.Month import java.time.YearMonth -class HeatMapDataTests { +class HeatMapDataTest { private val october2022 = YearMonth.of(2022, Month.OCTOBER) private val november2022 = YearMonth.of(2022, Month.NOVEMBER) private val december2022 = YearMonth.of(2022, Month.DECEMBER) diff --git a/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTest.kt similarity index 99% rename from data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt rename to data/src/test/java/com/kizitonwose/calendar/data/MonthDataTest.kt index cbe3fbfa..9988790e 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/MonthDataTest.kt @@ -13,7 +13,7 @@ import java.time.DayOfWeek import java.time.Month import java.time.YearMonth -class MonthDataTests { +class MonthDataTest { private val may2019 = YearMonth.of(2019, Month.MAY) private val november2019 = YearMonth.of(2019, Month.NOVEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY diff --git a/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTest.kt similarity index 99% rename from data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt rename to data/src/test/java/com/kizitonwose/calendar/data/WeekDataTest.kt index 70bdb9a9..7b471530 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/WeekDataTest.kt @@ -10,7 +10,7 @@ import java.time.LocalDate import java.time.Month import java.time.YearMonth -class WeekDataTests { +class WeekDataTest { private val may2019 = YearMonth.of(2019, Month.MAY) private val november2019 = YearMonth.of(2019, Month.NOVEMBER) private val firstDayOfWeek = DayOfWeek.MONDAY diff --git a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt similarity index 99% rename from data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt rename to data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt index 423e921a..ec38befe 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTests.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt @@ -9,7 +9,7 @@ import java.time.Month import java.time.Year import java.time.YearMonth -class YearDataTests { +class YearDataTest { @Test fun `year data is accurate with non-leap year`() { diff --git a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarComposeTests.kt b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarComposeTest.kt similarity index 99% rename from sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarComposeTests.kt rename to sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarComposeTest.kt index 4efb7bf6..857ef4a2 100644 --- a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarComposeTests.kt +++ b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarComposeTest.kt @@ -44,7 +44,7 @@ import java.time.temporal.WeekFields */ @RunWith(AndroidJUnit4::class) @LargeTest -class CalendarComposeTests { +class CalendarComposeTest { @get:Rule val composeTestRule = createComposeRule() diff --git a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTests.kt b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt similarity index 99% rename from sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTests.kt rename to sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt index 96f6141e..1fc86863 100644 --- a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTests.kt +++ b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/CalendarViewTest.kt @@ -41,7 +41,7 @@ import java.time.YearMonth */ @RunWith(AndroidJUnit4::class) @LargeTest -class CalendarViewTests { +class CalendarViewTest { @get:Rule val homeScreenRule = ActivityScenarioRule(CalendarViewActivity::class.java) diff --git a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/WeekCalendarViewTests.kt b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/WeekCalendarViewTest.kt similarity index 99% rename from sample/src/androidTest/java/com/kizitonwose/calendar/sample/WeekCalendarViewTests.kt rename to sample/src/androidTest/java/com/kizitonwose/calendar/sample/WeekCalendarViewTest.kt index 6dbcc75a..9f1dbea4 100644 --- a/sample/src/androidTest/java/com/kizitonwose/calendar/sample/WeekCalendarViewTests.kt +++ b/sample/src/androidTest/java/com/kizitonwose/calendar/sample/WeekCalendarViewTest.kt @@ -30,7 +30,7 @@ import java.time.YearMonth */ @RunWith(AndroidJUnit4::class) @LargeTest -class WeekCalendarViewTests { +class WeekCalendarViewTest { @get:Rule val homeScreenRule = ActivityScenarioRule(CalendarViewActivity::class.java) From c125c9c13a5776807e74566e74ab818f6dff52aa Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 07:13:31 +0200 Subject: [PATCH 31/48] Add multiplatform full screen example --- .../sample/src/commonMain/kotlin/App.kt | 1 + .../sample/src/commonMain/kotlin/Colors.kt | 4 + .../src/commonMain/kotlin/Example8Page.kt | 249 ++++++++++++++++++ .../sample/src/commonMain/kotlin/ListPage.kt | 13 +- 4 files changed, 260 insertions(+), 7 deletions(-) create mode 100644 compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt index 33d27d97..c4afc553 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt @@ -170,6 +170,7 @@ private fun AppNavHost( horizontallyAnimatedComposable(Page.Example5.name) { Example5Page { navController.popBackStack() } } horizontallyAnimatedComposable(Page.Example6.name) { Example6Page() } horizontallyAnimatedComposable(Page.Example7.name) { Example7Page() } + horizontallyAnimatedComposable(Page.Example8.name) { Example8Page() } horizontallyAnimatedComposable(Page.Example9.name) { Example9Page() } } } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt index fcb1535a..bd7a0d7b 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt @@ -2,6 +2,10 @@ import androidx.compose.ui.graphics.Color object Colors { val example1Selection = Color(0xFFFCCA3E) + val example1Bg = Color(0xFF3A284C) + val example1BgLight = Color(0xFF433254) + val example1BgSecondary = Color(0xFF51356E) + val example1WhiteLight = Color(0x4DFFFFFF) val inactiveText = Color(0xFFBEBEBE) val example4Gray = Color(0xFF474747) val example4GrayPast = Color(0xFFBEBEBE) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt new file mode 100644 index 00000000..f6e164ab --- /dev/null +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt @@ -0,0 +1,249 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kizitonwose.calendar.compose.CalendarState +import com.kizitonwose.calendar.compose.ContentHeightMode +import com.kizitonwose.calendar.compose.HorizontalCalendar +import com.kizitonwose.calendar.compose.VerticalCalendar +import com.kizitonwose.calendar.compose.rememberCalendarState +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.OutDateStyle +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.minusMonths +import com.kizitonwose.calendar.core.now +import com.kizitonwose.calendar.core.plusMonths +import com.kizitonwose.calendar.core.yearMonth +import kotlinx.coroutines.launch +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun Example8Page(horizontal: Boolean = true) { + val today = remember { LocalDate.now() } + val currentMonth = remember(today) { today.yearMonth } + val startMonth = remember { currentMonth.minusMonths(500) } + val endMonth = remember { currentMonth.plusMonths(500) } + val selections = remember { mutableStateListOf() } + val daysOfWeek = remember { daysOfWeek() } + Column( + modifier = Modifier + .fillMaxSize() + .background(Colors.example1BgLight) + .padding(top = 20.dp), + ) { + val state = rememberCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = currentMonth, + firstDayOfWeek = daysOfWeek.first(), + outDateStyle = OutDateStyle.EndOfGrid, + ) + val coroutineScope = rememberCoroutineScope() + val visibleMonth = rememberFirstVisibleMonthAfterScroll(state) + // Draw light content on dark background. + CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + SimpleCalendarTitle( + modifier = Modifier.padding(bottom = 14.dp, start = 8.dp, end = 8.dp), + currentMonth = visibleMonth.yearMonth, + goToPrevious = { + coroutineScope.launch { + state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previous) + } + }, + goToNext = { + coroutineScope.launch { + state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.next) + } + }, + ) + FullScreenCalendar( + modifier = Modifier + .fillMaxSize() + .background(Colors.example1Bg) + .testTag("Calendar"), + state = state, + horizontal = horizontal, + dayContent = { day -> + Day( + day = day, + isSelected = selections.contains(day), + isToday = day.position == DayPosition.MonthDate && day.date == today, + ) { clicked -> + if (selections.contains(clicked)) { + selections.remove(clicked) + } else { + selections.add(clicked) + } + } + }, + // The month body is only needed for ui test tag. + monthBody = { _, content -> + Box( + modifier = Modifier + .weight(1f) + .testTag("MonthBody"), + ) { + content() + } + }, + monthHeader = { + MonthHeader(daysOfWeek = daysOfWeek) + }, + monthFooter = { month -> + val count = month.weekDays.flatten() + .count { selections.contains(it) } + MonthFooter(selectionCount = count) + }, + ) + } + } +} + +@Composable +private fun FullScreenCalendar( + modifier: Modifier, + state: CalendarState, + horizontal: Boolean, + dayContent: @Composable BoxScope.(CalendarDay) -> Unit, + monthHeader: @Composable ColumnScope.(CalendarMonth) -> Unit, + monthBody: @Composable ColumnScope.(CalendarMonth, content: @Composable () -> Unit) -> Unit, + monthFooter: @Composable ColumnScope.(CalendarMonth) -> Unit, +) { + if (horizontal) { + HorizontalCalendar( + modifier = modifier, + state = state, + calendarScrollPaged = true, + contentHeightMode = ContentHeightMode.Fill, + dayContent = dayContent, + monthBody = monthBody, + monthHeader = monthHeader, + monthFooter = monthFooter, + ) + } else { + VerticalCalendar( + modifier = modifier, + state = state, + calendarScrollPaged = true, + contentHeightMode = ContentHeightMode.Fill, + dayContent = dayContent, + monthBody = monthBody, + monthHeader = monthHeader, + monthFooter = monthFooter, + ) + } +} + +@Composable +private fun MonthHeader(daysOfWeek: List) { + Row( + Modifier + .fillMaxWidth() + .testTag("MonthHeader") + .background(Colors.example1BgSecondary) + .padding(vertical = 8.dp), + ) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = 15.sp, + text = dayOfWeek.displayText(), + ) + } + } +} + +@Composable +private fun MonthFooter(selectionCount: Int) { + Box( + Modifier + .fillMaxWidth() + .testTag("MonthFooter") + .background(Colors.example1BgSecondary) + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center, + ) { + val text = if (selectionCount == 0) { + "No selections in this month" + } else if (selectionCount == 1) { + "$selectionCount selection in this month" + } else { + "$selectionCount selections in this month" + } + Text(text = text) + } +} + +@Composable +private fun Day( + day: CalendarDay, + isSelected: Boolean, + isToday: Boolean, + onClick: (CalendarDay) -> Unit, +) { + Box( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .clip(RectangleShape) + .background( + color = when { + isSelected -> Colors.example1Selection + isToday -> Colors.example1WhiteLight + else -> Color.Transparent + }, + ) + // Disable clicks on inDates/outDates + .clickable( + enabled = day.position == DayPosition.MonthDate, + showRipple = !isSelected, + onClick = { onClick(day) }, + ), + contentAlignment = Alignment.Center, + ) { + val textColor = when (day.position) { + // Color.Unspecified will use the default text color from the current theme + DayPosition.MonthDate -> if (isSelected) Colors.example1Bg else Color.Unspecified + DayPosition.InDate, DayPosition.OutDate -> Colors.example1WhiteLight + } + Text( + text = day.date.dayOfMonth.toString(), + color = textColor, + fontSize = 15.sp, + ) + } +} + +@Preview +@Composable +private fun Example8Preview() { + Example8Page() +} diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt b/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt index b88cffc9..2664ea8a 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt @@ -57,14 +57,13 @@ enum class Page(val title: String, val subtitle: String, val showToolBar: Boolea subtitle = "Week Calendar - Continuous scroll, custom day content width, single selection.", showToolBar = true, ), - - // Example8( -// title = "Example 8", -// subtitle = "Fullscreen Horizontal Calendar - Month header and footer, paged horizontal scrolling. Shows the \"Fill\" option of ContentHeightMode property.", -// showToolBar = false, -// ), - Example9( + Example8( title = "Example 8", + subtitle = "Fullscreen Horizontal Calendar - Month header and footer, paged horizontal scrolling. Shows the \"Fill\" option of ContentHeightMode property.", + showToolBar = false, + ), + Example9( + title = "Example 9", subtitle = "Month and week calendar toggle with animations.", showToolBar = true, ), From b6f510dea1c963356be983c1b0b53cb6c5228b08 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 09:01:49 +0200 Subject: [PATCH 32/48] Add page options to full screen example. --- .../sample/src/commonMain/kotlin/App.kt | 2 +- .../src/commonMain/kotlin/Example8Page.kt | 83 +++++++++++++++---- .../commonMain/kotlin/SimpleCalendarTitle.kt | 12 ++- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt index c4afc553..7eba1a77 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt @@ -170,7 +170,7 @@ private fun AppNavHost( horizontallyAnimatedComposable(Page.Example5.name) { Example5Page { navController.popBackStack() } } horizontallyAnimatedComposable(Page.Example6.name) { Example6Page() } horizontallyAnimatedComposable(Page.Example7.name) { Example7Page() } - horizontallyAnimatedComposable(Page.Example8.name) { Example8Page() } + horizontallyAnimatedComposable(Page.Example8.name) { Example8Page() { navController.popBackStack() } } horizontallyAnimatedComposable(Page.Example9.name) { Example9Page() } } } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt index f6e164ab..611f8d27 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt @@ -1,4 +1,5 @@ import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column @@ -8,14 +9,23 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -45,7 +55,7 @@ import kotlinx.datetime.LocalDate import org.jetbrains.compose.ui.tooling.preview.Preview @Composable -fun Example8Page(horizontal: Boolean = true) { +fun Example8Page(close: () -> Unit = {}) { val today = remember { LocalDate.now() } val currentMonth = remember(today) { today.yearMonth } val startMonth = remember { currentMonth.minusMonths(500) } @@ -58,19 +68,22 @@ fun Example8Page(horizontal: Boolean = true) { .background(Colors.example1BgLight) .padding(top = 20.dp), ) { - val state = rememberCalendarState( - startMonth = startMonth, - endMonth = endMonth, - firstVisibleMonth = currentMonth, - firstDayOfWeek = daysOfWeek.first(), - outDateStyle = OutDateStyle.EndOfGrid, - ) - val coroutineScope = rememberCoroutineScope() - val visibleMonth = rememberFirstVisibleMonthAfterScroll(state) // Draw light content on dark background. CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + var selectedIndex by remember { mutableStateOf(0) } + PageOptions(selectedIndex, close = close) { selectedIndex = it } + val state = rememberCalendarState( + startMonth = startMonth, + endMonth = endMonth, + firstVisibleMonth = currentMonth, + firstDayOfWeek = daysOfWeek.first(), + outDateStyle = OutDateStyle.EndOfGrid, + ) + val coroutineScope = rememberCoroutineScope() + val visibleMonth = rememberFirstVisibleMonthAfterScroll(state) SimpleCalendarTitle( - modifier = Modifier.padding(bottom = 14.dp, start = 8.dp, end = 8.dp), + modifier = Modifier.padding(bottom = 14.dp, top = 4.dp, start = 8.dp, end = 8.dp), + isHorizontal = selectedIndex == 0, currentMonth = visibleMonth.yearMonth, goToPrevious = { coroutineScope.launch { @@ -89,7 +102,7 @@ fun Example8Page(horizontal: Boolean = true) { .background(Colors.example1Bg) .testTag("Calendar"), state = state, - horizontal = horizontal, + horizontal = selectedIndex == 0, dayContent = { day -> Day( day = day, @@ -161,6 +174,42 @@ private fun FullScreenCalendar( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PageOptions(selectedIndex: Int, close: () -> Unit = {}, onSelect: (Int) -> Unit) { + Row( + modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + if (!isMobile()) { + Button( + onClick = close, + colors = ButtonDefaults.buttonColors().copy(containerColor = Colors.example1Bg), + ) { + Text("Close") + } + } + val options = listOf("Horizontal", "Vertical") + SingleChoiceSegmentedButtonRow(modifier = Modifier.weight(1f)) { + options.forEachIndexed { index, label -> + SegmentedButton( + colors = SegmentedButtonDefaults.colors().copy( + activeContainerColor = Colors.example1Bg, + activeContentColor = Color.White, + inactiveContainerColor = Color.Transparent, + inactiveContentColor = Color.White, + ), + shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + onClick = { onSelect(index) }, + selected = index == selectedIndex, + ) { + Text(label) + } + } + } + } +} + @Composable private fun MonthHeader(daysOfWeek: List) { Row( @@ -191,12 +240,10 @@ private fun MonthFooter(selectionCount: Int) { .padding(vertical = 10.dp), contentAlignment = Alignment.Center, ) { - val text = if (selectionCount == 0) { - "No selections in this month" - } else if (selectionCount == 1) { - "$selectionCount selection in this month" - } else { - "$selectionCount selections in this month" + val text = when (selectionCount) { + 0 -> "No selections in this month" + 1 -> "$selectionCount selection in this month" + else -> "$selectionCount selections in this month" } Text(text = text) } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/SimpleCalendarTitle.kt b/compose-multiplatform/sample/src/commonMain/kotlin/SimpleCalendarTitle.kt index 0fe2edbf..f0cf13ef 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/SimpleCalendarTitle.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/SimpleCalendarTitle.kt @@ -1,4 +1,4 @@ - +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -14,9 +14,11 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -30,6 +32,7 @@ import com.kizitonwose.calendar.core.YearMonth fun SimpleCalendarTitle( modifier: Modifier, currentMonth: YearMonth, + isHorizontal: Boolean = true, goToPrevious: () -> Unit, goToNext: () -> Unit, ) { @@ -41,6 +44,7 @@ fun SimpleCalendarTitle( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = "Previous", onClick = goToPrevious, + isHorizontal = isHorizontal, ) Text( modifier = Modifier @@ -55,6 +59,7 @@ fun SimpleCalendarTitle( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Next", onClick = goToNext, + isHorizontal = isHorizontal, ) } } @@ -63,6 +68,7 @@ fun SimpleCalendarTitle( private fun CalendarNavigationIcon( imageVector: ImageVector, contentDescription: String, + isHorizontal: Boolean = true, onClick: () -> Unit, ) = Box( modifier = Modifier @@ -71,11 +77,13 @@ private fun CalendarNavigationIcon( .clip(shape = CircleShape) .clickable(role = Role.Button, onClick = onClick), ) { + val rotation by animateFloatAsState(if (isHorizontal) 0f else 90f) Icon( modifier = Modifier .fillMaxSize() .padding(4.dp) - .align(Alignment.Center), + .align(Alignment.Center) + .rotate(rotation), imageVector = imageVector, contentDescription = contentDescription, ) From 5ded0bb43f357873a060d002968845a07b3e5fef Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 09:56:49 +0200 Subject: [PATCH 33/48] Migrate sample to Material3 --- gradle/libs.versions.toml | 2 +- sample/build.gradle.kts | 2 +- .../sample/compose/CalendarComposeActivity.kt | 29 +++++++++++++------ .../calendar/sample/compose/Example10Page.kt | 10 +++---- .../calendar/sample/compose/Example11Page.kt | 6 ++-- .../calendar/sample/compose/Example1Page.kt | 2 +- .../calendar/sample/compose/Example2Page.kt | 16 +++++----- .../calendar/sample/compose/Example3Page.kt | 24 +++++++-------- .../calendar/sample/compose/Example4Page.kt | 6 ++-- .../calendar/sample/compose/Example5Page.kt | 7 +++-- .../calendar/sample/compose/Example6Page.kt | 4 +-- .../calendar/sample/compose/Example7Page.kt | 8 ++--- .../calendar/sample/compose/Example8Page.kt | 8 ++--- .../calendar/sample/compose/Example9Page.kt | 8 ++--- .../calendar/sample/compose/ListPage.kt | 24 +++++---------- .../sample/compose/SimpleCalendarTitle.kt | 4 +-- .../calendar/sample/compose/Utils.kt | 2 +- 17 files changed, 83 insertions(+), 79 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 735d0f9f..b5bc8a1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,7 +35,7 @@ compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } -compose-material = { module = "androidx.compose.material:material", version.ref = "compose" } +compose-material3 = { module = "androidx.compose.material3:material3", version = "1.3.0-beta05" } compose-activity = { module = "androidx.activity:activity-compose", version = "1.9.0" } compose-navigation = { module = "androidx.navigation:navigation-compose", version = "2.7.7" } diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index f425f849..b37110cd 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -59,7 +59,7 @@ dependencies { implementation(libs.compose.ui.tooling) implementation(libs.compose.foundation) implementation(libs.compose.runtime) - implementation(libs.compose.material) + implementation(libs.compose.material3) implementation(libs.compose.activity) implementation(libs.compose.navigation) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt index 1cc58d5a..76b79005 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt @@ -4,11 +4,14 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.padding -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar -import androidx.compose.material.rememberScaffoldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -17,6 +20,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -33,9 +37,9 @@ class CalendarComposeActivity : AppCompatActivity() { val primaryColor = colorResource(id = R.color.colorPrimary) var toolBarTitle by remember { mutableStateOf("") } var toolBarVisible by remember { mutableStateOf(true) } + val snackbarHostState = remember { SnackbarHostState() } val navController = rememberNavController() val coroutineScope = rememberCoroutineScope() - val scaffoldState = rememberScaffoldState() LaunchedEffect(navController) { navController.currentBackStackEntryFlow.collect { backStackEntry -> val page = Page.valueOf(backStackEntry.destination.route ?: return@collect) @@ -43,21 +47,23 @@ class CalendarComposeActivity : AppCompatActivity() { toolBarVisible = page.showToolBar } } - MaterialTheme(colors = MaterialTheme.colors.copy(primary = primaryColor)) { + MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy(primary = primaryColor)) { Scaffold( - scaffoldState = scaffoldState, topBar = { if (toolBarVisible) { AppToolBar(title = toolBarTitle, navController) } }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, content = { AppNavHost( modifier = Modifier.padding(it), navController = navController, showSnack = { message -> coroutineScope.launch { - scaffoldState.snackbarHostState.showSnackbar(message) + snackbarHostState.showSnackbar(message) } }, ) @@ -67,10 +73,15 @@ class CalendarComposeActivity : AppCompatActivity() { } } + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AppToolBar(title: String, navController: NavHostController) { TopAppBar( title = { Text(text = title) }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = Color.White, + ), navigationIcon = { NavigationIcon icon@{ val destination = diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index 902b3d3f..831463b2 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -18,9 +18,9 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf @@ -277,7 +277,7 @@ private fun Example10Preview() { private val headerBackground = Color(0xFFF1F1F1) private fun simpleTextColor(isSelected: Boolean) = - if (isSelected) darkColors().onSurface else lightColors().onSurface + if (isSelected) darkColorScheme().onSurface else lightColorScheme().onSurface private fun simpleTextBackground(isSelected: Boolean) = - if (isSelected) darkColors().surface else lightColors().surface + if (isSelected) darkColorScheme().surface else lightColorScheme().surface diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt index 5b1c001c..7cb5e9c6 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt @@ -12,8 +12,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember @@ -148,7 +148,7 @@ private fun YearHeader(year: Year) { text = year.toString(), fontWeight = FontWeight.Medium, ) - Divider() + HorizontalDivider() } } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt index 89d20a9d..842f8cd3 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example1Page.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2Page.kt index 9c4c3f55..5b7c1bc8 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2Page.kt @@ -19,11 +19,11 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.Divider -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -69,7 +69,7 @@ fun Example2Page( var selection by remember { mutableStateOf(DateSelection()) } val daysOfWeek = remember { daysOfWeek() } StatusBarColorUpdateEffect(Color.White) - MaterialTheme(colors = MaterialTheme.colors.copy(primary = primaryColor)) { + MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy(primary = primaryColor)) { Box( modifier = Modifier .fillMaxSize() @@ -247,7 +247,7 @@ private fun CalendarTop( } } } - Divider() + HorizontalDivider() } } @@ -258,7 +258,7 @@ private fun CalendarBottom( save: () -> Unit, ) { Column(modifier.fillMaxWidth()) { - Divider() + HorizontalDivider() Row( modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically, diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt index 225307de..9d878bb0 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt @@ -19,14 +19,14 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.LocalContentColor -import androidx.compose.material.LocalRippleConfiguration -import androidx.compose.material.RippleConfiguration -import androidx.compose.material.Text -import androidx.compose.material.darkColors import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.RippleConfiguration +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -107,7 +107,7 @@ fun Example3Page() { } // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColors().onSurface) { + CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { SimpleCalendarTitle( modifier = Modifier .background(toolbarColor) @@ -128,7 +128,7 @@ fun Example3Page() { modifier = Modifier.wrapContentWidth(), state = state, dayContent = { day -> - @OptIn(ExperimentalMaterialApi::class) + @OptIn(ExperimentalMaterial3Api::class) CompositionLocalProvider(LocalRippleConfiguration provides Example3RippleConfiguration) { val colors = if (day.position == DayPosition.MonthDate) { flights[day.date].orEmpty().map { colorResource(it.color) } @@ -151,7 +151,7 @@ fun Example3Page() { ) }, ) - Divider(color = pageBackgroundColor) + HorizontalDivider(color = pageBackgroundColor) LazyColumn(modifier = Modifier.fillMaxWidth()) { items(items = flightsInSelectedDate.value) { flight -> FlightInformation(flight) @@ -272,7 +272,7 @@ private fun LazyItemScope.FlightInformation(flight: Flight) { AirportInformation(flight.destination, isDeparture = false) } } - Divider(color = pageBackgroundColor, thickness = 2.dp) + HorizontalDivider(thickness = 2.dp, color = pageBackgroundColor) } @Composable @@ -322,7 +322,7 @@ private fun AirportInformation(airport: Airport, isDeparture: Boolean) { } // The default dark them ripple is too bright so we tone it down. -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterial3Api::class) private val Example3RippleConfiguration = RippleConfiguration( color = Color.Gray, // Copied from RippleTheme#DarkThemeRippleAlpha diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt index a161b470..45cbd928 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example4Page.kt @@ -14,8 +14,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Divider -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -125,7 +125,7 @@ private fun MonthHeader(calendarMonth: CalendarMonth) { ) } } - Divider(color = Color.Black) + HorizontalDivider(color = Color.Black) } } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example5Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example5Page.kt index 76ecbe3e..c2f58dde 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example5Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example5Page.kt @@ -10,8 +10,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -33,6 +34,7 @@ import com.kizitonwose.calendar.sample.shared.getWeekPageTitle import java.time.LocalDate import java.time.format.DateTimeFormatter +@OptIn(ExperimentalMaterial3Api::class) @Composable fun Example5Page(close: () -> Unit = {}) { val currentDate = remember { LocalDate.now() } @@ -51,7 +53,6 @@ fun Example5Page(close: () -> Unit = {}) { ) val visibleWeek = rememberFirstVisibleWeekAfterScroll(state) TopAppBar( - elevation = 0.dp, title = { Text(text = getWeekPageTitle(visibleWeek)) }, navigationIcon = { NavigationIcon(onBackClick = close) }, ) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt index d9ff85e0..70856752 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt @@ -13,8 +13,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt index 04e60159..fea485c5 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt @@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.LocalContentColor -import androidx.compose.material.Text -import androidx.compose.material.darkColors +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -54,7 +54,7 @@ fun Example7Page() { firstVisibleWeekDate = currentDate, ) // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColors().onSurface) { + CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { WeekCalendar( modifier = Modifier.padding(vertical = 4.dp), state = state, diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt index 1543e869..04efb43d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt @@ -10,9 +10,9 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.LocalContentColor -import androidx.compose.material.Text -import androidx.compose.material.darkColors +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableStateListOf @@ -75,7 +75,7 @@ fun Example8Page(horizontal: Boolean = true) { val coroutineScope = rememberCoroutineScope() val visibleMonth = rememberFirstVisibleMonthAfterScroll(state) // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColors().onSurface) { + CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { SimpleCalendarTitle( modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), currentMonth = visibleMonth.yearMonth, diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example9Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example9Page.kt index 0218c36d..7ce49e64 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example9Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example9Page.kt @@ -17,10 +17,10 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Checkbox -import androidx.compose.material.CheckboxDefaults -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CheckboxDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt index 9f1fdfbc..f63c0bdd 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/ListPage.kt @@ -8,16 +8,14 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.ContentAlpha -import androidx.compose.material.Divider -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp enum class Page(val title: String, val subtitle: String, val showToolBar: Boolean) { List( @@ -98,25 +96,19 @@ fun ListPage(click: (Page) -> Unit) { .padding(horizontal = 16.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - val titleStyle = MaterialTheme.typography.subtitle1 + val titleStyle = MaterialTheme.typography.titleMedium Text( text = item.title, fontWeight = FontWeight.Medium, - style = titleStyle.copy( - fontSize = 20.sp, - color = titleStyle.color.copy(alpha = ContentAlpha.high), - ), + style = titleStyle, ) - val subtitleStyle = MaterialTheme.typography.body2 + val subtitleStyle = MaterialTheme.typography.bodyMedium Text( text = item.subtitle, - style = subtitleStyle.copy( - fontSize = 16.sp, - color = subtitleStyle.color.copy(alpha = ContentAlpha.medium), - ), + style = subtitleStyle, ) } - Divider() + HorizontalDivider() } } } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt index 173a2e21..5231358a 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt @@ -9,11 +9,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index fa711fed..1535fd4b 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -10,9 +10,9 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf From c21b3f133c3f3acc069a21112461bce95d06949b Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 10:10:11 +0200 Subject: [PATCH 34/48] Add page options to Android full screen example. --- .../src/commonMain/kotlin/Example8Page.kt | 5 ++- .../calendar/sample/compose/Example8Page.kt | 39 ++++++++++++++++++- .../sample/compose/SimpleCalendarTitle.kt | 14 ++++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt index 611f8d27..f33c6eb7 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt @@ -1,3 +1,4 @@ + import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -21,8 +22,8 @@ import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -70,7 +71,7 @@ fun Example8Page(close: () -> Unit = {}) { ) { // Draw light content on dark background. CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { - var selectedIndex by remember { mutableStateOf(0) } + var selectedIndex by remember { mutableIntStateOf(0) } PageOptions(selectedIndex, close = close) { selectedIndex = it } val state = rememberCalendarState( startMonth = startMonth, diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt index 04efb43d..085a9b5d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt @@ -11,13 +11,19 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -51,7 +57,7 @@ import java.time.DayOfWeek import java.time.LocalDate @Composable -fun Example8Page(horizontal: Boolean = true) { +fun Example8Page(horizontal: Boolean? = null) { val today = remember { LocalDate.now() } val currentMonth = remember(today) { today.yearMonth } val startMonth = remember { currentMonth.minusMonths(500) } @@ -65,6 +71,8 @@ fun Example8Page(horizontal: Boolean = true) { .background(colorResource(id = R.color.example_1_bg_light)) .padding(top = 20.dp), ) { + var selectedIndex by remember { mutableIntStateOf(0) } + PageOptions(selectedIndex) { selectedIndex = it } val state = rememberCalendarState( startMonth = startMonth, endMonth = endMonth, @@ -79,6 +87,7 @@ fun Example8Page(horizontal: Boolean = true) { SimpleCalendarTitle( modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), currentMonth = visibleMonth.yearMonth, + isHorizontal = selectedIndex == 0, goToPrevious = { coroutineScope.launch { state.animateScrollToMonth(state.firstVisibleMonth.yearMonth.previousMonth) @@ -96,7 +105,7 @@ fun Example8Page(horizontal: Boolean = true) { .background(colorResource(id = R.color.example_1_bg)) .testTag("Calendar"), state = state, - horizontal = horizontal, + horizontal = horizontal ?: (selectedIndex == 0), dayContent = { day -> Day( day = day, @@ -168,6 +177,32 @@ private fun FullScreenCalendar( } } +@Composable +private fun PageOptions(selectedIndex: Int, onSelect: (Int) -> Unit) { + val options = listOf("Horizontal", "Vertical") + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp, horizontal = 16.dp), + ) { + options.forEachIndexed { index, label -> + SegmentedButton( + colors = SegmentedButtonDefaults.colors().copy( + activeContainerColor = colorResource(R.color.example_1_bg), + activeContentColor = Color.White, + inactiveContainerColor = Color.Transparent, + inactiveContentColor = Color.White, + ), + shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + onClick = { onSelect(index) }, + selected = index == selectedIndex, + ) { + Text(label) + } + } + } +} + @Composable private fun MonthHeader(daysOfWeek: List) { Row( diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt index 5231358a..2b6359a4 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/SimpleCalendarTitle.kt @@ -1,5 +1,6 @@ package com.kizitonwose.calendar.sample.compose +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row @@ -15,9 +16,11 @@ import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -32,6 +35,7 @@ import java.time.YearMonth fun SimpleCalendarTitle( modifier: Modifier, currentMonth: YearMonth, + isHorizontal: Boolean = true, goToPrevious: () -> Unit, goToNext: () -> Unit, ) { @@ -43,6 +47,7 @@ fun SimpleCalendarTitle( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, contentDescription = "Previous", onClick = goToPrevious, + isHorizontal = isHorizontal, ) Text( modifier = Modifier @@ -57,6 +62,7 @@ fun SimpleCalendarTitle( imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Next", onClick = goToNext, + isHorizontal = isHorizontal, ) } } @@ -65,6 +71,7 @@ fun SimpleCalendarTitle( private fun CalendarNavigationIcon( imageVector: ImageVector, contentDescription: String, + isHorizontal: Boolean = true, onClick: () -> Unit, ) = Box( modifier = Modifier @@ -73,11 +80,16 @@ private fun CalendarNavigationIcon( .clip(shape = CircleShape) .clickable(role = Role.Button, onClick = onClick), ) { + val rotation by animateFloatAsState( + targetValue = if (isHorizontal) 0f else 90f, + label = "CalendarNavigationIconAnimation", + ) Icon( modifier = Modifier .fillMaxSize() .padding(4.dp) - .align(Alignment.Center), + .align(Alignment.Center) + .rotate(rotation), imageVector = imageVector, contentDescription = contentDescription, ) From 8a5df62da214f20a34e73ede3d0177a069f02510 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 10:27:05 +0200 Subject: [PATCH 35/48] Clean up dependencies --- .../kotlin/com/kizitonwose/calendar/data/MonthData.kt | 2 +- .../kotlin/com/kizitonwose/calendar/data/WeekData.kt | 2 +- gradle/libs.versions.toml | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/MonthData.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/MonthData.kt index c940b023..e0a524c8 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/MonthData.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/MonthData.kt @@ -16,7 +16,7 @@ import com.kizitonwose.calendar.core.plusMonths import com.kizitonwose.calendar.core.yearMonth import kotlinx.datetime.DayOfWeek -internal data class MonthData internal constructor( +internal data class MonthData( private val month: YearMonth, private val inDays: Int, private val outDays: Int, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt index 4ca68408..5d96a3c3 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/data/WeekData.kt @@ -38,7 +38,7 @@ internal fun getWeekCalendarData( return WeekData(firstDayInWeek, desiredStartDate, desiredEndDate) } -internal data class WeekData internal constructor( +internal data class WeekData( private val firstDayInWeek: LocalDate, private val desiredStartDate: LocalDate, private val desiredEndDate: LocalDate, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5bc8a1c..f59193a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] agp = "8.5.1" kotlin = "2.0.0" -compose = "1.7.0-beta05" +compose = "1.7.0-beta06" espresso = "3.6.1" junit5 = "5.10.3" -kotlinxSerializationCore = "1.7.1" +kotlinxSerialization = "1.7.1" [libraries] kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationCore" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version = "0.6.0" } desugar = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" } @@ -29,14 +29,13 @@ androidx-test-junit = { module = "androidx.test.ext:junit", version = "1.2.1" } test-junit4 = { module = "junit:junit", version = "4.13.2" } test-junit5-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } -test-junit5-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit5" } compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } compose-material3 = { module = "androidx.compose.material3:material3", version = "1.3.0-beta05" } -compose-activity = { module = "androidx.activity:activity-compose", version = "1.9.0" } +compose-activity = { module = "androidx.activity:activity-compose", version = "1.9.1" } compose-navigation = { module = "androidx.navigation:navigation-compose", version = "2.7.7" } compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } From e33c2d61f347efaa9f82cb3a855fe4d6a03380cc Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 19:30:05 +0200 Subject: [PATCH 36/48] Add multiplatform horizontal year calendar sample --- .../kizitonwose/calendar/compose/Calendar.kt | 3 + .../compose/yearcalendar/YearCalendarState.kt | 2 + .../calendar/core/ExperimentalCalendarApi.kt | 9 + .../sample/src/commonMain/kotlin/App.kt | 6 +- .../src/commonMain/kotlin/Example10Page.kt | 283 ++++++++++++++++++ .../sample/src/commonMain/kotlin/Format.kt | 6 +- .../sample/src/commonMain/kotlin/ListPage.kt | 10 + .../sample/src/commonMain/kotlin/Utils.kt | 80 +++++ .../sample/src/iosMain/kotlin/Format.ios.kt | 9 +- .../sample/src/jvmMain/kotlin/Format.jvm.kt | 5 +- .../src/wasmJsMain/kotlin/Format.wasmJs.kt | 4 +- .../calendar/sample/compose/Example10Page.kt | 8 +- .../calendar/sample/shared/Utils.kt | 4 + 13 files changed, 413 insertions(+), 16 deletions(-) create mode 100644 compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt create mode 100644 compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt index 2e88b9a1..3ee5378e 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt @@ -27,6 +27,7 @@ import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.CalendarMonth import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.WeekDay import kotlinx.datetime.DayOfWeek @@ -356,6 +357,7 @@ public fun HeatMapCalendar( * customisations are rendered. */ @Composable +@ExperimentalCalendarApi public fun HorizontalYearCalendar( modifier: Modifier = Modifier, state: YearCalendarState = rememberYearCalendarState(), @@ -457,6 +459,7 @@ public fun HorizontalYearCalendar( * The actual container content is provided in the block and must be called after your desired * customisations are rendered. */ +@ExperimentalCalendarApi @Composable public fun VerticalYearCalendar( modifier: Modifier = Modifier, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt index fefc904f..cd4b6908 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -18,6 +18,7 @@ import androidx.compose.runtime.setValue import com.kizitonwose.calendar.compose.CalendarInfo import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.core.CalendarYear +import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.OutDateStyle import com.kizitonwose.calendar.core.Year import com.kizitonwose.calendar.core.firstDayOfWeekFromLocale @@ -39,6 +40,7 @@ import kotlinx.datetime.DayOfWeek * @param outDateStyle the initial value for [YearCalendarState.outDateStyle] */ @Composable +@ExperimentalCalendarApi public fun rememberYearCalendarState( startYear: Year = Year.now(), endYear: Year = startYear, diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt new file mode 100644 index 00000000..df02b05a --- /dev/null +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/ExperimentalCalendarApi.kt @@ -0,0 +1,9 @@ +package com.kizitonwose.calendar.core + +@RequiresOptIn( + message = "This calendar API is experimental and is " + + "likely to change or to be removed in the future.", + level = RequiresOptIn.Level.ERROR, +) +@Retention(AnnotationRetention.BINARY) +public annotation class ExperimentalCalendarApi diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt index 7eba1a77..8e401f55 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt @@ -41,8 +41,7 @@ import kotlin.math.roundToInt fun App() { MaterialTheme(MaterialTheme.colorScheme.copy(primary = Colors.primary)) { BoxWithConstraints( - Modifier.fillMaxSize(), - contentAlignment = Alignment.TopCenter, + modifier = Modifier.fillMaxSize(), ) { if (maxWidth >= 600.dp) { val widthPx = maxWidth.value.roundToInt() @@ -170,8 +169,9 @@ private fun AppNavHost( horizontallyAnimatedComposable(Page.Example5.name) { Example5Page { navController.popBackStack() } } horizontallyAnimatedComposable(Page.Example6.name) { Example6Page() } horizontallyAnimatedComposable(Page.Example7.name) { Example7Page() } - horizontallyAnimatedComposable(Page.Example8.name) { Example8Page() { navController.popBackStack() } } + horizontallyAnimatedComposable(Page.Example8.name) { Example8Page { navController.popBackStack() } } horizontallyAnimatedComposable(Page.Example9.name) { Example9Page() } + horizontallyAnimatedComposable(Page.Example10.name) { Example10Page() } } } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt new file mode 100644 index 00000000..7644010e --- /dev/null +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt @@ -0,0 +1,283 @@ +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider +import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kizitonwose.calendar.compose.HorizontalYearCalendar +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.ExperimentalCalendarApi +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.minusYears +import com.kizitonwose.calendar.core.plusYears +import com.kizitonwose.calendar.core.yearsUntil +import kotlinx.coroutines.launch +import org.jetbrains.compose.ui.tooling.preview.Preview +import kotlin.math.abs + +@OptIn(ExperimentalCalendarApi::class) +@Composable +fun Example10Page(adjacentYears: Int = 50) { + val currentYear = remember { Year.now() } + val startYear = remember { currentYear.minusYears(adjacentYears) } + val endYear = remember { currentYear.plusYears(adjacentYears) } + val selections = remember { mutableStateListOf() } + val daysOfWeek = remember { daysOfWeek() } + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + ) { + val isTablet = maxWidth >= 600.dp + val isPortrait = maxHeight > maxWidth + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + ) { + val scope = rememberCoroutineScope() + val state = rememberYearCalendarState( + startYear = startYear, + endYear = endYear, + firstVisibleYear = currentYear, + firstDayOfWeek = daysOfWeek.first(), + ) + val visibleYear = rememberFirstVisibleYearAfterScroll(state).year + val headerState = rememberLazyListState() + LaunchedEffect(visibleYear) { + val index = startYear.yearsUntil(visibleYear) + headerState.animateScrollAndCenterItem(index) + } + YearHeader( + startYear = startYear, + endYear = endYear, + visibleYear = visibleYear, + headerState = headerState, + isTablet = isTablet, + ) click@{ targetYear -> + if (targetYear == visibleYear) return@click + scope.launch { + if (abs(visibleYear.yearsUntil(targetYear)) <= 8) { + state.animateScrollToYear(targetYear) + } else { + val nearbyYear = if (targetYear > visibleYear) { + targetYear.minusYears(5) + } else { + targetYear.plusYears(5) + } + state.scrollToYear(nearbyYear) + state.animateScrollToYear(targetYear) + } + } + } + HorizontalYearCalendar( + modifier = Modifier + .fillMaxSize() + .testTag("Calendar"), + state = state, + columns = if (isPortrait) { + 3 + } else { + if (isTablet) 4 else 6 + }, + dayContent = { day -> + Day( + day = day, + isSelected = selections.contains(day), + isTablet = isTablet, + ) { clicked -> + if (selections.contains(clicked)) { + selections.remove(clicked) + } else { + selections.add(clicked) + } + } + }, + contentHeightMode = YearContentHeightMode.Fill, + monthHorizontalSpacing = if (isTablet) { + if (isPortrait) 52.dp else 92.dp + } else { + 10.dp + }, + monthVerticalSpacing = if (isTablet) 20.dp else 4.dp, + yearBodyContentPadding = if (isTablet) { + PaddingValues(horizontal = if (isPortrait) 52.dp else 92.dp, vertical = 20.dp) + } else { + PaddingValues(all = 10.dp) + }, + monthHeader = { + MonthHeader( + calendarMonth = it, + isTablet = isTablet, + ) + }, + ) + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun YearHeader( + startYear: Year, + endYear: Year, + visibleYear: Year, + headerState: LazyListState, + isTablet: Boolean, + modifier: Modifier = Modifier, + onClick: (Year) -> Unit, +) { + LazyRow( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .background(headerBackground), + state = headerState, + flingBehavior = rememberSnapFlingBehavior(SnapLayoutInfoProvider(headerState, SnapPositionInLayout.CenterToCenter)), + contentPadding = PaddingValues(horizontal = if (isTablet) 40.dp else 10.dp), + ) { + items(count = startYear.yearsUntil(endYear)) { index -> + val year = startYear.plusYears(index) + val isSelected = visibleYear == year + Box( + modifier = Modifier + .then( + if (isSelected) { + Modifier.background( + color = simpleTextBackground(isSelected = true), + shape = RoundedCornerShape(4.dp), + ) + } else { + Modifier + }, + ) + .clickable(onClick = { onClick(year) }) + .padding( + horizontal = if (isTablet) 60.dp else 28.dp, + vertical = if (isTablet) 10.dp else 6.dp, + ), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = year.value.toString(), + textAlign = TextAlign.Center, + fontSize = if (isTablet) 24.sp else 18.sp, + color = simpleTextColor(isSelected), + fontWeight = if (isSelected) FontWeight.Black else FontWeight.Light, + ) + } + } + } +} + +@Composable +private fun MonthHeader( + calendarMonth: CalendarMonth, + isTablet: Boolean, + modifier: Modifier = Modifier, +) { + val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(if (isTablet) 12.dp else 8.dp), + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = calendarMonth.yearMonth.month.displayText(short = false), + fontSize = if (isTablet) 16.sp else 12.sp, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium, + ) + Row(modifier = Modifier.fillMaxWidth()) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = if (isTablet) 11.sp else 9.sp, + text = dayOfWeek.displayText(uppercase = true, narrow = true), + fontWeight = FontWeight.SemiBold, + ) + } + } + } +} + +@Composable +private fun Day( + day: CalendarDay, + isSelected: Boolean, + isTablet: Boolean, + onClick: (CalendarDay) -> Unit, +) { + Box( + modifier = Modifier + .aspectRatio(1f) // This is important for square-sizing! + .testTag("MonthDay") + .padding(if (isTablet) 2.dp else 0.dp) + .clip(CircleShape) + .background(simpleTextBackground(isSelected)) + // Disable clicks on inDates/outDates + .clickable( + enabled = day.position == DayPosition.MonthDate, + showRipple = !isSelected, + onClick = { onClick(day) }, + ), + contentAlignment = Alignment.Center, + ) { + if (day.position == DayPosition.MonthDate) { + Text( + text = day.date.dayOfMonth.toString(), + fontSize = if (isTablet) 11.sp else 9.sp, + color = simpleTextColor(isSelected), + ) + } + } +} + +@Preview +@Composable +private fun Example10Preview() { + Example10Page() +} + +private val headerBackground = Color(0xFFF1F1F1) +private fun simpleTextColor(isSelected: Boolean) = + if (isSelected) darkColorScheme().onSurface else lightColorScheme().onSurface + +private fun simpleTextBackground(isSelected: Boolean) = + if (isSelected) darkColorScheme().surface else lightColorScheme().surface diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Format.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Format.kt index 405ebb9d..d7ccf16b 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Format.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Format.kt @@ -14,15 +14,15 @@ fun Month.displayText(short: Boolean = true): String { return getDisplayName(short, enLocale) } -fun DayOfWeek.displayText(uppercase: Boolean = false): String { - return getShortDisplayName(enLocale).let { value -> +fun DayOfWeek.displayText(uppercase: Boolean = false, narrow: Boolean = false): String { + return getDisplayName(narrow, enLocale).let { value -> if (uppercase) value.toUpperCase(enLocale) else value } } expect fun Month.getDisplayName(short: Boolean, locale: Locale): String -expect fun DayOfWeek.getShortDisplayName(locale: Locale): String +expect fun DayOfWeek.getDisplayName(narrow: Boolean = false, locale: Locale): String private val enLocale = Locale("en-US") diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt b/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt index 2664ea8a..f226b615 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/ListPage.kt @@ -67,6 +67,16 @@ enum class Page(val title: String, val subtitle: String, val showToolBar: Boolea subtitle = "Month and week calendar toggle with animations.", showToolBar = true, ), + Example10( + title = "Example 10", + subtitle = "Horizontal year calendar - Year header and paged scrolling. Best suited for large screens.", + showToolBar = true, + ), + Example11( + title = "Example 11", + subtitle = "Vertical year calendar - Hidden past months with continuous scroll. Best suited for large screens.", + showToolBar = true, + ), } @Composable diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt index 02ced119..ea08723c 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt @@ -1,11 +1,13 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -26,7 +28,10 @@ import androidx.compose.ui.unit.dp import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarLayoutInfo +import com.kizitonwose.calendar.compose.yearcalendar.YearCalendarState import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.CalendarYear import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.YearMonth import com.kizitonwose.calendar.core.minus @@ -145,6 +150,42 @@ fun rememberFirstMostVisibleMonth( return visibleMonth.value } +/** + * Find the first year on the calendar visible up to the given [viewportPercent] size. + * + * @see [rememberFirstVisibleYearAfterScroll] + */ +@Composable +fun rememberFirstMostVisibleYear( + state: YearCalendarState, + viewportPercent: Float = 50f, +): CalendarYear { + val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleYear) } + LaunchedEffect(state) { + snapshotFlow { state.layoutInfo.firstMostVisibleYear(viewportPercent) } + .filterNotNull() + .collect { month -> visibleMonth.value = month } + } + return visibleMonth.value +} + +/** + * Returns the first visible year in a paged calendar **after** scrolling stops. + * + * @see [rememberFirstMostVisibleYear] + */ +@Composable +fun rememberFirstVisibleYearAfterScroll(state: YearCalendarState): CalendarYear { + val visibleYear = remember(state) { mutableStateOf(state.firstVisibleYear) } + LaunchedEffect(state) { + snapshotFlow { state.isScrollInProgress } + .filter { scrolling -> !scrolling } + .collect { visibleYear.value = state.firstVisibleYear } + } + return visibleYear.value +} + + private val CalendarLayoutInfo.completelyVisibleMonths: List get() { val visibleItemsInfo = this.visibleMonthsInfo.toMutableList() @@ -179,5 +220,44 @@ private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50 } } +private fun YearCalendarLayoutInfo.firstMostVisibleYear(viewportPercent: Float = 50f): CalendarYear? { + return if (visibleYearsInfo.isEmpty()) { + null + } else { + val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f + visibleYearsInfo.firstOrNull { itemInfo -> + if (itemInfo.offset < 0) { + itemInfo.offset + itemInfo.size >= viewportSize + } else { + itemInfo.size - itemInfo.offset >= viewportSize + } + }?.year + } +} + +suspend fun LazyListState.animateScrollAndCenterItem(index: Int) { + suspend fun animateScrollIfVisible(): Boolean { + val layoutInfo = layoutInfo + val containerSize = layoutInfo.viewportSize.width - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding + val target = layoutInfo.visibleItemsInfo.firstOrNull { it.index == index } ?: return false + val targetOffset = containerSize / 2f - target.size / 2f + animateScrollBy(target.offset - targetOffset) + return true + } + if (!animateScrollIfVisible()) { + val visibleItemsInfo = layoutInfo.visibleItemsInfo + val currentIndex = visibleItemsInfo.getOrNull(visibleItemsInfo.size / 2)?.index ?: -1 + scrollToItem( + if (index > currentIndex) { + (index - visibleItemsInfo.size + 1) + } else { + index + }.coerceIn(0, layoutInfo.totalItemsCount), + ) + animateScrollIfVisible() + } +} + + val YearMonth.next: YearMonth get() = this.plus(1, DateTimeUnit.MONTH) val YearMonth.previous: YearMonth get() = this.minus(1, DateTimeUnit.MONTH) diff --git a/compose-multiplatform/sample/src/iosMain/kotlin/Format.ios.kt b/compose-multiplatform/sample/src/iosMain/kotlin/Format.ios.kt index 5291f3fa..438ab831 100644 --- a/compose-multiplatform/sample/src/iosMain/kotlin/Format.ios.kt +++ b/compose-multiplatform/sample/src/iosMain/kotlin/Format.ios.kt @@ -11,10 +11,15 @@ actual fun Month.getDisplayName(short: Boolean, locale: Locale): String = it.monthSymbols[Month.entries.indexOf(this)] as String } -actual fun DayOfWeek.getShortDisplayName(locale: Locale): String = +actual fun DayOfWeek.getDisplayName(narrow: Boolean, locale: Locale): String = NSCalendar.currentCalendar.let { it.setLocale(NSLocale(locale.toLanguageTag())) - it.shortWeekdaySymbols[sundayBasedWeek.indexOf(this)] as String + val values = if (narrow) { + it.veryShortWeekdaySymbols + } else { + it.shortWeekdaySymbols + } + values[sundayBasedWeek.indexOf(this)] as String } private val sundayBasedWeek = daysOfWeek(firstDayOfWeek = DayOfWeek.SUNDAY) diff --git a/compose-multiplatform/sample/src/jvmMain/kotlin/Format.jvm.kt b/compose-multiplatform/sample/src/jvmMain/kotlin/Format.jvm.kt index 13b90094..3f2ffbeb 100644 --- a/compose-multiplatform/sample/src/jvmMain/kotlin/Format.jvm.kt +++ b/compose-multiplatform/sample/src/jvmMain/kotlin/Format.jvm.kt @@ -9,6 +9,7 @@ actual fun Month.getDisplayName(short: Boolean, locale: Locale): String { return getDisplayName(style, JavaLocale.forLanguageTag(locale.toLanguageTag())) } -actual fun DayOfWeek.getShortDisplayName(locale: Locale): String { - return getDisplayName(TextStyle.SHORT, JavaLocale.forLanguageTag(locale.toLanguageTag())) +actual fun DayOfWeek.getDisplayName(narrow: Boolean, locale: Locale): String { + val style = if (narrow) TextStyle.NARROW else TextStyle.SHORT + return getDisplayName(style, JavaLocale.forLanguageTag(locale.toLanguageTag())) } diff --git a/compose-multiplatform/sample/src/wasmJsMain/kotlin/Format.wasmJs.kt b/compose-multiplatform/sample/src/wasmJsMain/kotlin/Format.wasmJs.kt index 7be940b6..b20385d0 100644 --- a/compose-multiplatform/sample/src/wasmJsMain/kotlin/Format.wasmJs.kt +++ b/compose-multiplatform/sample/src/wasmJsMain/kotlin/Format.wasmJs.kt @@ -9,8 +9,8 @@ actual fun Month.getDisplayName(short: Boolean, locale: Locale): String { return if (short) name.take(3) else name } -actual fun DayOfWeek.getShortDisplayName(locale: Locale): String { - return name.toLowerCase(enLocale).capitalize(enLocale).take(3) +actual fun DayOfWeek.getDisplayName(narrow: Boolean, locale: Locale): String { + return name.toLowerCase(enLocale).capitalize(enLocale).take(if (narrow) 1 else 3) } private val enLocale = Locale("en-US") diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index 831463b2..a2a968ce 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -48,9 +48,9 @@ import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.core.ExperimentalCalendarApi import com.kizitonwose.calendar.core.daysOfWeek import com.kizitonwose.calendar.sample.shared.displayText +import com.kizitonwose.calendar.sample.shared.yearsUntil import kotlinx.coroutines.launch import java.time.Year -import java.time.temporal.ChronoUnit import kotlin.math.abs @OptIn(ExperimentalCalendarApi::class) @@ -79,7 +79,7 @@ fun Example10Page(adjacentYears: Long = 50) { val visibleYear = rememberFirstVisibleYearAfterScroll(state).year val headerState = rememberLazyListState() LaunchedEffect(visibleYear) { - val index = ChronoUnit.YEARS.between(startYear, visibleYear).toInt() + val index = startYear.yearsUntil(visibleYear).toInt() headerState.animateScrollAndCenterItem(index) } YearHeader( @@ -91,7 +91,7 @@ fun Example10Page(adjacentYears: Long = 50) { ) click@{ targetYear -> if (targetYear == visibleYear) return@click scope.launch { - if (abs(ChronoUnit.YEARS.between(visibleYear, targetYear)) <= 8) { + if (abs(visibleYear.yearsUntil(targetYear)) <= 8) { state.animateScrollToYear(targetYear) } else { val nearbyYear = if (targetYear > visibleYear) { @@ -168,7 +168,7 @@ private fun YearHeader( flingBehavior = rememberSnapFlingBehavior(lazyListState = headerState, SnapPosition.Center), contentPadding = PaddingValues(horizontal = if (isTablet) 40.dp else 10.dp), ) { - items(count = ChronoUnit.YEARS.between(startYear, endYear).toInt()) { index -> + items(count = startYear.yearsUntil(endYear).toInt()) { index -> val year = startYear.plusYears(index.toLong()) val isSelected = visibleYear == year Box( diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt index 645c5304..a54d4e8e 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/shared/Utils.kt @@ -7,8 +7,10 @@ import com.kizitonwose.calendar.core.Week import com.kizitonwose.calendar.core.yearMonth import java.time.DayOfWeek import java.time.Month +import java.time.Year import java.time.YearMonth import java.time.format.TextStyle +import java.time.temporal.ChronoUnit import java.util.Locale fun YearMonth.displayText(short: Boolean = false): String { @@ -51,3 +53,5 @@ fun getWeekPageTitle(week: Week): String { } } } + +fun Year.yearsUntil(other: Year) = ChronoUnit.YEARS.between(this, other) From 10bae105f67befee9d43bcdb3d602245786a10c3 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 19:37:16 +0200 Subject: [PATCH 37/48] Add multiplatform vertical year calendar sample --- .../sample/src/commonMain/kotlin/App.kt | 1 + .../src/commonMain/kotlin/Example11Page.kt | 188 ++++++++++++++++++ .../calendar/sample/compose/Example11Page.kt | 4 +- 3 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 compose-multiplatform/sample/src/commonMain/kotlin/Example11Page.kt diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt index 8e401f55..fbb56ba3 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt @@ -172,6 +172,7 @@ private fun AppNavHost( horizontallyAnimatedComposable(Page.Example8.name) { Example8Page { navController.popBackStack() } } horizontallyAnimatedComposable(Page.Example9.name) { Example9Page() } horizontallyAnimatedComposable(Page.Example10.name) { Example10Page() } + horizontallyAnimatedComposable(Page.Example11.name) { Example11Page() } } } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example11Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example11Page.kt new file mode 100644 index 00000000..a526d1d7 --- /dev/null +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example11Page.kt @@ -0,0 +1,188 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.kizitonwose.calendar.compose.VerticalYearCalendar +import com.kizitonwose.calendar.compose.yearcalendar.YearContentHeightMode +import com.kizitonwose.calendar.compose.yearcalendar.rememberYearCalendarState +import com.kizitonwose.calendar.core.CalendarDay +import com.kizitonwose.calendar.core.CalendarMonth +import com.kizitonwose.calendar.core.DayPosition +import com.kizitonwose.calendar.core.ExperimentalCalendarApi +import com.kizitonwose.calendar.core.Year +import com.kizitonwose.calendar.core.YearMonth +import com.kizitonwose.calendar.core.daysOfWeek +import com.kizitonwose.calendar.core.plusYears +import org.jetbrains.compose.ui.tooling.preview.Preview + +@OptIn(ExperimentalCalendarApi::class) +@Composable +fun Example11Page(adjacentYears: Int = 50) { + val currentMonth = remember { YearMonth.now() } + val currentYear = remember { Year(currentMonth.year) } + val endYear = remember { currentYear.plusYears(adjacentYears) } + val selections = remember { mutableStateListOf() } + val daysOfWeek = remember { daysOfWeek() } + BoxWithConstraints( + modifier = Modifier.fillMaxSize(), + ) { + val isTablet = maxWidth >= 600.dp + Column( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + ) { + val state = rememberYearCalendarState( + startYear = currentYear, + endYear = endYear, + firstVisibleYear = currentYear, + firstDayOfWeek = daysOfWeek.first(), + ) + VerticalYearCalendar( + modifier = Modifier + .fillMaxSize() + .testTag("Calendar"), + state = state, + dayContent = { day -> + Day( + day = day, + isSelected = selections.contains(day), + isTablet = isTablet, + ) { clicked -> + if (selections.contains(clicked)) { + selections.remove(clicked) + } else { + selections.add(clicked) + } + } + }, + calendarScrollPaged = false, + contentHeightMode = YearContentHeightMode.Wrap, + monthVerticalSpacing = 20.dp, + monthHorizontalSpacing = if (isTablet) 52.dp else 10.dp, + contentPadding = PaddingValues(horizontal = if (isTablet) 52.dp else 10.dp), + isMonthVisible = { + it.yearMonth >= currentMonth + }, + yearHeader = { + YearHeader(it.year) + }, + monthHeader = { + MonthHeader(it) + }, + ) + } + } +} + +@Composable +private fun MonthHeader(calendarMonth: CalendarMonth) { + val daysOfWeek = calendarMonth.weekDays.first().map { it.date.dayOfWeek } + Column( + modifier = Modifier + .wrapContentHeight() + .padding(top = 6.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp), + text = calendarMonth.yearMonth.month.displayText(short = false), + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + Row(modifier = Modifier.fillMaxWidth()) { + for (dayOfWeek in daysOfWeek) { + Text( + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + fontSize = 11.sp, + text = dayOfWeek.displayText(uppercase = true, narrow = true), + fontWeight = FontWeight.Medium, + ) + } + } + } +} + +@Composable +private fun YearHeader(year: Year) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, bottom = 20.dp) + .testTag("MonthHeader"), + fontSize = 52.sp, + text = year.toString(), + fontWeight = FontWeight.Medium, + ) + HorizontalDivider() + } +} + +@Composable +private fun Day( + day: CalendarDay, + isSelected: Boolean, + isTablet: Boolean, + onClick: (CalendarDay) -> Unit, +) { + Box( + modifier = Modifier + .aspectRatio(1f) // This is important for square-sizing! + .testTag("MonthDay") + .padding(if (isTablet) 2.dp else 0.dp) + .clip(CircleShape) + .background(color = if (isSelected) Colors.example1Selection else Color.Transparent) + // Disable clicks on inDates/outDates + .clickable( + enabled = day.position == DayPosition.MonthDate, + showRipple = !isSelected, + onClick = { onClick(day) }, + ), + contentAlignment = Alignment.Center, + ) { + if (day.position == DayPosition.MonthDate) { + Text( + text = day.date.dayOfMonth.toString(), + fontSize = if (isTablet) 10.sp else 9.sp, + color = if (isSelected) Color.White else Color.Unspecified, + ) + } + } +} + +@Preview +@Composable +private fun Example11Preview() { + Example11Page() +} diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt index 7cb5e9c6..6cdea3b5 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example11Page.kt @@ -46,10 +46,10 @@ import java.time.YearMonth @OptIn(ExperimentalCalendarApi::class) @Composable -fun Example11Page(adjacentMonths: Long = 50) { +fun Example11Page(adjacentYears: Long = 50) { val currentMonth = remember { YearMonth.now() } val currentYear = remember { Year.of(currentMonth.year) } - val endYear = remember { currentYear.plusYears(adjacentMonths) } + val endYear = remember { currentYear.plusYears(adjacentYears) } val selections = remember { mutableStateListOf() } val daysOfWeek = remember { daysOfWeek() } val config = LocalConfiguration.current From d2d80c9062eaf2dce4d7447050c15e9507acba0a Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 19:53:42 +0200 Subject: [PATCH 38/48] Material3 color fixes --- .../sample/src/commonMain/kotlin/Example10Page.kt | 7 +++---- .../sample/src/commonMain/kotlin/Example3Page.kt | 4 ++-- .../sample/src/commonMain/kotlin/Example7Page.kt | 4 ++-- .../sample/src/commonMain/kotlin/Example8Page.kt | 3 +-- .../kizitonwose/calendar/sample/compose/Example10Page.kt | 6 ++---- .../kizitonwose/calendar/sample/compose/Example3Page.kt | 3 +-- .../kizitonwose/calendar/sample/compose/Example7Page.kt | 3 +-- .../kizitonwose/calendar/sample/compose/Example8Page.kt | 3 +-- 8 files changed, 13 insertions(+), 20 deletions(-) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt index 7644010e..d6fd6746 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt @@ -1,3 +1,4 @@ + import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider @@ -20,8 +21,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf @@ -277,7 +276,7 @@ private fun Example10Preview() { private val headerBackground = Color(0xFFF1F1F1) private fun simpleTextColor(isSelected: Boolean) = - if (isSelected) darkColorScheme().onSurface else lightColorScheme().onSurface + if (isSelected) Color.White else Color.Black private fun simpleTextBackground(isSelected: Boolean) = - if (isSelected) darkColorScheme().surface else lightColorScheme().surface + if (isSelected) Color.Black else Color.White diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt index 7e652980..70f1441a 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example3Page.kt @@ -1,3 +1,4 @@ + import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -24,7 +25,6 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -95,7 +95,7 @@ fun Example3Page(close: () -> Unit = {}) { } // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + CompositionLocalProvider(LocalContentColor provides Color.White) { SimpleCalendarTitle( modifier = Modifier .background(toolbarColor) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt index eb45eca1..d6cb43ba 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example7Page.kt @@ -1,3 +1,4 @@ + import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -12,7 +13,6 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -52,7 +52,7 @@ fun Example7Page() { firstVisibleWeekDate = currentDate, ) // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + CompositionLocalProvider(LocalContentColor provides Color.White) { WeekCalendar( modifier = Modifier.padding(vertical = 4.dp), state = state, diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt index f33c6eb7..414765b5 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example8Page.kt @@ -18,7 +18,6 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -70,7 +69,7 @@ fun Example8Page(close: () -> Unit = {}) { .padding(top = 20.dp), ) { // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + CompositionLocalProvider(LocalContentColor provides Color.White) { var selectedIndex by remember { mutableIntStateOf(0) } PageOptions(selectedIndex, close = close) { selectedIndex = it } val state = rememberCalendarState( diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index a2a968ce..d0dae98d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -19,8 +19,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf @@ -277,7 +275,7 @@ private fun Example10Preview() { private val headerBackground = Color(0xFFF1F1F1) private fun simpleTextColor(isSelected: Boolean) = - if (isSelected) darkColorScheme().onSurface else lightColorScheme().onSurface + if (isSelected) Color.White else Color.Black private fun simpleTextBackground(isSelected: Boolean) = - if (isSelected) darkColorScheme().surface else lightColorScheme().surface + if (isSelected) Color.Black else Color.White diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt index 9d878bb0..27fcccb1 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt @@ -26,7 +26,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalRippleConfiguration import androidx.compose.material3.RippleConfiguration import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -107,7 +106,7 @@ fun Example3Page() { } // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + CompositionLocalProvider(LocalContentColor provides Color.White) { SimpleCalendarTitle( modifier = Modifier .background(toolbarColor) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt index fea485c5..b2c53d66 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example7Page.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -54,7 +53,7 @@ fun Example7Page() { firstVisibleWeekDate = currentDate, ) // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + CompositionLocalProvider(LocalContentColor provides Color.White) { WeekCalendar( modifier = Modifier.padding(vertical = 4.dp), state = state, diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt index 085a9b5d..880c70d8 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt @@ -15,7 +15,6 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text -import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -83,7 +82,7 @@ fun Example8Page(horizontal: Boolean? = null) { val coroutineScope = rememberCoroutineScope() val visibleMonth = rememberFirstVisibleMonthAfterScroll(state) // Draw light content on dark background. - CompositionLocalProvider(LocalContentColor provides darkColorScheme().onSurface) { + CompositionLocalProvider(LocalContentColor provides Color.White) { SimpleCalendarTitle( modifier = Modifier.padding(vertical = 10.dp, horizontal = 8.dp), currentMonth = visibleMonth.yearMonth, From 9e04fcf3e30f26532ae149e507f08e7476bc4bee Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Thu, 25 Jul 2024 20:09:20 +0200 Subject: [PATCH 39/48] Reorder annotations --- .../kotlin/com/kizitonwose/calendar/compose/Calendar.kt | 2 +- .../calendar/compose/yearcalendar/YearCalendarState.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt index 3ee5378e..7da01948 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/Calendar.kt @@ -356,8 +356,8 @@ public fun HeatMapCalendar( * The actual container content is provided in the block and must be called after your desired * customisations are rendered. */ -@Composable @ExperimentalCalendarApi +@Composable public fun HorizontalYearCalendar( modifier: Modifier = Modifier, state: YearCalendarState = rememberYearCalendarState(), diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt index cd4b6908..0dd3b554 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState.kt @@ -39,8 +39,8 @@ import kotlinx.datetime.DayOfWeek * @param firstVisibleYear the initial value for [YearCalendarState.firstVisibleYear] * @param outDateStyle the initial value for [YearCalendarState.outDateStyle] */ -@Composable @ExperimentalCalendarApi +@Composable public fun rememberYearCalendarState( startYear: Year = Year.now(), endYear: Year = startYear, From bf410a8b4ace27343d073164df6c807f4672e2d6 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 26 Jul 2024 11:39:46 +0200 Subject: [PATCH 40/48] Improve colors in the sample app --- .../sample/src/commonMain/kotlin/App.kt | 2 +- .../sample/src/commonMain/kotlin/Colors.kt | 3 +- .../src/commonMain/kotlin/Example1Page.kt | 2 +- .../kotlin/Example2PageHighlight.kt | 8 ++-- .../src/commonMain/kotlin/Example9Page.kt | 2 +- .../sample/src/commonMain/kotlin/Theme.kt | 41 ++++++++++++++++++ .../sample/compose/CalendarComposeActivity.kt | 5 +-- .../calendar/sample/compose/Theme.kt | 43 +++++++++++++++++++ .../res/layout/calendar_view_activity.xml | 3 +- sample/src/main/res/layout/home_activity.xml | 3 +- 10 files changed, 97 insertions(+), 15 deletions(-) create mode 100644 compose-multiplatform/sample/src/commonMain/kotlin/Theme.kt create mode 100644 sample/src/main/java/com/kizitonwose/calendar/sample/compose/Theme.kt diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt index fbb56ba3..abcc1230 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/App.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/App.kt @@ -39,7 +39,7 @@ import kotlin.math.roundToInt @Composable @Preview fun App() { - MaterialTheme(MaterialTheme.colorScheme.copy(primary = Colors.primary)) { + MaterialTheme(SampleColorScheme) { BoxWithConstraints( modifier = Modifier.fillMaxSize(), ) { diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt index bd7a0d7b..f17b67d2 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Colors.kt @@ -6,9 +6,8 @@ object Colors { val example1BgLight = Color(0xFF433254) val example1BgSecondary = Color(0xFF51356E) val example1WhiteLight = Color(0x4DFFFFFF) - val inactiveText = Color(0xFFBEBEBE) - val example4Gray = Color(0xFF474747) val example4GrayPast = Color(0xFFBEBEBE) + val example4Gray = Color(0xFF474747) val example5PageBgColor = Color(0xFF0E0E0E) val example5ItemViewBgColor = Color(0xFF1B1B1B) val example5ToolbarColor = Color(0xFF282828) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt index f24ce49f..eca27eed 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example1Page.kt @@ -125,7 +125,7 @@ private fun Day(day: CalendarDay, isSelected: Boolean, onClick: (CalendarDay) -> val textColor = when (day.position) { // Color.Unspecified will use the default text color from the current theme DayPosition.MonthDate -> if (isSelected) Color.White else Color.Unspecified - DayPosition.InDate, DayPosition.OutDate -> Colors.inactiveText + DayPosition.InDate, DayPosition.OutDate -> Colors.example4GrayPast } Text( text = day.date.dayOfMonth.toString(), diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt index 82c1d7cf..59c40f0b 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt @@ -54,7 +54,7 @@ fun Modifier.backgroundHighlight( DayPosition.MonthDate -> { when { day.date < today -> { - textColor(Colors.inactiveText) + textColor(Colors.example4GrayPast) this } @@ -98,7 +98,7 @@ fun Modifier.backgroundHighlight( .border( width = 1.dp, shape = CircleShape, - color = Colors.inactiveText, + color = Colors.example4GrayPast, ) } @@ -152,7 +152,7 @@ fun Modifier.backgroundHighlightLegacy( DayPosition.MonthDate -> { when { day.date < today -> { - textColor(Colors.inactiveText) + textColor(Colors.example4GrayPast) this } @@ -195,7 +195,7 @@ fun Modifier.backgroundHighlightLegacy( .border( width = 1.dp, shape = CircleShape, - color = Colors.inactiveText, + color = Colors.example4GrayPast, ) } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt index 1a842fa0..362f93f7 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example9Page.kt @@ -284,7 +284,7 @@ object Example9PageSharedComponents { val textColor = when { isSelected -> Color.White isSelectable -> Color.Unspecified - else -> Colors.inactiveText + else -> Colors.example4GrayPast } Text( text = day.dayOfMonth.toString(), diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Theme.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Theme.kt new file mode 100644 index 00000000..ae8d73b6 --- /dev/null +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Theme.kt @@ -0,0 +1,41 @@ +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +val SampleColorScheme = lightColorScheme( + primary = Color(0xFF3F51B5), + onPrimary = Color.White, + primaryContainer = Color(0xFF3F51B5), + onPrimaryContainer = Color.White, + inversePrimary = Color.Black, + secondary = Color(0xFF3F51B5), + onSecondary = Color.White, + secondaryContainer = Color(0xFF3F51B5), + onSecondaryContainer = Color.White, + tertiary = Color(0xFF3F51B5), + onTertiary = Color.White, + tertiaryContainer = Color(0xFF3F51B5), + onTertiaryContainer = Color.White, + background = Color.White, + onBackground = Color.Black, + surface = Color.White, + onSurface = Color.Black, + surfaceVariant = Color.White, + onSurfaceVariant = Color.Black, + surfaceTint = Color.White, + inverseSurface = Color(0xFF121212), + inverseOnSurface = Color.White, + error = Color(0xFFB00020), + onError = Color.White, + errorContainer = Color(0xFFB00020), + onErrorContainer = Color.White, + outline = Color(0xFFAFAFAF), + outlineVariant = Color(0xFFCCCCCC), + scrim = Color.Black, + surfaceBright = Color.White, + surfaceContainer = Color.White, + surfaceContainerHigh = Color.White, + surfaceContainerHighest = Color.White, + surfaceContainerLow = Color.White, + surfaceContainerLowest = Color.White, + surfaceDim = Color.White, +) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt index 76b79005..324fa467 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/CalendarComposeActivity.kt @@ -21,12 +21,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import com.kizitonwose.calendar.sample.R import com.kizitonwose.calendar.sample.shared.dateRangeDisplayText import kotlinx.coroutines.launch @@ -34,7 +32,6 @@ class CalendarComposeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - val primaryColor = colorResource(id = R.color.colorPrimary) var toolBarTitle by remember { mutableStateOf("") } var toolBarVisible by remember { mutableStateOf(true) } val snackbarHostState = remember { SnackbarHostState() } @@ -47,7 +44,7 @@ class CalendarComposeActivity : AppCompatActivity() { toolBarVisible = page.showToolBar } } - MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy(primary = primaryColor)) { + MaterialTheme(colorScheme = SampleColorScheme) { Scaffold( topBar = { if (toolBarVisible) { diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Theme.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Theme.kt new file mode 100644 index 00000000..5fce7d6e --- /dev/null +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Theme.kt @@ -0,0 +1,43 @@ +package com.kizitonwose.calendar.sample.compose + +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +val SampleColorScheme = lightColorScheme( + primary = Color(0xFF3F51B5), + onPrimary = Color.White, + primaryContainer = Color(0xFF3F51B5), + onPrimaryContainer = Color.White, + inversePrimary = Color.Black, + secondary = Color(0xFF3F51B5), + onSecondary = Color.White, + secondaryContainer = Color(0xFF3F51B5), + onSecondaryContainer = Color.White, + tertiary = Color(0xFF3F51B5), + onTertiary = Color.White, + tertiaryContainer = Color(0xFF3F51B5), + onTertiaryContainer = Color.White, + background = Color.White, + onBackground = Color.Black, + surface = Color.White, + onSurface = Color.Black, + surfaceVariant = Color.White, + onSurfaceVariant = Color.Black, + surfaceTint = Color.White, + inverseSurface = Color(0xFF121212), + inverseOnSurface = Color.White, + error = Color(0xFFB00020), + onError = Color.White, + errorContainer = Color(0xFFB00020), + onErrorContainer = Color.White, + outline = Color(0xFFAFAFAF), + outlineVariant = Color(0xFFCCCCCC), + scrim = Color.Black, + surfaceBright = Color.White, + surfaceContainer = Color.White, + surfaceContainerHigh = Color.White, + surfaceContainerHighest = Color.White, + surfaceContainerLow = Color.White, + surfaceContainerLowest = Color.White, + surfaceDim = Color.White, +) diff --git a/sample/src/main/res/layout/calendar_view_activity.xml b/sample/src/main/res/layout/calendar_view_activity.xml index d001e1a0..39b066f3 100644 --- a/sample/src/main/res/layout/calendar_view_activity.xml +++ b/sample/src/main/res/layout/calendar_view_activity.xml @@ -5,6 +5,7 @@ android:id="@+id/homeRootLayout" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/white" tools:context=".view.CalendarViewActivity"> - \ No newline at end of file + diff --git a/sample/src/main/res/layout/home_activity.xml b/sample/src/main/res/layout/home_activity.xml index c3b8d749..8197c798 100644 --- a/sample/src/main/res/layout/home_activity.xml +++ b/sample/src/main/res/layout/home_activity.xml @@ -6,6 +6,7 @@ android:layout_width="match_parent" android:orientation="vertical" android:layout_height="match_parent" + android:background="@color/white" tools:context=".HomeActivity"> - \ No newline at end of file + From 7f22bc54268889879e8c8be14a9ad02c8c163d03 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 26 Jul 2024 12:00:13 +0200 Subject: [PATCH 41/48] Update compose docs --- docs/Compose.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Compose.md b/docs/Compose.md index ae77a96e..f740684c 100644 --- a/docs/Compose.md +++ b/docs/Compose.md @@ -39,9 +39,9 @@ Add the library to your project [here](https://github.com/kizitonwose/Calendar#s ## Compose Multiplatform Information -The APIs for the compose libraries for Android and Multiplatform projects have been designed such that you can copy examples across both projects and they would work without code changes as the classes have the same names and package declarations. The only difference in some cases would be that the code for the Android calendar library needs to import classes such as `LocalDate` and `YearMonth` from the `java.time` package while the multiplaform calendar library needs to import such classes from the [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) library. +The APIs for the compose libraries for Android and Multiplatform projects have been designed such that you can copy examples across both projects and they would work without code changes as the classes have the same names and package declarations. The only difference in some cases would be that the code for the Android calendar library needs to import classes such as `LocalDate`, `YearMonth` and `Year` from the `java.time` package while the multiplaform calendar library needs to import such classes from the [kotlinx-datetime](https://github.com/Kotlin/kotlinx-datetime) library. -Note that the `YearMonth` class does not yet exist in the `kotlinx-datetime` library, therefore the multiplatfrom calendar library includes a minimal `YearMonth` class implementation to bridge this gap until the class [is hopefully added](https://github.com/Kotlin/kotlinx-datetime/issues/168) to the `kotlinx-datetime` library. +Note that the `YearMonth` and `Year` classes do not yet exist in the `kotlinx-datetime` library, therefore the multiplaform calendar library includes minimal `YearMonth` and `Year` class implementations to bridge this gap until these classes [are hopefully added](https://github.com/Kotlin/kotlinx-datetime/issues/168) to the `kotlinx-datetime` library. ## Compose UI version compatibility From 9792f896e00fd188d4ee221fc42f82900484fccc Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 26 Jul 2024 13:28:20 +0200 Subject: [PATCH 42/48] Run all tests --- .github/workflows/check.yml | 2 +- build.gradle.kts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fd8ba9f2..a02fb9ec 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -52,7 +52,7 @@ jobs: with: gradle-home-cache-cleanup: true - name: Unit tests - run: ./gradlew testDebugUnitTest + run: ./gradlew test instrumentation-tests: name: Instrumentation tests diff --git a/build.gradle.kts b/build.gradle.kts index 624bb8de..a7a1fbd3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,14 +23,14 @@ plugins { allprojects { apply(plugin = rootProject.libs.plugins.kotlinter.get().pluginId) - plugins.withType().configureEach { + plugins.withType { extensions.configure { if ("sample" !in project.name) { explicitApi() } } } - tasks.withType().configureEach { + tasks.withType { useJUnitPlatform() // https://docs.gradle.org/8.8/userguide/performance.html#execute_tests_in_parallel maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) From 81da2993ecb34c100d23e28bc40d36d46c09019f Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 26 Jul 2024 15:42:07 +0200 Subject: [PATCH 43/48] Update docs --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4fb89cce..e415d391 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ A highly customizable calendar library for Android and Compose Multiplatform, ba ## Features -- [x] Single, multiple or range selection - Total flexibility to implement the date selection +- [x] Week, month, or year modes - Show a week-based calendar, or the typical month calendar, or a year-based calendar. +- [x] Single, multiple, or range selection - Total flexibility to implement the date selection whichever way you like. -- [x] Week or month mode - Show a week-based calendar, or the typical month calendar. - [x] Disable desired dates - Prevent selection of some dates by disabling them. - [x] Boundary dates - Limit the calendar date range. - [x] Custom date view/composable - Make your day cells look however you want, with any @@ -28,8 +28,8 @@ A highly customizable calendar library for Android and Compose Multiplatform, ba - [x] Horizontal or vertical scrolling calendar. - [x] HeatMap calendar - Suitable for showing how data changes over time, like GitHub's contribution chart. -- [x] Month/Week headers and footers - Add headers/footers of any kind on each month/week. -- [x] Easily scroll to any date/week/month on the calendar via swipe actions or programmatically. +- [x] Year/Month/Week headers and footers - Add headers/footers of any kind on each year/month/week. +- [x] Easily scroll to any date/week/month/year on the calendar via swipe actions or programmatically. - [x] Use all RecyclerView/LazyRow/LazyColumn customizations since the calendar extends from RecyclerView for the view system and uses LazyRow/LazyColumn for compose. - [x] Design your calendar [however you want.](https://github.com/kizitonwose/Calendar/issues/1) The @@ -124,12 +124,12 @@ For the compose calendar library, ensure that you are using the library version | Compose UI | Android Calendar Library | Multiplatform Calendar Library | |:----------:|:------------------------:|:------------------------------:| -| 1.2.x | 2.0.x | - | -| 1.3.x | 2.1.x - 2.2.x | - | -| 1.4.x | 2.3.x | - | -| 1.5.x | 2.4.x | - | -| 1.6.x | 2.5.x | - | -| 1.7.x | 2.6.x | 2.6.x | +| 1.2.x | 2.0.x | - | +| 1.3.x | 2.1.x - 2.2.x | - | +| 1.4.x | 2.3.x | - | +| 1.5.x | 2.4.x | - | +| 1.6.x | 2.5.x | 2.5.x | +| 1.7.x | 2.6.x | 2.6.x | ## Usage From 2d6f6ec85a944b41c7e7917331b4cbc63d74c618 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 26 Jul 2024 17:24:57 +0200 Subject: [PATCH 44/48] Api dump --- .../library/api/android/library.api | 207 +++++++++++++++--- .../library/api/desktop/library.api | 207 +++++++++++++++--- compose/api/compose.api | 80 +++++++ core/api/core.api | 16 ++ data/api/data.api | 9 +- 5 files changed, 465 insertions(+), 54 deletions(-) diff --git a/compose-multiplatform/library/api/android/library.api b/compose-multiplatform/library/api/android/library.api index cef6c873..8ad819cc 100644 --- a/compose-multiplatform/library/api/android/library.api +++ b/compose-multiplatform/library/api/android/library.api @@ -12,7 +12,9 @@ public final class com/kizitonwose/calendar/compose/CalendarItemInfo : androidx/ public final class com/kizitonwose/calendar/compose/CalendarKt { public static final fun HeatMapCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState;Lcom/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeekHeaderPosition;ZLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V public static final fun HorizontalCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/CalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lcom/kizitonwose/calendar/compose/ContentHeightMode;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun HorizontalYearCalendar-Y3kUhCI (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState;IZZZLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFLcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;IIII)V public static final fun VerticalCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/CalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lcom/kizitonwose/calendar/compose/ContentHeightMode;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun VerticalYearCalendar-Y3kUhCI (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState;IZZZLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFLcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;IIII)V public static final fun WeekCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V } @@ -179,6 +181,84 @@ public final class com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarSta public static final fun rememberWeekCalendarState (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Ljava/time/DayOfWeek;Landroidx/compose/runtime/Composer;II)Lcom/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState; } +public final class com/kizitonwose/calendar/compose/yearcalendar/ComposableSingletons$YearCalendarMonthsKt { + public static final field INSTANCE Lcom/kizitonwose/calendar/compose/yearcalendar/ComposableSingletons$YearCalendarMonthsKt; + public static field lambda-1 Lkotlin/jvm/functions/Function5; + public static field lambda-2 Lkotlin/jvm/functions/Function5; + public static field lambda-3 Lkotlin/jvm/functions/Function5; + public static field lambda-4 Lkotlin/jvm/functions/Function5; + public fun ()V + public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function5; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarItemInfo : androidx/compose/foundation/lazy/LazyListItemInfo { + public static final field $stable I + public fun (Landroidx/compose/foundation/lazy/LazyListItemInfo;Lcom/kizitonwose/calendar/core/CalendarYear;)V + public fun getContentType ()Ljava/lang/Object; + public fun getIndex ()I + public fun getKey ()Ljava/lang/Object; + public fun getOffset ()I + public fun getSize ()I + public final fun getYear ()Lcom/kizitonwose/calendar/core/CalendarYear; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo : androidx/compose/foundation/lazy/LazyListLayoutInfo { + public static final field $stable I + public fun (Landroidx/compose/foundation/lazy/LazyListLayoutInfo;Lkotlin/jvm/functions/Function1;)V + public fun getAfterContentPadding ()I + public fun getBeforeContentPadding ()I + public fun getMainAxisItemSpacing ()I + public fun getOrientation ()Landroidx/compose/foundation/gestures/Orientation; + public fun getReverseLayout ()Z + public fun getTotalItemsCount ()I + public fun getViewportEndOffset ()I + public fun getViewportSize-YbymL2g ()J + public fun getViewportStartOffset ()I + public fun getVisibleItemsInfo ()Ljava/util/List; + public final fun getVisibleYearsInfo ()Ljava/util/List; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState : androidx/compose/foundation/gestures/ScrollableState { + public static final field $stable I + public static final field Companion Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState$Companion; + public final fun animateScrollToYear (Lcom/kizitonwose/calendar/core/Year;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun dispatchRawDelta (F)F + public final fun getEndYear ()Lcom/kizitonwose/calendar/core/Year; + public final fun getFirstDayOfWeek ()Ljava/time/DayOfWeek; + public final fun getFirstVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear; + public final fun getInteractionSource ()Landroidx/compose/foundation/interaction/InteractionSource; + public final fun getLastVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear; + public final fun getLayoutInfo ()Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo; + public final fun getOutDateStyle ()Lcom/kizitonwose/calendar/core/OutDateStyle; + public final fun getStartYear ()Lcom/kizitonwose/calendar/core/Year; + public fun isScrollInProgress ()Z + public fun scroll (Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun scrollToYear (Lcom/kizitonwose/calendar/core/Year;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setEndYear (Lcom/kizitonwose/calendar/core/Year;)V + public final fun setFirstDayOfWeek (Ljava/time/DayOfWeek;)V + public final fun setOutDateStyle (Lcom/kizitonwose/calendar/core/OutDateStyle;)V + public final fun setStartYear (Lcom/kizitonwose/calendar/core/Year;)V +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState$Companion { +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarStateKt { + public static final fun rememberYearCalendarState (Lcom/kizitonwose/calendar/core/Year;Lcom/kizitonwose/calendar/core/Year;Lcom/kizitonwose/calendar/core/Year;Ljava/time/DayOfWeek;Lcom/kizitonwose/calendar/core/OutDateStyle;Landroidx/compose/runtime/Composer;II)Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode : java/lang/Enum { + public static final field Fill Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static final field Stretch Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static final field Wrap Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static fun values ()[Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; +} + public final class com/kizitonwose/calendar/core/CalendarDay { public static final field $stable I public fun (Lkotlinx/datetime/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;)V @@ -206,8 +286,24 @@ public final class com/kizitonwose/calendar/core/CalendarMonth { public fun toString ()Ljava/lang/String; } +public final class com/kizitonwose/calendar/core/CalendarYear { + public static final field $stable I + public fun (Lcom/kizitonwose/calendar/core/Year;Ljava/util/List;)V + public final fun component1 ()Lcom/kizitonwose/calendar/core/Year; + public final fun component2 ()Ljava/util/List; + public final fun copy (Lcom/kizitonwose/calendar/core/Year;Ljava/util/List;)Lcom/kizitonwose/calendar/core/CalendarYear; + public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/CalendarYear;Lcom/kizitonwose/calendar/core/Year;Ljava/util/List;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/CalendarYear; + public fun equals (Ljava/lang/Object;)Z + public final fun getMonths ()Ljava/util/List; + public final fun getYear ()Lcom/kizitonwose/calendar/core/Year; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/kizitonwose/calendar/core/ConvertersKt { + public static final fun toJavaYear (Lcom/kizitonwose/calendar/core/Year;)Ljava/time/Year; public static final fun toJavaYearMonth (Lcom/kizitonwose/calendar/core/YearMonth;)Ljava/time/YearMonth; + public static final fun toKotlinYear (Ljava/time/Year;)Lcom/kizitonwose/calendar/core/Year; public static final fun toKotlinYearMonth (Ljava/time/YearMonth;)Lcom/kizitonwose/calendar/core/YearMonth; } @@ -220,12 +316,21 @@ public final class com/kizitonwose/calendar/core/DayPosition : java/lang/Enum { public static fun values ()[Lcom/kizitonwose/calendar/core/DayPosition; } +public abstract interface annotation class com/kizitonwose/calendar/core/ExperimentalCalendarApi : java/lang/annotation/Annotation { +} + public final class com/kizitonwose/calendar/core/ExtensionsKt { public static final fun daysOfWeek (Ljava/time/DayOfWeek;)Ljava/util/List; public static synthetic fun daysOfWeek$default (Ljava/time/DayOfWeek;ILjava/lang/Object;)Ljava/util/List; public static final fun getYearMonth (Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun minusDays (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun minusMonths (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun minusYears (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; public static final fun now (Lkotlinx/datetime/LocalDate$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;)Lkotlinx/datetime/LocalDate; public static synthetic fun now$default (Lkotlinx/datetime/LocalDate$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;ILjava/lang/Object;)Lkotlinx/datetime/LocalDate; + public static final fun plusDays (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun plusMonths (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun plusYears (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; } public final class com/kizitonwose/calendar/core/Extensions_jvmKt { @@ -275,7 +380,43 @@ public final class com/kizitonwose/calendar/core/WeekDayPosition : java/lang/Enu public static fun values ()[Lcom/kizitonwose/calendar/core/WeekDayPosition; } -public final class com/kizitonwose/calendar/core/YearMonth : java/io/Serializable, java/lang/Comparable { +public final class com/kizitonwose/calendar/core/Year : java/lang/Comparable { + public static final field $stable I + public static final field Companion Lcom/kizitonwose/calendar/core/Year$Companion; + public fun (I)V + public fun compareTo (Lcom/kizitonwose/calendar/core/Year;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun component1 ()I + public final fun copy (I)Lcom/kizitonwose/calendar/core/Year; + public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/Year;IILjava/lang/Object;)Lcom/kizitonwose/calendar/core/Year; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/kizitonwose/calendar/core/Year$Companion { + public final fun isLeap (I)Z + public final fun now (Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;)Lcom/kizitonwose/calendar/core/Year; + public static synthetic fun now$default (Lcom/kizitonwose/calendar/core/Year$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/Year; + public final fun parseIso8601 (Ljava/lang/String;)Lcom/kizitonwose/calendar/core/Year; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/kizitonwose/calendar/core/YearKt { + public static final fun atDay (Lcom/kizitonwose/calendar/core/Year;I)Lkotlinx/datetime/LocalDate; + public static final fun atMonth (Lcom/kizitonwose/calendar/core/Year;I)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun atMonth (Lcom/kizitonwose/calendar/core/Year;Ljava/time/Month;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun atMonthDay (Lcom/kizitonwose/calendar/core/Year;II)Lkotlinx/datetime/LocalDate; + public static final fun atMonthDay (Lcom/kizitonwose/calendar/core/Year;Ljava/time/Month;I)Lkotlinx/datetime/LocalDate; + public static final fun isLeap (Lcom/kizitonwose/calendar/core/Year;)Z + public static final fun length (Lcom/kizitonwose/calendar/core/Year;)I + public static final fun minusYears (Lcom/kizitonwose/calendar/core/Year;I)Lcom/kizitonwose/calendar/core/Year; + public static final fun plusYears (Lcom/kizitonwose/calendar/core/Year;I)Lcom/kizitonwose/calendar/core/Year; + public static final fun yearsUntil (Lcom/kizitonwose/calendar/core/Year;Lcom/kizitonwose/calendar/core/Year;)I +} + +public final class com/kizitonwose/calendar/core/YearMonth : java/lang/Comparable { public static final field $stable I public static final field Companion Lcom/kizitonwose/calendar/core/YearMonth$Companion; public fun (II)V @@ -288,6 +429,7 @@ public final class com/kizitonwose/calendar/core/YearMonth : java/io/Serializabl public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/YearMonth;ILjava/time/Month;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/YearMonth; public fun equals (Ljava/lang/Object;)Z public final fun getMonth ()Ljava/time/Month; + public final fun getMonthNumber ()I public final fun getYear ()I public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -296,48 +438,61 @@ public final class com/kizitonwose/calendar/core/YearMonth : java/io/Serializabl public final class com/kizitonwose/calendar/core/YearMonth$Companion { public final fun now (Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;)Lcom/kizitonwose/calendar/core/YearMonth; public static synthetic fun now$default (Lcom/kizitonwose/calendar/core/YearMonth$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/YearMonth; + public final fun parseIso8601 (Ljava/lang/String;)Lcom/kizitonwose/calendar/core/YearMonth; + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class com/kizitonwose/calendar/core/YearMonthKt { public static final fun atDay (Lcom/kizitonwose/calendar/core/YearMonth;I)Lkotlinx/datetime/LocalDate; public static final fun atEndOfMonth (Lcom/kizitonwose/calendar/core/YearMonth;)Lkotlinx/datetime/LocalDate; public static final fun atStartOfMonth (Lcom/kizitonwose/calendar/core/YearMonth;)Lkotlinx/datetime/LocalDate; - public static final fun getNext (Lcom/kizitonwose/calendar/core/YearMonth;)Lcom/kizitonwose/calendar/core/YearMonth; - public static final fun getPrevious (Lcom/kizitonwose/calendar/core/YearMonth;)Lcom/kizitonwose/calendar/core/YearMonth; public static final fun lengthOfMonth (Lcom/kizitonwose/calendar/core/YearMonth;)I public static final fun minus (Lcom/kizitonwose/calendar/core/YearMonth;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun minusMonths (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun minusYears (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; public static final fun monthsUntil (Lcom/kizitonwose/calendar/core/YearMonth;Lcom/kizitonwose/calendar/core/YearMonth;)I public static final fun plus (Lcom/kizitonwose/calendar/core/YearMonth;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun plusMonths (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun plusYears (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; } -public final class com/kizitonwose/calendar/data/WeekData { +public final class com/kizitonwose/calendar/core/serializers/YearComponentSerializer : kotlinx/serialization/KSerializer { public static final field $stable I - public final fun copy (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/data/WeekData; - public static synthetic fun copy$default (Lcom/kizitonwose/calendar/data/WeekData;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;ILjava/lang/Object;)Lcom/kizitonwose/calendar/data/WeekData; - public fun equals (Ljava/lang/Object;)Z - public final fun getWeek ()Lcom/kizitonwose/calendar/core/Week; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearComponentSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/Year; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/Year;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } -public final class com/kizitonwose/calendar/data/WeekDataKt { - public static final fun getWeekCalendarAdjustedRange (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Ljava/time/DayOfWeek;)Lcom/kizitonwose/calendar/data/WeekDateRange; - public static final fun getWeekCalendarData (Lkotlinx/datetime/LocalDate;ILkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/data/WeekData; - public static final fun getWeekIndex (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I - public static final fun getWeekIndicesCount (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I +public final class com/kizitonwose/calendar/core/serializers/YearIso8601Serializer : kotlinx/serialization/KSerializer { + public static final field $stable I + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearIso8601Serializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/Year; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/Year;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } -public final class com/kizitonwose/calendar/data/WeekDateRange { +public final class com/kizitonwose/calendar/core/serializers/YearMonthComponentSerializer : kotlinx/serialization/KSerializer { public static final field $stable I - public fun (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)V - public final fun component1 ()Lkotlinx/datetime/LocalDate; - public final fun component2 ()Lkotlinx/datetime/LocalDate; - public final fun copy (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/data/WeekDateRange; - public static synthetic fun copy$default (Lcom/kizitonwose/calendar/data/WeekDateRange;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;ILjava/lang/Object;)Lcom/kizitonwose/calendar/data/WeekDateRange; - public fun equals (Ljava/lang/Object;)Z - public final fun getEndDateAdjusted ()Lkotlinx/datetime/LocalDate; - public final fun getStartDateAdjusted ()Lkotlinx/datetime/LocalDate; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearMonthComponentSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/YearMonth; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/YearMonth;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + +public final class com/kizitonwose/calendar/core/serializers/YearMonthIso8601Serializer : kotlinx/serialization/KSerializer { + public static final field $stable I + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearMonthIso8601Serializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/YearMonth; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/YearMonth;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } diff --git a/compose-multiplatform/library/api/desktop/library.api b/compose-multiplatform/library/api/desktop/library.api index 362a1914..873fe1df 100644 --- a/compose-multiplatform/library/api/desktop/library.api +++ b/compose-multiplatform/library/api/desktop/library.api @@ -12,7 +12,9 @@ public final class com/kizitonwose/calendar/compose/CalendarItemInfo : androidx/ public final class com/kizitonwose/calendar/compose/CalendarKt { public static final fun HeatMapCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState;Lcom/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeekHeaderPosition;ZLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V public static final fun HorizontalCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/CalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lcom/kizitonwose/calendar/compose/ContentHeightMode;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun HorizontalYearCalendar-Y3kUhCI (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState;IZZZLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFLcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;IIII)V public static final fun VerticalCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/CalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lcom/kizitonwose/calendar/compose/ContentHeightMode;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun VerticalYearCalendar-Y3kUhCI (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState;IZZZLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFLcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;IIII)V public static final fun WeekCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V } @@ -179,6 +181,84 @@ public final class com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarSta public static final fun rememberWeekCalendarState (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Ljava/time/DayOfWeek;Landroidx/compose/runtime/Composer;II)Lcom/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState; } +public final class com/kizitonwose/calendar/compose/yearcalendar/ComposableSingletons$YearCalendarMonthsKt { + public static final field INSTANCE Lcom/kizitonwose/calendar/compose/yearcalendar/ComposableSingletons$YearCalendarMonthsKt; + public static field lambda-1 Lkotlin/jvm/functions/Function5; + public static field lambda-2 Lkotlin/jvm/functions/Function5; + public static field lambda-3 Lkotlin/jvm/functions/Function5; + public static field lambda-4 Lkotlin/jvm/functions/Function5; + public fun ()V + public final fun getLambda-1$library ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-2$library ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-3$library ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-4$library ()Lkotlin/jvm/functions/Function5; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarItemInfo : androidx/compose/foundation/lazy/LazyListItemInfo { + public static final field $stable I + public fun (Landroidx/compose/foundation/lazy/LazyListItemInfo;Lcom/kizitonwose/calendar/core/CalendarYear;)V + public fun getContentType ()Ljava/lang/Object; + public fun getIndex ()I + public fun getKey ()Ljava/lang/Object; + public fun getOffset ()I + public fun getSize ()I + public final fun getYear ()Lcom/kizitonwose/calendar/core/CalendarYear; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo : androidx/compose/foundation/lazy/LazyListLayoutInfo { + public static final field $stable I + public fun (Landroidx/compose/foundation/lazy/LazyListLayoutInfo;Lkotlin/jvm/functions/Function1;)V + public fun getAfterContentPadding ()I + public fun getBeforeContentPadding ()I + public fun getMainAxisItemSpacing ()I + public fun getOrientation ()Landroidx/compose/foundation/gestures/Orientation; + public fun getReverseLayout ()Z + public fun getTotalItemsCount ()I + public fun getViewportEndOffset ()I + public fun getViewportSize-YbymL2g ()J + public fun getViewportStartOffset ()I + public fun getVisibleItemsInfo ()Ljava/util/List; + public final fun getVisibleYearsInfo ()Ljava/util/List; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState : androidx/compose/foundation/gestures/ScrollableState { + public static final field $stable I + public static final field Companion Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState$Companion; + public final fun animateScrollToYear (Lcom/kizitonwose/calendar/core/Year;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun dispatchRawDelta (F)F + public final fun getEndYear ()Lcom/kizitonwose/calendar/core/Year; + public final fun getFirstDayOfWeek ()Ljava/time/DayOfWeek; + public final fun getFirstVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear; + public final fun getInteractionSource ()Landroidx/compose/foundation/interaction/InteractionSource; + public final fun getLastVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear; + public final fun getLayoutInfo ()Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo; + public final fun getOutDateStyle ()Lcom/kizitonwose/calendar/core/OutDateStyle; + public final fun getStartYear ()Lcom/kizitonwose/calendar/core/Year; + public fun isScrollInProgress ()Z + public fun scroll (Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun scrollToYear (Lcom/kizitonwose/calendar/core/Year;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setEndYear (Lcom/kizitonwose/calendar/core/Year;)V + public final fun setFirstDayOfWeek (Ljava/time/DayOfWeek;)V + public final fun setOutDateStyle (Lcom/kizitonwose/calendar/core/OutDateStyle;)V + public final fun setStartYear (Lcom/kizitonwose/calendar/core/Year;)V +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState$Companion { +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarStateKt { + public static final fun rememberYearCalendarState (Lcom/kizitonwose/calendar/core/Year;Lcom/kizitonwose/calendar/core/Year;Lcom/kizitonwose/calendar/core/Year;Ljava/time/DayOfWeek;Lcom/kizitonwose/calendar/core/OutDateStyle;Landroidx/compose/runtime/Composer;II)Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode : java/lang/Enum { + public static final field Fill Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static final field Stretch Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static final field Wrap Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static fun values ()[Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; +} + public final class com/kizitonwose/calendar/core/CalendarDay { public static final field $stable I public fun (Lkotlinx/datetime/LocalDate;Lcom/kizitonwose/calendar/core/DayPosition;)V @@ -206,8 +286,24 @@ public final class com/kizitonwose/calendar/core/CalendarMonth { public fun toString ()Ljava/lang/String; } +public final class com/kizitonwose/calendar/core/CalendarYear { + public static final field $stable I + public fun (Lcom/kizitonwose/calendar/core/Year;Ljava/util/List;)V + public final fun component1 ()Lcom/kizitonwose/calendar/core/Year; + public final fun component2 ()Ljava/util/List; + public final fun copy (Lcom/kizitonwose/calendar/core/Year;Ljava/util/List;)Lcom/kizitonwose/calendar/core/CalendarYear; + public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/CalendarYear;Lcom/kizitonwose/calendar/core/Year;Ljava/util/List;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/CalendarYear; + public fun equals (Ljava/lang/Object;)Z + public final fun getMonths ()Ljava/util/List; + public final fun getYear ()Lcom/kizitonwose/calendar/core/Year; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/kizitonwose/calendar/core/ConvertersKt { + public static final fun toJavaYear (Lcom/kizitonwose/calendar/core/Year;)Ljava/time/Year; public static final fun toJavaYearMonth (Lcom/kizitonwose/calendar/core/YearMonth;)Ljava/time/YearMonth; + public static final fun toKotlinYear (Ljava/time/Year;)Lcom/kizitonwose/calendar/core/Year; public static final fun toKotlinYearMonth (Ljava/time/YearMonth;)Lcom/kizitonwose/calendar/core/YearMonth; } @@ -220,12 +316,21 @@ public final class com/kizitonwose/calendar/core/DayPosition : java/lang/Enum { public static fun values ()[Lcom/kizitonwose/calendar/core/DayPosition; } +public abstract interface annotation class com/kizitonwose/calendar/core/ExperimentalCalendarApi : java/lang/annotation/Annotation { +} + public final class com/kizitonwose/calendar/core/ExtensionsKt { public static final fun daysOfWeek (Ljava/time/DayOfWeek;)Ljava/util/List; public static synthetic fun daysOfWeek$default (Ljava/time/DayOfWeek;ILjava/lang/Object;)Ljava/util/List; public static final fun getYearMonth (Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun minusDays (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun minusMonths (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun minusYears (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; public static final fun now (Lkotlinx/datetime/LocalDate$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;)Lkotlinx/datetime/LocalDate; public static synthetic fun now$default (Lkotlinx/datetime/LocalDate$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;ILjava/lang/Object;)Lkotlinx/datetime/LocalDate; + public static final fun plusDays (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun plusMonths (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; + public static final fun plusYears (Lkotlinx/datetime/LocalDate;I)Lkotlinx/datetime/LocalDate; } public final class com/kizitonwose/calendar/core/Extensions_jvmKt { @@ -275,7 +380,43 @@ public final class com/kizitonwose/calendar/core/WeekDayPosition : java/lang/Enu public static fun values ()[Lcom/kizitonwose/calendar/core/WeekDayPosition; } -public final class com/kizitonwose/calendar/core/YearMonth : java/io/Serializable, java/lang/Comparable { +public final class com/kizitonwose/calendar/core/Year : java/lang/Comparable { + public static final field $stable I + public static final field Companion Lcom/kizitonwose/calendar/core/Year$Companion; + public fun (I)V + public fun compareTo (Lcom/kizitonwose/calendar/core/Year;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun component1 ()I + public final fun copy (I)Lcom/kizitonwose/calendar/core/Year; + public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/Year;IILjava/lang/Object;)Lcom/kizitonwose/calendar/core/Year; + public fun equals (Ljava/lang/Object;)Z + public final fun getValue ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/kizitonwose/calendar/core/Year$Companion { + public final fun isLeap (I)Z + public final fun now (Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;)Lcom/kizitonwose/calendar/core/Year; + public static synthetic fun now$default (Lcom/kizitonwose/calendar/core/Year$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/Year; + public final fun parseIso8601 (Ljava/lang/String;)Lcom/kizitonwose/calendar/core/Year; + public final fun serializer ()Lkotlinx/serialization/KSerializer; +} + +public final class com/kizitonwose/calendar/core/YearKt { + public static final fun atDay (Lcom/kizitonwose/calendar/core/Year;I)Lkotlinx/datetime/LocalDate; + public static final fun atMonth (Lcom/kizitonwose/calendar/core/Year;I)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun atMonth (Lcom/kizitonwose/calendar/core/Year;Ljava/time/Month;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun atMonthDay (Lcom/kizitonwose/calendar/core/Year;II)Lkotlinx/datetime/LocalDate; + public static final fun atMonthDay (Lcom/kizitonwose/calendar/core/Year;Ljava/time/Month;I)Lkotlinx/datetime/LocalDate; + public static final fun isLeap (Lcom/kizitonwose/calendar/core/Year;)Z + public static final fun length (Lcom/kizitonwose/calendar/core/Year;)I + public static final fun minusYears (Lcom/kizitonwose/calendar/core/Year;I)Lcom/kizitonwose/calendar/core/Year; + public static final fun plusYears (Lcom/kizitonwose/calendar/core/Year;I)Lcom/kizitonwose/calendar/core/Year; + public static final fun yearsUntil (Lcom/kizitonwose/calendar/core/Year;Lcom/kizitonwose/calendar/core/Year;)I +} + +public final class com/kizitonwose/calendar/core/YearMonth : java/lang/Comparable { public static final field $stable I public static final field Companion Lcom/kizitonwose/calendar/core/YearMonth$Companion; public fun (II)V @@ -288,6 +429,7 @@ public final class com/kizitonwose/calendar/core/YearMonth : java/io/Serializabl public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/YearMonth;ILjava/time/Month;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/YearMonth; public fun equals (Ljava/lang/Object;)Z public final fun getMonth ()Ljava/time/Month; + public final fun getMonthNumber ()I public final fun getYear ()I public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -296,48 +438,61 @@ public final class com/kizitonwose/calendar/core/YearMonth : java/io/Serializabl public final class com/kizitonwose/calendar/core/YearMonth$Companion { public final fun now (Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;)Lcom/kizitonwose/calendar/core/YearMonth; public static synthetic fun now$default (Lcom/kizitonwose/calendar/core/YearMonth$Companion;Lkotlinx/datetime/Clock;Lkotlinx/datetime/TimeZone;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/YearMonth; + public final fun parseIso8601 (Ljava/lang/String;)Lcom/kizitonwose/calendar/core/YearMonth; + public final fun serializer ()Lkotlinx/serialization/KSerializer; } public final class com/kizitonwose/calendar/core/YearMonthKt { public static final fun atDay (Lcom/kizitonwose/calendar/core/YearMonth;I)Lkotlinx/datetime/LocalDate; public static final fun atEndOfMonth (Lcom/kizitonwose/calendar/core/YearMonth;)Lkotlinx/datetime/LocalDate; public static final fun atStartOfMonth (Lcom/kizitonwose/calendar/core/YearMonth;)Lkotlinx/datetime/LocalDate; - public static final fun getNext (Lcom/kizitonwose/calendar/core/YearMonth;)Lcom/kizitonwose/calendar/core/YearMonth; - public static final fun getPrevious (Lcom/kizitonwose/calendar/core/YearMonth;)Lcom/kizitonwose/calendar/core/YearMonth; public static final fun lengthOfMonth (Lcom/kizitonwose/calendar/core/YearMonth;)I public static final fun minus (Lcom/kizitonwose/calendar/core/YearMonth;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun minusMonths (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun minusYears (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; public static final fun monthsUntil (Lcom/kizitonwose/calendar/core/YearMonth;Lcom/kizitonwose/calendar/core/YearMonth;)I public static final fun plus (Lcom/kizitonwose/calendar/core/YearMonth;ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun plusMonths (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; + public static final fun plusYears (Lcom/kizitonwose/calendar/core/YearMonth;I)Lcom/kizitonwose/calendar/core/YearMonth; } -public final class com/kizitonwose/calendar/data/WeekData { +public final class com/kizitonwose/calendar/core/serializers/YearComponentSerializer : kotlinx/serialization/KSerializer { public static final field $stable I - public final fun copy (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/data/WeekData; - public static synthetic fun copy$default (Lcom/kizitonwose/calendar/data/WeekData;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;ILjava/lang/Object;)Lcom/kizitonwose/calendar/data/WeekData; - public fun equals (Ljava/lang/Object;)Z - public final fun getWeek ()Lcom/kizitonwose/calendar/core/Week; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearComponentSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/Year; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/Year;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } -public final class com/kizitonwose/calendar/data/WeekDataKt { - public static final fun getWeekCalendarAdjustedRange (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;Ljava/time/DayOfWeek;)Lcom/kizitonwose/calendar/data/WeekDateRange; - public static final fun getWeekCalendarData (Lkotlinx/datetime/LocalDate;ILkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/data/WeekData; - public static final fun getWeekIndex (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I - public static final fun getWeekIndicesCount (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)I +public final class com/kizitonwose/calendar/core/serializers/YearIso8601Serializer : kotlinx/serialization/KSerializer { + public static final field $stable I + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearIso8601Serializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/Year; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/Year;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } -public final class com/kizitonwose/calendar/data/WeekDateRange { +public final class com/kizitonwose/calendar/core/serializers/YearMonthComponentSerializer : kotlinx/serialization/KSerializer { public static final field $stable I - public fun (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)V - public final fun component1 ()Lkotlinx/datetime/LocalDate; - public final fun component2 ()Lkotlinx/datetime/LocalDate; - public final fun copy (Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;)Lcom/kizitonwose/calendar/data/WeekDateRange; - public static synthetic fun copy$default (Lcom/kizitonwose/calendar/data/WeekDateRange;Lkotlinx/datetime/LocalDate;Lkotlinx/datetime/LocalDate;ILjava/lang/Object;)Lcom/kizitonwose/calendar/data/WeekDateRange; - public fun equals (Ljava/lang/Object;)Z - public final fun getEndDateAdjusted ()Lkotlinx/datetime/LocalDate; - public final fun getStartDateAdjusted ()Lkotlinx/datetime/LocalDate; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearMonthComponentSerializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/YearMonth; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/YearMonth;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V +} + +public final class com/kizitonwose/calendar/core/serializers/YearMonthIso8601Serializer : kotlinx/serialization/KSerializer { + public static final field $stable I + public static final field INSTANCE Lcom/kizitonwose/calendar/core/serializers/YearMonthIso8601Serializer; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/kizitonwose/calendar/core/YearMonth; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/kizitonwose/calendar/core/YearMonth;)V + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V } diff --git a/compose/api/compose.api b/compose/api/compose.api index 81615668..4a4796d1 100644 --- a/compose/api/compose.api +++ b/compose/api/compose.api @@ -12,7 +12,9 @@ public final class com/kizitonwose/calendar/compose/CalendarItemInfo : androidx/ public final class com/kizitonwose/calendar/compose/CalendarKt { public static final fun HeatMapCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/heatmapcalendar/HeatMapCalendarState;Lcom/kizitonwose/calendar/compose/heatmapcalendar/HeatMapWeekHeaderPosition;ZLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V public static final fun HorizontalCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/CalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lcom/kizitonwose/calendar/compose/ContentHeightMode;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun HorizontalYearCalendar-Y3kUhCI (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState;IZZZLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFLcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;IIII)V public static final fun VerticalCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/CalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lcom/kizitonwose/calendar/compose/ContentHeightMode;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;III)V + public static final fun VerticalYearCalendar-Y3kUhCI (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState;IZZZLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/foundation/layout/PaddingValues;FFLcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function5;Landroidx/compose/runtime/Composer;IIII)V public static final fun WeekCalendar (Landroidx/compose/ui/Modifier;Lcom/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState;ZZZLandroidx/compose/foundation/layout/PaddingValues;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V } @@ -179,3 +181,81 @@ public final class com/kizitonwose/calendar/compose/weekcalendar/WeekCalendarSta public static final fun rememberWeekCalendarState (Ljava/time/LocalDate;Ljava/time/LocalDate;Ljava/time/LocalDate;Ljava/time/DayOfWeek;Landroidx/compose/runtime/Composer;II)Lcom/kizitonwose/calendar/compose/weekcalendar/WeekCalendarState; } +public final class com/kizitonwose/calendar/compose/yearcalendar/ComposableSingletons$YearCalendarMonthsKt { + public static final field INSTANCE Lcom/kizitonwose/calendar/compose/yearcalendar/ComposableSingletons$YearCalendarMonthsKt; + public static field lambda-1 Lkotlin/jvm/functions/Function5; + public static field lambda-2 Lkotlin/jvm/functions/Function5; + public static field lambda-3 Lkotlin/jvm/functions/Function5; + public static field lambda-4 Lkotlin/jvm/functions/Function5; + public fun ()V + public final fun getLambda-1$compose_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-2$compose_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-3$compose_release ()Lkotlin/jvm/functions/Function5; + public final fun getLambda-4$compose_release ()Lkotlin/jvm/functions/Function5; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarItemInfo : androidx/compose/foundation/lazy/LazyListItemInfo { + public static final field $stable I + public fun (Landroidx/compose/foundation/lazy/LazyListItemInfo;Lcom/kizitonwose/calendar/core/CalendarYear;)V + public fun getContentType ()Ljava/lang/Object; + public fun getIndex ()I + public fun getKey ()Ljava/lang/Object; + public fun getOffset ()I + public fun getSize ()I + public final fun getYear ()Lcom/kizitonwose/calendar/core/CalendarYear; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo : androidx/compose/foundation/lazy/LazyListLayoutInfo { + public static final field $stable I + public fun (Landroidx/compose/foundation/lazy/LazyListLayoutInfo;Lkotlin/jvm/functions/Function1;)V + public fun getAfterContentPadding ()I + public fun getBeforeContentPadding ()I + public fun getMainAxisItemSpacing ()I + public fun getOrientation ()Landroidx/compose/foundation/gestures/Orientation; + public fun getReverseLayout ()Z + public fun getTotalItemsCount ()I + public fun getViewportEndOffset ()I + public fun getViewportSize-YbymL2g ()J + public fun getViewportStartOffset ()I + public fun getVisibleItemsInfo ()Ljava/util/List; + public final fun getVisibleYearsInfo ()Ljava/util/List; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState : androidx/compose/foundation/gestures/ScrollableState { + public static final field $stable I + public static final field Companion Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState$Companion; + public final fun animateScrollToYear (Ljava/time/Year;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public fun dispatchRawDelta (F)F + public final fun getEndYear ()Ljava/time/Year; + public final fun getFirstDayOfWeek ()Ljava/time/DayOfWeek; + public final fun getFirstVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear; + public final fun getInteractionSource ()Landroidx/compose/foundation/interaction/InteractionSource; + public final fun getLastVisibleYear ()Lcom/kizitonwose/calendar/core/CalendarYear; + public final fun getLayoutInfo ()Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarLayoutInfo; + public final fun getOutDateStyle ()Lcom/kizitonwose/calendar/core/OutDateStyle; + public final fun getStartYear ()Ljava/time/Year; + public fun isScrollInProgress ()Z + public fun scroll (Landroidx/compose/foundation/MutatePriority;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun scrollToYear (Ljava/time/Year;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun setEndYear (Ljava/time/Year;)V + public final fun setFirstDayOfWeek (Ljava/time/DayOfWeek;)V + public final fun setOutDateStyle (Lcom/kizitonwose/calendar/core/OutDateStyle;)V + public final fun setStartYear (Ljava/time/Year;)V +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarState$Companion { +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearCalendarStateKt { + public static final fun rememberYearCalendarState (Ljava/time/Year;Ljava/time/Year;Ljava/time/Year;Ljava/time/DayOfWeek;Lcom/kizitonwose/calendar/core/OutDateStyle;Landroidx/compose/runtime/Composer;II)Lcom/kizitonwose/calendar/compose/yearcalendar/YearCalendarState; +} + +public final class com/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode : java/lang/Enum { + public static final field Fill Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static final field Stretch Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static final field Wrap Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; + public static fun values ()[Lcom/kizitonwose/calendar/compose/yearcalendar/YearContentHeightMode; +} + diff --git a/core/api/core.api b/core/api/core.api index dca5da5f..7aa710cb 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -24,6 +24,19 @@ public final class com/kizitonwose/calendar/core/CalendarMonth : java/io/Seriali public fun toString ()Ljava/lang/String; } +public final class com/kizitonwose/calendar/core/CalendarYear : java/io/Serializable { + public fun (Ljava/time/Year;Ljava/util/List;)V + public final fun component1 ()Ljava/time/Year; + public final fun component2 ()Ljava/util/List; + public final fun copy (Ljava/time/Year;Ljava/util/List;)Lcom/kizitonwose/calendar/core/CalendarYear; + public static synthetic fun copy$default (Lcom/kizitonwose/calendar/core/CalendarYear;Ljava/time/Year;Ljava/util/List;ILjava/lang/Object;)Lcom/kizitonwose/calendar/core/CalendarYear; + public fun equals (Ljava/lang/Object;)Z + public final fun getMonths ()Ljava/util/List; + public final fun getYear ()Ljava/time/Year; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/kizitonwose/calendar/core/DayPosition : java/lang/Enum { public static final field InDate Lcom/kizitonwose/calendar/core/DayPosition; public static final field MonthDate Lcom/kizitonwose/calendar/core/DayPosition; @@ -33,6 +46,9 @@ public final class com/kizitonwose/calendar/core/DayPosition : java/lang/Enum { public static fun values ()[Lcom/kizitonwose/calendar/core/DayPosition; } +public abstract interface annotation class com/kizitonwose/calendar/core/ExperimentalCalendarApi : java/lang/annotation/Annotation { +} + public final class com/kizitonwose/calendar/core/ExtensionsKt { public static final fun atStartOfMonth (Ljava/time/YearMonth;)Ljava/time/LocalDate; public static final fun daysOfWeek ()Ljava/util/List; diff --git a/data/api/data.api b/data/api/data.api index 8c53e103..b8777a33 100644 --- a/data/api/data.api +++ b/data/api/data.api @@ -44,8 +44,7 @@ public final class com/kizitonwose/calendar/data/MonthDataKt { } public final class com/kizitonwose/calendar/data/UtilsKt { - public static final fun checkDateRange (Ljava/time/LocalDate;Ljava/time/LocalDate;)V - public static final fun checkDateRange (Ljava/time/YearMonth;Ljava/time/YearMonth;)V + public static final fun checkRange (Ljava/lang/Comparable;Ljava/lang/Comparable;)V } public final class com/kizitonwose/calendar/data/WeekData { @@ -77,3 +76,9 @@ public final class com/kizitonwose/calendar/data/WeekDateRange { public fun toString ()Ljava/lang/String; } +public final class com/kizitonwose/calendar/data/YearDataKt { + public static final fun getCalendarYearData (Ljava/time/Year;ILjava/time/DayOfWeek;Lcom/kizitonwose/calendar/core/OutDateStyle;)Lcom/kizitonwose/calendar/core/CalendarYear; + public static final fun getYearIndex (Ljava/time/Year;Ljava/time/Year;)I + public static final fun getYearIndicesCount (Ljava/time/Year;Ljava/time/Year;)I +} + From acf12ddcb88a98e2a8882ade3f204b6ea0f80cfb Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Fri, 26 Jul 2024 17:42:45 +0200 Subject: [PATCH 45/48] Fix lint --- .../kotlin/com/kizitonwose/calendar/core/Year.kt | 1 - .../com/kizitonwose/calendar/core/format/Format.kt | 6 +++++- .../core/serializers/YearMonthSerializers.kt | 2 -- .../calendar/core/serializers/YearSerializers.kt | 2 -- .../com/kizitonwose/calendar/data/YearDataTest.kt | 1 - .../src/commonMain/kotlin/Example2PageHighlight.kt | 12 ++++++++---- .../sample/src/commonMain/kotlin/Example6Page.kt | 6 ++++-- .../sample/src/commonMain/kotlin/Utils.kt | 2 -- .../com/kizitonwose/calendar/data/YearDataTest.kt | 1 - .../calendar/sample/compose/Example2PageHighlight.kt | 12 ++++++++---- .../calendar/sample/compose/Example6Page.kt | 6 ++++-- .../calendar/sample/view/Example4Fragment.kt | 6 ++++-- 12 files changed, 33 insertions(+), 24 deletions(-) diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt index f6a43968..e0861d37 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/Year.kt @@ -107,7 +107,6 @@ public fun Year.isLeap(): Boolean = Year.isLeap(year) */ public fun Year.length(): Int = if (isLeap()) 366 else 365 - /** * Returns the [LocalDate] at the specified [dayOfYear] in this year. * diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt index 9910ea67..cc837660 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/format/Format.kt @@ -9,7 +9,11 @@ import kotlinx.datetime.LocalDate import kotlinx.datetime.format.char private val ISO_YEAR_MONTH by lazy { - LocalDate.Format { year(); char('-'); monthNumber() } + LocalDate.Format { + year() + char('-') + monthNumber() + } } private val ISO_YEAR by lazy { diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt index b4025eeb..64fffe95 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearMonthSerializers.kt @@ -26,7 +26,6 @@ import kotlinx.serialization.encoding.encodeStructure * @see YearMonth.toString */ public object YearMonthIso8601Serializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("com.kizitonwose.calendar.core.YearMonth", PrimitiveKind.STRING) @@ -44,7 +43,6 @@ public object YearMonthIso8601Serializer : KSerializer { * JSON example: `{"year":2020,"month":12}` */ public object YearMonthComponentSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("com.kizitonwose.calendar.core.YearMonth") { element("year") diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt index e3ec0844..557942b2 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/core/serializers/YearSerializers.kt @@ -26,7 +26,6 @@ import kotlinx.serialization.encoding.encodeStructure * @see Year.toString */ public object YearIso8601Serializer : KSerializer { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("com.kizitonwose.calendar.core.Year", PrimitiveKind.STRING) @@ -44,7 +43,6 @@ public object YearIso8601Serializer : KSerializer { * JSON example: `{"year":2020}` */ public object YearComponentSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("com.kizitonwose.calendar.core.Year") { element("year") diff --git a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt index 1cb74a27..da9ee0c4 100644 --- a/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt +++ b/compose-multiplatform/library/src/commonTest/kotlin/com/kizitonwose/calendar/data/YearDataTest.kt @@ -12,7 +12,6 @@ import kotlin.test.Test import kotlin.test.assertEquals class YearDataTest { - @Test @JsName("test1") fun `year data is accurate with non-leap year`() { diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt index 59c40f0b..03f78536 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example2PageHighlight.kt @@ -111,7 +111,8 @@ fun Modifier.backgroundHighlight( DayPosition.InDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isInDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) @@ -123,7 +124,8 @@ fun Modifier.backgroundHighlight( DayPosition.OutDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isOutDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) @@ -208,7 +210,8 @@ fun Modifier.backgroundHighlightLegacy( DayPosition.InDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isInDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) @@ -220,7 +223,8 @@ fun Modifier.backgroundHighlightLegacy( DayPosition.OutDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isOutDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt index 2aecec71..f61f9b51 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example6Page.kt @@ -274,8 +274,10 @@ private fun getMonthWithYear( val firstItem = visibleItemsInfo.first() val daySizePx = with(density) { daySize.toPx() } if ( - firstItem.size < daySizePx * 3 || // Ensure the Month + Year text can fit. - firstItem.offset < layoutInfo.viewportStartOffset && // Ensure the week row size - 1 is visible. + // Ensure the Month + Year text can fit. + firstItem.size < daySizePx * 3 || + // Ensure the week row size - 1 is visible. + firstItem.offset < layoutInfo.viewportStartOffset && (layoutInfo.viewportStartOffset - firstItem.offset > daySizePx) ) { visibleItemsInfo[1].month.yearMonth diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt index ea08723c..d894f57e 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Utils.kt @@ -185,7 +185,6 @@ fun rememberFirstVisibleYearAfterScroll(state: YearCalendarState): CalendarYear return visibleYear.value } - private val CalendarLayoutInfo.completelyVisibleMonths: List get() { val visibleItemsInfo = this.visibleMonthsInfo.toMutableList() @@ -258,6 +257,5 @@ suspend fun LazyListState.animateScrollAndCenterItem(index: Int) { } } - val YearMonth.next: YearMonth get() = this.plus(1, DateTimeUnit.MONTH) val YearMonth.previous: YearMonth get() = this.minus(1, DateTimeUnit.MONTH) diff --git a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt index ec38befe..ef864114 100644 --- a/data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt +++ b/data/src/test/java/com/kizitonwose/calendar/data/YearDataTest.kt @@ -10,7 +10,6 @@ import java.time.Year import java.time.YearMonth class YearDataTest { - @Test fun `year data is accurate with non-leap year`() { val year = Year.of(2019) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2PageHighlight.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2PageHighlight.kt index a90030f2..50a9269d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2PageHighlight.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example2PageHighlight.kt @@ -109,7 +109,8 @@ fun Modifier.backgroundHighlight( } DayPosition.InDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isInDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) @@ -120,7 +121,8 @@ fun Modifier.backgroundHighlight( } DayPosition.OutDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isOutDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) @@ -198,7 +200,8 @@ fun Modifier.backgroundHighlightLegacy( } DayPosition.InDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isInDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) @@ -209,7 +212,8 @@ fun Modifier.backgroundHighlightLegacy( } DayPosition.OutDate -> { textColor(Color.Transparent) - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isOutDateBetweenSelection(day.date, startDate, endDate) ) { padding(vertical = padding) diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt index 70856752..ce2eb27f 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example6Page.kt @@ -275,8 +275,10 @@ private fun getMonthWithYear( val firstItem = visibleItemsInfo.first() val daySizePx = with(density) { daySize.toPx() } if ( - firstItem.size < daySizePx * 3 || // Ensure the Month + Year text can fit. - firstItem.offset < layoutInfo.viewportStartOffset && // Ensure the week row size - 1 is visible. + // Ensure the Month + Year text can fit. + firstItem.size < daySizePx * 3 || + // Ensure the week row size - 1 is visible. + firstItem.offset < layoutInfo.viewportStartOffset && (layoutInfo.viewportStartOffset - firstItem.offset > daySizePx) ) { visibleItemsInfo[1].month.yearMonth diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt index aefcf2f3..b105d78b 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/view/Example4Fragment.kt @@ -224,13 +224,15 @@ class Example4Fragment : BaseFragment(R.layout.example_4_fragment), HasToolbar, // Make the coloured selection background continuous on the // invisible in and out dates across various months. DayPosition.InDate -> - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isInDateBetweenSelection(data.date, startDate, endDate) ) { continuousBgView.applyBackground(rangeMiddleBackground) } DayPosition.OutDate -> - if (startDate != null && endDate != null && + if (startDate != null && + endDate != null && isOutDateBetweenSelection(data.date, startDate, endDate) ) { continuousBgView.applyBackground(rangeMiddleBackground) From fb0be240f1f64f4554f73b5f7d3e385fca180fcf Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 27 Jul 2024 08:16:36 +0200 Subject: [PATCH 46/48] Prepare version 2.5.3 --- .../kizitonwose/calendar/buildsrc/Build.kt | 4 +-- .../calendar/compose/CalendarDefaults.kt | 10 ++++--- gradle/libs.versions.toml | 4 +-- .../calendar/sample/compose/Example10Page.kt | 7 +++-- .../calendar/sample/compose/Example3Page.kt | 27 +++++++++---------- .../calendar/sample/compose/Example8Page.kt | 2 ++ .../calendar/sample/compose/Utils.kt | 2 +- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt b/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt index a360d6e5..9a09360e 100644 --- a/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt +++ b/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt @@ -12,8 +12,8 @@ object Config { } object Version { - val android = "2.6.0-SNAPSHOT" - val multiplatfrom = "2.6.0-SNAPSHOT" + val android = "2.5.3" + val multiplatfrom = "2.5.3" fun String.isNoPublish() = this == VERSION_NO_PUBLISH } diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt index 5bb39f4e..3856d891 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -22,7 +22,7 @@ internal object CalendarDefaults { @Composable private fun pagedFlingBehavior(state: LazyListState): FlingBehavior { val snappingLayout = remember(state) { - val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) + val provider = SnapLayoutInfoProvider(state, CalendarSnapPositionInLayout()) CalendarSnapLayoutInfoProvider(provider) } return rememberSnapFlingBehavior(snappingLayout) @@ -46,5 +46,9 @@ private fun CalendarSnapLayoutInfoProvider( * In compose 1.3, the default was single page snapping (zero), but this changed * in compose 1.4 to decayed page snapping which is not great for calendar usage. */ - override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f + override fun calculateApproachOffset(initialVelocity: Float): Float = 0f } + +@OptIn(ExperimentalFoundationApi::class) +@Suppress("FunctionName") +private fun CalendarSnapPositionInLayout() = SnapPositionInLayout { _, _, _, _, _ -> 0 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f59193a0..10537388 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] agp = "8.5.1" kotlin = "2.0.0" -compose = "1.7.0-beta06" +compose = "1.6.8" espresso = "3.6.1" junit5 = "5.10.3" kotlinxSerialization = "1.7.1" @@ -34,7 +34,7 @@ compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } -compose-material3 = { module = "androidx.compose.material3:material3", version = "1.3.0-beta05" } +compose-material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" } compose-activity = { module = "androidx.activity:activity-compose", version = "1.9.1" } compose-navigation = { module = "androidx.navigation:navigation-compose", version = "2.7.7" } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index d0dae98d..0371d9ba 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -1,7 +1,9 @@ package com.kizitonwose.calendar.sample.compose +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider +import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -147,6 +149,7 @@ fun Example10Page(adjacentYears: Long = 50) { } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun YearHeader( startYear: Year, @@ -163,7 +166,7 @@ private fun YearHeader( .wrapContentHeight() .background(headerBackground), state = headerState, - flingBehavior = rememberSnapFlingBehavior(lazyListState = headerState, SnapPosition.Center), + flingBehavior = rememberSnapFlingBehavior(SnapLayoutInfoProvider(headerState, SnapPositionInLayout.CenterToCenter)), contentPadding = PaddingValues(horizontal = if (isTablet) 40.dp else 10.dp), ) { items(count = startYear.yearsUntil(endYear).toInt()) { index -> diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt index 27fcccb1..b5363187 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt @@ -19,12 +19,9 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items -import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalRippleConfiguration -import androidx.compose.material3.RippleConfiguration import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -128,7 +125,7 @@ fun Example3Page() { state = state, dayContent = { day -> @OptIn(ExperimentalMaterial3Api::class) - CompositionLocalProvider(LocalRippleConfiguration provides Example3RippleConfiguration) { +// CompositionLocalProvider(LocalRippleConfiguration provides Example3RippleConfiguration) { val colors = if (day.position == DayPosition.MonthDate) { flights[day.date].orEmpty().map { colorResource(it.color) } } else { @@ -141,7 +138,7 @@ fun Example3Page() { ) { clicked -> selection = clicked } - } +// } }, monthHeader = { MonthHeader( @@ -322,16 +319,16 @@ private fun AirportInformation(airport: Airport, isDeparture: Boolean) { // The default dark them ripple is too bright so we tone it down. @OptIn(ExperimentalMaterial3Api::class) -private val Example3RippleConfiguration = RippleConfiguration( - color = Color.Gray, - // Copied from RippleTheme#DarkThemeRippleAlpha - rippleAlpha = RippleAlpha( - pressedAlpha = 0.10f, - focusedAlpha = 0.12f, - draggedAlpha = 0.08f, - hoveredAlpha = 0.04f, - ), -) +//private val Example3RippleConfiguration = RippleConfiguration( +// color = Color.Gray, +// // Copied from RippleTheme#DarkThemeRippleAlpha +// rippleAlpha = RippleAlpha( +// pressedAlpha = 0.10f, +// focusedAlpha = 0.12f, +// draggedAlpha = 0.08f, +// hoveredAlpha = 0.04f, +// ), +//) @Preview(heightDp = 600) @Composable diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt index 880c70d8..16888f83 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -176,6 +177,7 @@ private fun FullScreenCalendar( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun PageOptions(selectedIndex: Int, onSelect: (Int) -> Unit) { val options = listOf("Horizontal", "Vertical") diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index 1535fd4b..7a027057 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LocalLifecycleOwner import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState From b3569e6105b930274b9ee1788bf99ed5a1822b6b Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 27 Jul 2024 08:20:08 +0200 Subject: [PATCH 47/48] Prepare versions 2.6.0-beta03-A|2.6.0-alpha04-MP --- .../kizitonwose/calendar/buildsrc/Build.kt | 4 +-- .../calendar/compose/CalendarDefaults.kt | 10 +++---- .../src/commonMain/kotlin/Example10Page.kt | 5 ++-- .../calendar/compose/CalendarDefaults.kt | 10 +++---- gradle/libs.versions.toml | 23 ++++++++-------- .../calendar/sample/compose/Example10Page.kt | 7 ++--- .../calendar/sample/compose/Example3Page.kt | 27 ++++++++++--------- .../calendar/sample/compose/Example8Page.kt | 2 -- .../calendar/sample/compose/Utils.kt | 2 +- 9 files changed, 40 insertions(+), 50 deletions(-) diff --git a/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt b/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt index 9a09360e..d22edb32 100644 --- a/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt +++ b/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt @@ -12,8 +12,8 @@ object Config { } object Version { - val android = "2.5.3" - val multiplatfrom = "2.5.3" + val android = "2.6.0-beta03" + val multiplatfrom = "2.6.0-alpha04" fun String.isNoPublish() = this == VERSION_NO_PUBLISH } diff --git a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt index 3856d891..5bb39f4e 100644 --- a/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt +++ b/compose-multiplatform/library/src/commonMain/kotlin/com/kizitonwose/calendar/compose/CalendarDefaults.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout +import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -22,7 +22,7 @@ internal object CalendarDefaults { @Composable private fun pagedFlingBehavior(state: LazyListState): FlingBehavior { val snappingLayout = remember(state) { - val provider = SnapLayoutInfoProvider(state, CalendarSnapPositionInLayout()) + val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) CalendarSnapLayoutInfoProvider(provider) } return rememberSnapFlingBehavior(snappingLayout) @@ -46,9 +46,5 @@ private fun CalendarSnapLayoutInfoProvider( * In compose 1.3, the default was single page snapping (zero), but this changed * in compose 1.4 to decayed page snapping which is not great for calendar usage. */ - override fun calculateApproachOffset(initialVelocity: Float): Float = 0f + override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f } - -@OptIn(ExperimentalFoundationApi::class) -@Suppress("FunctionName") -private fun CalendarSnapPositionInLayout() = SnapPositionInLayout { _, _, _, _, _ -> 0 } diff --git a/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt b/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt index d6fd6746..0cacf2e6 100644 --- a/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt +++ b/compose-multiplatform/sample/src/commonMain/kotlin/Example10Page.kt @@ -1,8 +1,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout +import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -167,7 +166,7 @@ private fun YearHeader( .wrapContentHeight() .background(headerBackground), state = headerState, - flingBehavior = rememberSnapFlingBehavior(SnapLayoutInfoProvider(headerState, SnapPositionInLayout.CenterToCenter)), + flingBehavior = rememberSnapFlingBehavior(lazyListState = headerState, SnapPosition.Center), contentPadding = PaddingValues(horizontal = if (isTablet) 40.dp else 10.dp), ) { items(count = startYear.yearsUntil(endYear)) { index -> diff --git a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt index 3856d891..5bb39f4e 100644 --- a/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt +++ b/compose/src/main/java/com/kizitonwose/calendar/compose/CalendarDefaults.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout +import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable @@ -22,7 +22,7 @@ internal object CalendarDefaults { @Composable private fun pagedFlingBehavior(state: LazyListState): FlingBehavior { val snappingLayout = remember(state) { - val provider = SnapLayoutInfoProvider(state, CalendarSnapPositionInLayout()) + val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) CalendarSnapLayoutInfoProvider(provider) } return rememberSnapFlingBehavior(snappingLayout) @@ -46,9 +46,5 @@ private fun CalendarSnapLayoutInfoProvider( * In compose 1.3, the default was single page snapping (zero), but this changed * in compose 1.4 to decayed page snapping which is not great for calendar usage. */ - override fun calculateApproachOffset(initialVelocity: Float): Float = 0f + override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f } - -@OptIn(ExperimentalFoundationApi::class) -@Suppress("FunctionName") -private fun CalendarSnapPositionInLayout() = SnapPositionInLayout { _, _, _, _, _ -> 0 } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10537388..8831358e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,8 @@ [versions] agp = "8.5.1" kotlin = "2.0.0" -compose = "1.6.8" +composeAndroid = "1.7.0-beta06" +composeMultiplatform = "1.7.0-alpha01" espresso = "3.6.1" junit5 = "5.10.3" kotlinxSerialization = "1.7.1" @@ -30,18 +31,18 @@ test-junit4 = { module = "junit:junit", version = "4.13.2" } test-junit5-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } test-junit5-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } -compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } -compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose" } -compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } -compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } -compose-material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" } +compose-ui-ui = { module = "androidx.compose.ui:ui", version.ref = "composeAndroid" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "composeAndroid" } +compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "composeAndroid" } +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeAndroid" } +compose-material3 = { module = "androidx.compose.material3:material3", version = "1.3.0-beta05" } compose-activity = { module = "androidx.activity:activity-compose", version = "1.9.1" } -compose-navigation = { module = "androidx.navigation:navigation-compose", version = "2.7.7" } +compose-navigation = { module = "androidx.navigation:navigation-compose", version = "2.8.0-beta06" } -compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" } -compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose" } +compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "composeAndroid" } +compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "composeAndroid" } -jetbrains-compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.7.0-alpha07" } +jetbrains-compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version = "2.8.0-alpha08" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } @@ -56,5 +57,5 @@ bcv = "org.jetbrains.kotlinx.binary-compatibility-validator:0.15.1" # KMM kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -jetbrainsCompose = { id = "org.jetbrains.compose", version = "1.6.11" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt index 0371d9ba..d0dae98d 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example10Page.kt @@ -1,9 +1,7 @@ package com.kizitonwose.calendar.sample.compose -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider -import androidx.compose.foundation.gestures.snapping.SnapPositionInLayout +import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -149,7 +147,6 @@ fun Example10Page(adjacentYears: Long = 50) { } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun YearHeader( startYear: Year, @@ -166,7 +163,7 @@ private fun YearHeader( .wrapContentHeight() .background(headerBackground), state = headerState, - flingBehavior = rememberSnapFlingBehavior(SnapLayoutInfoProvider(headerState, SnapPositionInLayout.CenterToCenter)), + flingBehavior = rememberSnapFlingBehavior(lazyListState = headerState, SnapPosition.Center), contentPadding = PaddingValues(horizontal = if (isTablet) 40.dp else 10.dp), ) { items(count = startYear.yearsUntil(endYear).toInt()) { index -> diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt index b5363187..27fcccb1 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example3Page.kt @@ -19,9 +19,12 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items +import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalRippleConfiguration +import androidx.compose.material3.RippleConfiguration import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider @@ -125,7 +128,7 @@ fun Example3Page() { state = state, dayContent = { day -> @OptIn(ExperimentalMaterial3Api::class) -// CompositionLocalProvider(LocalRippleConfiguration provides Example3RippleConfiguration) { + CompositionLocalProvider(LocalRippleConfiguration provides Example3RippleConfiguration) { val colors = if (day.position == DayPosition.MonthDate) { flights[day.date].orEmpty().map { colorResource(it.color) } } else { @@ -138,7 +141,7 @@ fun Example3Page() { ) { clicked -> selection = clicked } -// } + } }, monthHeader = { MonthHeader( @@ -319,16 +322,16 @@ private fun AirportInformation(airport: Airport, isDeparture: Boolean) { // The default dark them ripple is too bright so we tone it down. @OptIn(ExperimentalMaterial3Api::class) -//private val Example3RippleConfiguration = RippleConfiguration( -// color = Color.Gray, -// // Copied from RippleTheme#DarkThemeRippleAlpha -// rippleAlpha = RippleAlpha( -// pressedAlpha = 0.10f, -// focusedAlpha = 0.12f, -// draggedAlpha = 0.08f, -// hoveredAlpha = 0.04f, -// ), -//) +private val Example3RippleConfiguration = RippleConfiguration( + color = Color.Gray, + // Copied from RippleTheme#DarkThemeRippleAlpha + rippleAlpha = RippleAlpha( + pressedAlpha = 0.10f, + focusedAlpha = 0.12f, + draggedAlpha = 0.08f, + hoveredAlpha = 0.04f, + ), +) @Preview(heightDp = 600) @Composable diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt index 16888f83..880c70d8 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Example8Page.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalContentColor import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -177,7 +176,6 @@ private fun FullScreenCalendar( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun PageOptions(selectedIndex: Int, onSelect: (Int) -> Unit) { val options = listOf("Horizontal", "Vertical") diff --git a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt index 7a027057..1535fd4b 100644 --- a/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt +++ b/sample/src/main/java/com/kizitonwose/calendar/sample/compose/Utils.kt @@ -26,9 +26,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.LocalLifecycleOwner import com.kizitonwose.calendar.compose.CalendarLayoutInfo import com.kizitonwose.calendar.compose.CalendarState import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState From c2169352645d479ca3c442a50595e72d367a8cb6 Mon Sep 17 00:00:00 2001 From: Kizito Nwose Date: Sat, 27 Jul 2024 09:55:51 +0200 Subject: [PATCH 48/48] Prepare next development version. --- .../src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt b/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt index d22edb32..a360d6e5 100644 --- a/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt +++ b/buildSrc/src/main/java/com/kizitonwose/calendar/buildsrc/Build.kt @@ -12,8 +12,8 @@ object Config { } object Version { - val android = "2.6.0-beta03" - val multiplatfrom = "2.6.0-alpha04" + val android = "2.6.0-SNAPSHOT" + val multiplatfrom = "2.6.0-SNAPSHOT" fun String.isNoPublish() = this == VERSION_NO_PUBLISH }