到底
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,256 @@
|
||||
public final class io/github/boguszpawlowski/composecalendar/BuildConfig {
|
||||
public static final field BUILD_TYPE Ljava/lang/String;
|
||||
public static final field DEBUG Z
|
||||
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/CalendarKt {
|
||||
public static final fun Calendar (Lio/github/boguszpawlowski/composecalendar/CalendarState;Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
public static final fun SelectableCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
public static final fun StaticCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
public static final fun rememberCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
|
||||
public static final fun rememberSelectableCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/CalendarState {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
|
||||
public final fun getMonthState ()Lio/github/boguszpawlowski/composecalendar/header/MonthState;
|
||||
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-10 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-11 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-12 Lkotlin/jvm/functions/Function3;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-3 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-4 Lkotlin/jvm/functions/Function3;
|
||||
public static field lambda-5 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-6 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-7 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-8 Lkotlin/jvm/functions/Function3;
|
||||
public static field lambda-9 Lkotlin/jvm/functions/Function4;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-10$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-11$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-12$library_release ()Lkotlin/jvm/functions/Function3;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function3;
|
||||
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function3;
|
||||
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-3 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-4 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-5 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-6 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-7 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-8 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-9 Lkotlin/jvm/functions/Function4;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarKt {
|
||||
public static final fun SelectableWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun StaticWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;ZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun WeekCalendar (Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;Landroidx/compose/ui/Modifier;Ljava/time/LocalDate;Ljava/time/DayOfWeek;ZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun rememberSelectableWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
|
||||
public static final fun rememberWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarState {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
|
||||
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
public final fun getWeekState ()Lio/github/boguszpawlowski/composecalendar/header/WeekState;
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/day/Day {
|
||||
public abstract fun getDate ()Ljava/time/LocalDate;
|
||||
public abstract fun isCurrentDay ()Z
|
||||
public abstract fun isFromCurrentMonth ()Z
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/day/DayState : io/github/boguszpawlowski/composecalendar/day/Day {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
|
||||
public final fun component2 ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
public final fun copy (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
|
||||
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/day/DayState;Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public fun getDate ()Ljava/time/LocalDate;
|
||||
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
public fun hashCode ()I
|
||||
public fun isCurrentDay ()Z
|
||||
public fun isFromCurrentMonth ()Z
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/day/DefaultDayKt {
|
||||
public static final fun DefaultDay-t6yy7ic (Lio/github/boguszpawlowski/composecalendar/day/DayState;Landroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function2;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function2;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function2;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function2;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeaderKt {
|
||||
public static final fun DefaultMonthHeader (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeaderKt {
|
||||
public static final fun DefaultWeekHeader (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/header/MonthState {
|
||||
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/MonthState$Companion;
|
||||
public abstract fun getCurrentMonth ()Ljava/time/YearMonth;
|
||||
public abstract fun getMaxMonth ()Ljava/time/YearMonth;
|
||||
public abstract fun getMinMonth ()Ljava/time/YearMonth;
|
||||
public abstract fun setCurrentMonth (Ljava/time/YearMonth;)V
|
||||
public abstract fun setMaxMonth (Ljava/time/YearMonth;)V
|
||||
public abstract fun setMinMonth (Ljava/time/YearMonth;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/MonthState$Companion {
|
||||
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/MonthStateKt {
|
||||
public static final fun MonthState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/header/MonthState;
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/header/WeekState {
|
||||
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/WeekState$Companion;
|
||||
public abstract fun getCurrentWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public abstract fun getMaxWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public abstract fun getMinWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public abstract fun setCurrentWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
|
||||
public abstract fun setMaxWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
|
||||
public abstract fun setMinWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/WeekState$Companion {
|
||||
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/WeekStateKt {
|
||||
public static final fun WeekState (Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)Lio/github/boguszpawlowski/composecalendar/header/WeekState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler {
|
||||
public static final field $stable I
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler;
|
||||
public final fun calculateNewSelection (Ljava/time/LocalDate;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)Ljava/util/List;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
|
||||
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun getSelection ()Ljava/util/List;
|
||||
public final fun getSelectionMode ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public fun isDateSelected (Ljava/time/LocalDate;)Z
|
||||
public fun onDateSelected (Ljava/time/LocalDate;)V
|
||||
public final fun setSelection (Ljava/util/List;)V
|
||||
public final fun setSelectionMode (Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/EmptySelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
|
||||
public static final field $stable I
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/EmptySelectionState;
|
||||
public fun isDateSelected (Ljava/time/LocalDate;)Z
|
||||
public fun onDateSelected (Ljava/time/LocalDate;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionMode : java/lang/Enum {
|
||||
public static final field Multiple Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static final field None Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static final field Period Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static final field Single Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static fun getEntries ()Lkotlin/enums/EnumEntries;
|
||||
public static fun valueOf (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static fun values ()[Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/selection/SelectionState {
|
||||
public abstract fun isDateSelected (Ljava/time/LocalDate;)Z
|
||||
public abstract fun onDateSelected (Ljava/time/LocalDate;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionState$DefaultImpls {
|
||||
public static fun isDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)Z
|
||||
public static fun onDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeaderKt {
|
||||
public static final fun DefaultDaysOfWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun DefaultWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/Week {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;
|
||||
public fun <init> (Ljava/util/List;)V
|
||||
public final fun compareTo (Lio/github/boguszpawlowski/composecalendar/week/Week;)I
|
||||
public final fun component1 ()Ljava/util/List;
|
||||
public final fun copy (Ljava/util/List;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public final fun dec ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getDays ()Ljava/util/List;
|
||||
public final fun getEnd ()Ljava/time/LocalDate;
|
||||
public final fun getStart ()Ljava/time/LocalDate;
|
||||
public final fun getYearMonth ()Ljava/time/YearMonth;
|
||||
public fun hashCode ()I
|
||||
public final fun inc ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public final fun minusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public final fun plusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/Week$Companion {
|
||||
public final fun now (Ljava/time/DayOfWeek;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public static synthetic fun now$default (Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;Ljava/time/DayOfWeek;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/WeekKt {
|
||||
public static final fun between (Ljava/time/temporal/ChronoUnit;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)I
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id(Android.LibraryPluginId)
|
||||
kotlin(Kotlin.AndroidPluginId)
|
||||
id("common-android-plugin")
|
||||
id(MavenPublish.PluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.boguszpawlowski.composecalendar"
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xexplicit-api=strict" + "-Xcontext-receivers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(Compose.Ui)
|
||||
implementation(Compose.UiTooling)
|
||||
implementation(Compose.AccompanistPager)
|
||||
implementation(Compose.Foundation)
|
||||
implementation(Compose.FoundationLayout)
|
||||
implementation(Compose.Material)
|
||||
implementation(Timber.Core)
|
||||
|
||||
testImplementation(Kotest.Assertions)
|
||||
testImplementation(Kotest.RunnerJunit5)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
POM_ARTIFACT_ID=composecalendar
|
||||
POM_DESCRIPTION=Library for handling the Calendar view in Jetpack Compose.
|
||||
POM_PACKAGING=aar
|
||||
SONATYPE_HOST=S01
|
||||
RELEASE_SIGNING_ENABLED=true
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
@file:Suppress("MatchingDeclarationName")
|
||||
|
||||
package io.github.boguszpawlowski.composecalendar
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.github.boguszpawlowski.composecalendar.day.DayState
|
||||
import io.github.boguszpawlowski.composecalendar.day.DefaultDay
|
||||
import io.github.boguszpawlowski.composecalendar.header.DefaultMonthHeader
|
||||
import io.github.boguszpawlowski.composecalendar.header.MonthState
|
||||
import io.github.boguszpawlowski.composecalendar.month.MonthContent
|
||||
import io.github.boguszpawlowski.composecalendar.month.MonthPager
|
||||
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek
|
||||
import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader
|
||||
import io.github.boguszpawlowski.composecalendar.week.rotateRight
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.temporal.WeekFields
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* State of the calendar composable
|
||||
*
|
||||
* @property monthState currently showed month
|
||||
* @property selectionState handler for the calendar's selection
|
||||
*/
|
||||
@Stable
|
||||
public class CalendarState<T : SelectionState>(
|
||||
public val monthState: MonthState,
|
||||
public val selectionState: T,
|
||||
)
|
||||
|
||||
/**
|
||||
* [Calendar] implementation using a [DynamicSelectionState] as a selection handler.
|
||||
*
|
||||
* * Basic usage:
|
||||
* ```
|
||||
* @Composable
|
||||
* fun MainScreen() {
|
||||
* SelectableCalendar()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param modifier
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param today current day, defaults to [LocalDate.now]
|
||||
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
|
||||
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
|
||||
* @param weekDaysScrollEnabled if the week days should be scrollable
|
||||
* @param calendarState state of the composable
|
||||
* @param dayContent composable rendering the current day
|
||||
* @param monthHeader header for showing the current month and controls for changing it
|
||||
* @param daysOfWeekHeader header for showing captions for each day of week
|
||||
* @param monthContainer container composable for all the days in current month
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun SelectableCalendar(
|
||||
modifier: Modifier = Modifier,
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
today: LocalDate = LocalDate.now(),
|
||||
showAdjacentMonths: Boolean = true,
|
||||
horizontalSwipeEnabled: Boolean = true,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
calendarState: CalendarState<DynamicSelectionState> = rememberSelectableCalendarState(),
|
||||
dayContent: @Composable BoxScope.(DayState<DynamicSelectionState>) -> Unit = { DefaultDay(it) },
|
||||
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
|
||||
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
|
||||
Box { content(PaddingValues()) }
|
||||
},
|
||||
) {
|
||||
Calendar(
|
||||
modifier = modifier,
|
||||
firstDayOfWeek = firstDayOfWeek,
|
||||
today = today,
|
||||
showAdjacentMonths = showAdjacentMonths,
|
||||
horizontalSwipeEnabled = horizontalSwipeEnabled,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
calendarState = calendarState,
|
||||
dayContent = dayContent,
|
||||
monthHeader = monthHeader,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
monthContainer = monthContainer
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [Calendar] implementation without any mechanism for the selection.
|
||||
*
|
||||
* Basic usage:
|
||||
* ```
|
||||
* @Composable
|
||||
* fun MainScreen() {
|
||||
* StaticCalendar()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param modifier
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param today current day, defaults to [LocalDate.now]
|
||||
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
|
||||
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
|
||||
* @param weekDaysScrollEnabled if the week days should be scrollable
|
||||
* @param calendarState state of the composable
|
||||
* @param dayContent composable rendering the current day
|
||||
* @param monthHeader header for showing the current month and controls for changing it
|
||||
* @param daysOfWeekHeader header for showing captions for each day of week
|
||||
* @param monthContainer container composable for all the days in current month
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun StaticCalendar(
|
||||
modifier: Modifier = Modifier,
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
today: LocalDate = LocalDate.now(),
|
||||
showAdjacentMonths: Boolean = true,
|
||||
horizontalSwipeEnabled: Boolean = true,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
calendarState: CalendarState<EmptySelectionState> = rememberCalendarState(),
|
||||
dayContent: @Composable BoxScope.(DayState<EmptySelectionState>) -> Unit = { DefaultDay(it) },
|
||||
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
|
||||
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
|
||||
Box { content(PaddingValues()) }
|
||||
},
|
||||
) {
|
||||
Calendar(
|
||||
modifier = modifier,
|
||||
firstDayOfWeek = firstDayOfWeek,
|
||||
today = today,
|
||||
showAdjacentMonths = showAdjacentMonths,
|
||||
horizontalSwipeEnabled = horizontalSwipeEnabled,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
calendarState = calendarState,
|
||||
dayContent = dayContent,
|
||||
monthHeader = monthHeader,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
monthContainer = monthContainer
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for showing a calendar. The calendar state has to be provided by the call site. If you
|
||||
* want to use built-in implementation, check out:
|
||||
* [SelectableCalendar] - calendar composable handling selection that can be changed at runtime
|
||||
* [StaticCalendar] - calendar without any mechanism for selection
|
||||
*
|
||||
* @param modifier
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param today current day, defaults to [LocalDate.now]
|
||||
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
|
||||
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
|
||||
* @param calendarState state of the composable
|
||||
* @param dayContent composable rendering the current day
|
||||
* @param monthHeader header for showing the current month and controls for changing it
|
||||
* @param daysOfWeekHeader header for showing captions for each day of week
|
||||
* @param monthContainer container composable for all the days in current month
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun <T : SelectionState> Calendar(
|
||||
calendarState: CalendarState<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
today: LocalDate = LocalDate.now(),
|
||||
showAdjacentMonths: Boolean = true,
|
||||
horizontalSwipeEnabled: Boolean = true,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit = { DefaultDay(it) },
|
||||
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
|
||||
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
|
||||
Box { content(PaddingValues()) }
|
||||
},
|
||||
) {
|
||||
val initialMonth = remember { calendarState.monthState.currentMonth }
|
||||
val daysOfWeek = remember(firstDayOfWeek) {
|
||||
DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
monthHeader(calendarState.monthState)
|
||||
if (horizontalSwipeEnabled) {
|
||||
MonthPager(
|
||||
initialMonth = initialMonth,
|
||||
showAdjacentMonths = showAdjacentMonths,
|
||||
monthState = calendarState.monthState,
|
||||
selectionState = calendarState.selectionState,
|
||||
today = today,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
daysOfWeek = daysOfWeek,
|
||||
dayContent = dayContent,
|
||||
weekHeader = daysOfWeekHeader,
|
||||
monthContainer = monthContainer,
|
||||
)
|
||||
} else {
|
||||
MonthContent(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
currentMonth = calendarState.monthState.currentMonth,
|
||||
showAdjacentMonths = showAdjacentMonths,
|
||||
selectionState = calendarState.selectionState,
|
||||
today = today,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
daysOfWeek = daysOfWeek,
|
||||
dayContent = dayContent,
|
||||
weekHeader = daysOfWeekHeader,
|
||||
monthContainer = monthContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for providing a [CalendarState] implementation with selection mechanism.
|
||||
*
|
||||
* @param initialMonth initially rendered month
|
||||
* @param initialSelection initial selection of the composable
|
||||
* @param initialSelectionMode initial mode of the selection
|
||||
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
|
||||
* @param minMonth first month that can be shown
|
||||
* @param maxMonth last month that can be shown
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun rememberSelectableCalendarState(
|
||||
initialMonth: YearMonth = YearMonth.now(),
|
||||
minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange),
|
||||
maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange),
|
||||
initialSelection: List<LocalDate> = emptyList(),
|
||||
initialSelectionMode: SelectionMode = SelectionMode.Single,
|
||||
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
|
||||
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
|
||||
MonthState(
|
||||
initialMonth = initialMonth,
|
||||
minMonth = minMonth,
|
||||
maxMonth = maxMonth
|
||||
)
|
||||
},
|
||||
selectionState: DynamicSelectionState = rememberSaveable(
|
||||
saver = DynamicSelectionState.Saver(confirmSelectionChange),
|
||||
) {
|
||||
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
|
||||
},
|
||||
): CalendarState<DynamicSelectionState> = remember { CalendarState(monthState, selectionState) }
|
||||
|
||||
/**
|
||||
* Helper function for providing a [CalendarState] implementation without a selection mechanism.
|
||||
*
|
||||
* @param initialMonth initially rendered month
|
||||
* @param minMonth first month that can be shown
|
||||
* @param maxMonth last month that can be shown
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun rememberCalendarState(
|
||||
initialMonth: YearMonth = YearMonth.now(),
|
||||
minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange),
|
||||
maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange),
|
||||
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
|
||||
MonthState(
|
||||
initialMonth = initialMonth,
|
||||
minMonth = minMonth,
|
||||
maxMonth = maxMonth
|
||||
)
|
||||
},
|
||||
): CalendarState<EmptySelectionState> = remember { CalendarState(monthState, EmptySelectionState) }
|
||||
|
||||
internal const val DefaultCalendarPagerRange = 10_000L
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
@file:Suppress("MatchingDeclarationName", "LongParameterList")
|
||||
|
||||
package io.github.boguszpawlowski.composecalendar
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.github.boguszpawlowski.composecalendar.day.DayState
|
||||
import io.github.boguszpawlowski.composecalendar.day.DefaultDay
|
||||
import io.github.boguszpawlowski.composecalendar.header.WeekState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek
|
||||
import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader
|
||||
import io.github.boguszpawlowski.composecalendar.week.Week
|
||||
import io.github.boguszpawlowski.composecalendar.week.WeekContent
|
||||
import io.github.boguszpawlowski.composecalendar.week.WeekPager
|
||||
import io.github.boguszpawlowski.composecalendar.week.getWeekDays
|
||||
import io.github.boguszpawlowski.composecalendar.week.rotateRight
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.WeekFields
|
||||
import java.util.Locale
|
||||
import io.github.boguszpawlowski.composecalendar.header.DefaultWeekHeader as DefaultProperWeekHeader
|
||||
|
||||
/**
|
||||
* State of the week calendar composable
|
||||
*
|
||||
* @property weekState currently showed week
|
||||
* @property selectionState handler for the calendar's selection
|
||||
*/
|
||||
@Stable
|
||||
public class WeekCalendarState<T : SelectionState>(
|
||||
public val weekState: WeekState,
|
||||
public val selectionState: T,
|
||||
)
|
||||
|
||||
/**
|
||||
* [WeekCalendar] implementation using a [DynamicSelectionState] as a selection handler.
|
||||
*
|
||||
* * Basic usage:
|
||||
* ```
|
||||
* @Composable
|
||||
* fun MainScreen() {
|
||||
* SelectableWeekCalendar()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param modifier
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param today current day, defaults to [LocalDate.now]
|
||||
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
|
||||
* @param weekDaysScrollEnabled if the week days should be scrollable
|
||||
* @param calendarState state of the composable
|
||||
* @param dayContent composable rendering the current day
|
||||
* @param weekHeader header for showing the current week and controls for changing it
|
||||
* @param daysOfWeekHeader header for showing captions for each day of week
|
||||
*/
|
||||
@Composable
|
||||
public fun SelectableWeekCalendar(
|
||||
modifier: Modifier = Modifier,
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
today: LocalDate = LocalDate.now(),
|
||||
horizontalSwipeEnabled: Boolean = true,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
calendarState: WeekCalendarState<DynamicSelectionState> = rememberSelectableWeekCalendarState(),
|
||||
dayContent: @Composable BoxScope.(DayState<DynamicSelectionState>) -> Unit = { DefaultDay(it) },
|
||||
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
|
||||
DefaultProperWeekHeader(it)
|
||||
},
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
|
||||
) {
|
||||
WeekCalendar(
|
||||
modifier = modifier,
|
||||
calendarState = calendarState,
|
||||
today = today,
|
||||
horizontalSwipeEnabled = horizontalSwipeEnabled,
|
||||
firstDayOfWeek = firstDayOfWeek,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
dayContent = dayContent,
|
||||
weekHeader = weekHeader,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [WeekCalendar] implementation without any mechanism for the selection.
|
||||
*
|
||||
* * Basic usage:
|
||||
* ```
|
||||
* @Composable
|
||||
* fun MainScreen() {
|
||||
* StaticWeekCalendar()
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param modifier
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param today current day, defaults to [LocalDate.now]
|
||||
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
|
||||
* @param calendarState state of the composable
|
||||
* @param dayContent composable rendering the current day
|
||||
* @param weekHeader header for showing the current week and controls for changing it
|
||||
* @param daysOfWeekHeader header for showing captions for each day of week
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun StaticWeekCalendar(
|
||||
modifier: Modifier = Modifier,
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
today: LocalDate = LocalDate.now(),
|
||||
horizontalSwipeEnabled: Boolean = true,
|
||||
calendarState: WeekCalendarState<EmptySelectionState> = rememberWeekCalendarState(),
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<EmptySelectionState>) -> Unit = { DefaultDay(it) },
|
||||
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
|
||||
DefaultProperWeekHeader(it)
|
||||
},
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
|
||||
) {
|
||||
WeekCalendar(
|
||||
modifier = modifier,
|
||||
calendarState = calendarState,
|
||||
today = today,
|
||||
horizontalSwipeEnabled = horizontalSwipeEnabled,
|
||||
firstDayOfWeek = firstDayOfWeek,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
dayContent = dayContent,
|
||||
weekHeader = weekHeader,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Composable for showing a week calendar. The calendar state has to be provided by the call site. If you
|
||||
* want to use built-in implementation, check out:
|
||||
* [SelectableWeekCalendar] - calendar composable handling selection that can be changed at runtime
|
||||
* [StaticWeekCalendar] - calendar without any mechanism for selection
|
||||
*
|
||||
* @param modifier
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param today current day, defaults to [LocalDate.now]
|
||||
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
|
||||
* @param calendarState state of the composable
|
||||
* @param dayContent composable rendering the current day
|
||||
* @param weekHeader header for showing the current week and controls for changing it
|
||||
* @param daysOfWeekHeader header for showing captions for each day of week
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun <T : SelectionState> WeekCalendar(
|
||||
calendarState: WeekCalendarState<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
today: LocalDate = LocalDate.now(),
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
horizontalSwipeEnabled: Boolean = true,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit = { DefaultDay(it) },
|
||||
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
|
||||
DefaultProperWeekHeader(it)
|
||||
},
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
|
||||
) {
|
||||
val initialWeek = remember { calendarState.weekState.currentWeek }
|
||||
val daysOfWeek = remember(firstDayOfWeek) {
|
||||
DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
weekHeader(calendarState.weekState)
|
||||
if (horizontalSwipeEnabled) {
|
||||
WeekPager(
|
||||
initialWeek = initialWeek,
|
||||
daysOfWeek = daysOfWeek,
|
||||
weekState = calendarState.weekState,
|
||||
selectionState = calendarState.selectionState,
|
||||
today = today,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
dayContent = dayContent,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
)
|
||||
} else {
|
||||
WeekContent(
|
||||
daysOfWeek = daysOfWeek,
|
||||
weekDays = calendarState.weekState.currentWeek.getWeekDays(today),
|
||||
selectionState = calendarState.selectionState,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
dayContent = dayContent,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for providing a [WeekCalendarState] implementation with selection mechanism.
|
||||
*
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param initialWeek initially rendered month
|
||||
* @param initialSelection initial selection of the composable
|
||||
* @param initialSelectionMode initial mode of the selection
|
||||
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
|
||||
* @param minWeek first week that can be shown
|
||||
* @param maxWeek last week that can be shown
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun rememberSelectableWeekCalendarState(
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
initialWeek: Week = Week.now(firstDayOfWeek),
|
||||
minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange),
|
||||
maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange),
|
||||
initialSelection: List<LocalDate> = emptyList(),
|
||||
initialSelectionMode: SelectionMode = SelectionMode.Single,
|
||||
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
|
||||
weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) {
|
||||
WeekState(
|
||||
initialWeek = initialWeek,
|
||||
minWeek = minWeek,
|
||||
maxWeek = maxWeek,
|
||||
)
|
||||
},
|
||||
selectionState: DynamicSelectionState = rememberSaveable(
|
||||
saver = DynamicSelectionState.Saver(confirmSelectionChange),
|
||||
) {
|
||||
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
|
||||
},
|
||||
): WeekCalendarState<DynamicSelectionState> =
|
||||
remember { WeekCalendarState(weekState, selectionState) }
|
||||
|
||||
/**
|
||||
* Helper function for providing a [WeekCalendarState] implementation without a selection mechanism.
|
||||
*
|
||||
* @param firstDayOfWeek first day of a week, defaults to current locale's
|
||||
* @param initialWeek initially rendered week
|
||||
* @param minWeek first week that can be shown
|
||||
* @param maxWeek last week that can be shown
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun rememberWeekCalendarState(
|
||||
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
|
||||
initialWeek: Week = Week.now(firstDayOfWeek),
|
||||
minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange),
|
||||
maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange),
|
||||
weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) {
|
||||
WeekState(
|
||||
initialWeek = initialWeek,
|
||||
minWeek = minWeek,
|
||||
maxWeek = maxWeek,
|
||||
)
|
||||
},
|
||||
): WeekCalendarState<EmptySelectionState> =
|
||||
remember { WeekCalendarState(weekState, EmptySelectionState) }
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.github.boguszpawlowski.composecalendar.day
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* Container for basic info about the displayed day
|
||||
*
|
||||
* @param date local date of the day
|
||||
* @param isCurrentDay whenever the day is the today's date
|
||||
* @param isFromCurrentMonth whenever the day is from currently rendered month
|
||||
*/
|
||||
public interface Day {
|
||||
public val date: LocalDate
|
||||
public val isCurrentDay: Boolean
|
||||
public val isFromCurrentMonth: Boolean
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.github.boguszpawlowski.composecalendar.day
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
|
||||
public typealias NonSelectableDayState = DayState<EmptySelectionState>
|
||||
|
||||
/**
|
||||
* Contains information about current selection as well as date of rendered day
|
||||
*/
|
||||
@Stable
|
||||
public data class DayState<T : SelectionState>(
|
||||
private val day: Day,
|
||||
val selectionState: T,
|
||||
) : Day by day
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.github.boguszpawlowski.composecalendar.day
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* Default implementation for day content. It supports different appearance for days from
|
||||
* current and adjacent month, as well as current day and selected day
|
||||
*
|
||||
* @param selectionColor color of the border, when day is selected
|
||||
* @param currentDayColor color of content for the current date
|
||||
* @param onClick callback for interacting with day clicks
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun <T : SelectionState> DefaultDay(
|
||||
state: DayState<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
selectionColor: Color = MaterialTheme.colors.secondary,
|
||||
currentDayColor: Color = MaterialTheme.colors.primary,
|
||||
onClick: (LocalDate) -> Unit = {},
|
||||
) {
|
||||
val date = state.date
|
||||
val selectionState = state.selectionState
|
||||
|
||||
val isSelected = selectionState.isDateSelected(date)
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.padding(2.dp),
|
||||
elevation = if (state.isFromCurrentMonth) 4.dp else 0.dp,
|
||||
border = if (state.isCurrentDay) BorderStroke(1.dp, currentDayColor) else null,
|
||||
contentColor = if (isSelected) selectionColor else contentColorFor(
|
||||
backgroundColor = MaterialTheme.colors.surface
|
||||
)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.clickable {
|
||||
onClick(date)
|
||||
selectionState.onDateSelected(date)
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(text = date.dayOfMonth.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package io.github.boguszpawlowski.composecalendar.day
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import java.time.LocalDate
|
||||
|
||||
@Immutable
|
||||
internal class WeekDay(
|
||||
override val date: LocalDate,
|
||||
override val isCurrentDay: Boolean,
|
||||
override val isFromCurrentMonth: Boolean
|
||||
) : Day
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package io.github.boguszpawlowski.composecalendar.debug
|
||||
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.drawscope.Fill
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.platform.debugInspectorInfo
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* A [Modifier] that draws a border around elements that are recomposing. The border increases in
|
||||
* size and interpolates from red to green as more recompositions occur before a timeout.
|
||||
*/
|
||||
@Stable
|
||||
internal fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
|
||||
|
||||
// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
|
||||
// Modifier.composed will still remember unique data per call site.
|
||||
private val recomposeModifier =
|
||||
Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
|
||||
// The total number of compositions that have occurred. We're not using a State<> here be
|
||||
// able to read/write the value without invalidating (which would cause infinite
|
||||
// recomposition).
|
||||
val totalCompositions = remember { arrayOf(0L) }
|
||||
totalCompositions[0]++
|
||||
|
||||
// The value of totalCompositions at the last timeout.
|
||||
val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
|
||||
|
||||
// Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions
|
||||
// as the key is really just to cause the timer to restart every composition).
|
||||
LaunchedEffect(totalCompositions[0]) {
|
||||
delay(3000)
|
||||
totalCompositionsAtLastTimeout.value = totalCompositions[0]
|
||||
}
|
||||
|
||||
Modifier.drawWithCache {
|
||||
onDrawWithContent {
|
||||
// Draw actual content.
|
||||
drawContent()
|
||||
|
||||
// Below is to draw the highlight, if necessary. A lot of the logic is copied from
|
||||
// Modifier.border
|
||||
val numCompositionsSinceTimeout =
|
||||
totalCompositions[0] - totalCompositionsAtLastTimeout.value
|
||||
|
||||
val hasValidBorderParams = size.minDimension > 0f
|
||||
if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
|
||||
return@onDrawWithContent
|
||||
}
|
||||
|
||||
val (color, strokeWidthPx) =
|
||||
when (numCompositionsSinceTimeout) {
|
||||
// We need at least one composition to draw, so draw the smallest border
|
||||
// color in blue.
|
||||
1L -> Color.Blue to 1f
|
||||
// 2 compositions is _probably_ okay.
|
||||
2L -> Color.Green to 2.dp.toPx()
|
||||
// 3 or more compositions before timeout may indicate an issue. lerp the
|
||||
// color from yellow to red, and continually increase the border size.
|
||||
else -> lerp(
|
||||
Color.Yellow.copy(alpha = 0.8f),
|
||||
Color.Red.copy(alpha = 0.5f),
|
||||
min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
|
||||
) to numCompositionsSinceTimeout.toInt().dp.toPx()
|
||||
}
|
||||
|
||||
val halfStroke = strokeWidthPx / 2
|
||||
val topLeft = Offset(halfStroke, halfStroke)
|
||||
val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
|
||||
|
||||
val fillArea = strokeWidthPx * 2 > size.minDimension
|
||||
val rectTopLeft = if (fillArea) Offset.Zero else topLeft
|
||||
val size = if (fillArea) size else borderSize
|
||||
val style = if (fillArea) Fill else Stroke(strokeWidthPx)
|
||||
|
||||
drawRect(
|
||||
brush = SolidColor(color),
|
||||
topLeft = rectTopLeft,
|
||||
size = size,
|
||||
style = style
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package io.github.boguszpawlowski.composecalendar.header
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.format.TextStyle.FULL
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Default implementation of month header, shows current month and year, as well as
|
||||
* 2 arrows for changing currently showed month
|
||||
*/
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun DefaultMonthHeader(
|
||||
monthState: MonthState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DecrementButton(monthState = monthState)
|
||||
Text(
|
||||
modifier = Modifier.testTag("MonthLabel"),
|
||||
text = monthState.currentMonth.month
|
||||
.getDisplayName(FULL, Locale.getDefault())
|
||||
.lowercase()
|
||||
.replaceFirstChar { it.titlecase() },
|
||||
style = MaterialTheme.typography.h4,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h4)
|
||||
IncrementButton(monthState = monthState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DecrementButton(
|
||||
monthState: MonthState,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag("Decrement"),
|
||||
enabled = monthState.currentMonth > monthState.minMonth,
|
||||
onClick = { monthState.currentMonth = monthState.currentMonth.minusMonths(1) }
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
|
||||
contentDescription = "Previous",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IncrementButton(
|
||||
monthState: MonthState,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag("Increment"),
|
||||
enabled = monthState.currentMonth < monthState.maxMonth,
|
||||
onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) }
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Default.KeyboardArrowRight,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
|
||||
contentDescription = "Next",
|
||||
)
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
package io.github.boguszpawlowski.composecalendar.header
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowRight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import java.time.format.TextStyle.FULL
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Default implementation of week header, shows current month and year, as well as
|
||||
* 2 arrows for changing currently showed month
|
||||
*/
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
@SuppressLint("NewApi")
|
||||
public fun DefaultWeekHeader(
|
||||
weekState: WeekState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
DecrementButton(weekState = weekState)
|
||||
Text(
|
||||
modifier = Modifier.testTag("WeekLabel"),
|
||||
text = weekState.currentWeek.yearMonth.month
|
||||
.getDisplayName(FULL, Locale.getDefault())
|
||||
.lowercase()
|
||||
.replaceFirstChar { it.titlecase() },
|
||||
style = MaterialTheme.typography.h4,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = weekState.currentWeek.yearMonth.year.toString(),
|
||||
style = MaterialTheme.typography.h4
|
||||
)
|
||||
IncrementButton(monthState = weekState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DecrementButton(
|
||||
weekState: WeekState,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag("Decrement"),
|
||||
enabled = weekState.currentWeek > weekState.minWeek,
|
||||
onClick = { weekState.currentWeek = weekState.currentWeek.dec() }
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Default.KeyboardArrowLeft,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
|
||||
contentDescription = "Previous",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun IncrementButton(
|
||||
monthState: WeekState,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag("Increment"),
|
||||
enabled = monthState.currentWeek < monthState.maxWeek,
|
||||
onClick = { monthState.currentWeek = monthState.currentWeek.inc() }
|
||||
) {
|
||||
Image(
|
||||
imageVector = Icons.Default.KeyboardArrowRight,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
|
||||
contentDescription = "Next",
|
||||
)
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package io.github.boguszpawlowski.composecalendar.header
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.setValue
|
||||
import java.time.YearMonth
|
||||
|
||||
@Suppress("FunctionName") // Factory function
|
||||
public fun MonthState(
|
||||
initialMonth: YearMonth,
|
||||
minMonth: YearMonth,
|
||||
maxMonth: YearMonth,
|
||||
): MonthState = MonthStateImpl(initialMonth, minMonth, maxMonth)
|
||||
|
||||
@Stable
|
||||
public interface MonthState {
|
||||
public var currentMonth: YearMonth
|
||||
public var minMonth: YearMonth
|
||||
public var maxMonth: YearMonth
|
||||
|
||||
public companion object {
|
||||
public fun Saver(): Saver<MonthState, Any> = mapSaver(
|
||||
save = { monthState ->
|
||||
mapOf(
|
||||
"currentMonth" to monthState.currentMonth.toString(),
|
||||
"minMonth" to monthState.minMonth.toString(),
|
||||
"maxMonth" to monthState.maxMonth.toString(),
|
||||
)
|
||||
},
|
||||
restore = { restoreMap ->
|
||||
MonthState(
|
||||
YearMonth.parse(restoreMap["currentMonth"] as String),
|
||||
YearMonth.parse(restoreMap["minMonth"] as String),
|
||||
YearMonth.parse(restoreMap["maxMonth"] as String),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
@SuppressLint("NewApi")
|
||||
private class MonthStateImpl(
|
||||
initialMonth: YearMonth,
|
||||
minMonth: YearMonth,
|
||||
maxMonth: YearMonth,
|
||||
) : MonthState {
|
||||
|
||||
private var _currentMonth by mutableStateOf(initialMonth)
|
||||
private var _minMonth by mutableStateOf(minMonth)
|
||||
private var _maxMonth by mutableStateOf(maxMonth)
|
||||
|
||||
override var currentMonth: YearMonth
|
||||
get() = _currentMonth
|
||||
set(value) {
|
||||
_currentMonth = value
|
||||
}
|
||||
|
||||
override var minMonth: YearMonth
|
||||
get() = _minMonth
|
||||
set(value) {
|
||||
if (value > _maxMonth) return
|
||||
_minMonth = value
|
||||
}
|
||||
|
||||
override var maxMonth: YearMonth
|
||||
get() = _maxMonth
|
||||
set(value) {
|
||||
if (value < _minMonth) return
|
||||
_maxMonth = value
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package io.github.boguszpawlowski.composecalendar.header
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.Saver
|
||||
import androidx.compose.runtime.saveable.mapSaver
|
||||
import androidx.compose.runtime.setValue
|
||||
import io.github.boguszpawlowski.composecalendar.week.Week
|
||||
import java.time.LocalDate
|
||||
|
||||
@Suppress("FunctionName") // Factory function
|
||||
public fun WeekState(
|
||||
initialWeek: Week,
|
||||
minWeek: Week,
|
||||
maxWeek: Week,
|
||||
): WeekState = WeekStateImpl(
|
||||
initialWeek = initialWeek,
|
||||
minWeek = minWeek,
|
||||
maxWeek = maxWeek,
|
||||
)
|
||||
|
||||
@Stable
|
||||
@SuppressLint("NewApi")
|
||||
public interface WeekState {
|
||||
public var currentWeek: Week
|
||||
public var minWeek: Week
|
||||
public var maxWeek: Week
|
||||
|
||||
public companion object {
|
||||
@Suppress("FunctionName") // Factory function
|
||||
public fun Saver(): Saver<WeekState, Any> = mapSaver(
|
||||
save = { weekState ->
|
||||
mapOf(
|
||||
CurrentWeekKey to weekState.currentWeek.days[0].toString(),
|
||||
MinWeekKey to weekState.minWeek.days[0].toString(),
|
||||
MaxWeekKey to weekState.maxWeek.days[0].toString(),
|
||||
)
|
||||
},
|
||||
restore = { restoreMap ->
|
||||
WeekState(
|
||||
initialWeek = Week(firstDay = LocalDate.parse(restoreMap[CurrentWeekKey] as String)),
|
||||
minWeek = Week(firstDay = LocalDate.parse(restoreMap[MinWeekKey] as String)),
|
||||
maxWeek = Week(firstDay = LocalDate.parse(restoreMap[MaxWeekKey] as String)),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private const val CurrentWeekKey = "CurrentWeek"
|
||||
private const val MinWeekKey = "MinWeek"
|
||||
private const val MaxWeekKey = "MaxWeek"
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private class WeekStateImpl(
|
||||
initialWeek: Week,
|
||||
minWeek: Week,
|
||||
maxWeek: Week,
|
||||
) : WeekState {
|
||||
|
||||
private var _currentWeek by mutableStateOf(initialWeek)
|
||||
private var _minWeek by mutableStateOf(minWeek)
|
||||
private var _maxWeek by mutableStateOf(maxWeek)
|
||||
|
||||
override var currentWeek: Week
|
||||
get() = _currentWeek
|
||||
set(value) {
|
||||
_currentWeek = value
|
||||
}
|
||||
|
||||
override var minWeek: Week
|
||||
get() = _minWeek
|
||||
set(value) {
|
||||
if (value > _maxWeek) return
|
||||
_minWeek = value
|
||||
}
|
||||
|
||||
override var maxWeek: Week
|
||||
get() = _maxWeek
|
||||
set(value) {
|
||||
if (value < _minWeek) return
|
||||
_maxWeek = value
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package io.github.boguszpawlowski.composecalendar.month
|
||||
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import dev.chrisbanes.snapper.ExperimentalSnapperApi
|
||||
import dev.chrisbanes.snapper.SnapOffsets
|
||||
import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults
|
||||
import dev.chrisbanes.snapper.SnapperLayoutInfo
|
||||
import dev.chrisbanes.snapper.rememberSnapperFlingBehavior
|
||||
import io.github.boguszpawlowski.composecalendar.day.DayState
|
||||
import io.github.boguszpawlowski.composecalendar.header.MonthState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import io.github.boguszpawlowski.composecalendar.week.WeekRow
|
||||
import io.github.boguszpawlowski.composecalendar.week.getWeeks
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@OptIn(ExperimentalSnapperApi::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod", "NewApi")
|
||||
internal fun <T : SelectionState> MonthPager(
|
||||
initialMonth: YearMonth,
|
||||
showAdjacentMonths: Boolean,
|
||||
selectionState: T,
|
||||
monthState: MonthState,
|
||||
daysOfWeek: List<DayOfWeek>,
|
||||
today: LocalDate,
|
||||
modifier: Modifier = Modifier,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
|
||||
weekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
|
||||
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val initialFirstVisibleItemIndex = remember(initialMonth, monthState.minMonth) {
|
||||
ChronoUnit.MONTHS.between(monthState.minMonth, initialMonth).toInt()
|
||||
}
|
||||
val listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
|
||||
)
|
||||
val flingBehavior = rememberSnapperFlingBehavior(
|
||||
lazyListState = listState,
|
||||
snapOffsetForItem = SnapOffsets.Start,
|
||||
springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec,
|
||||
decayAnimationSpec = rememberSplineBasedDecay(),
|
||||
snapIndex = coerceSnapIndex,
|
||||
)
|
||||
|
||||
val monthListState = remember {
|
||||
MonthListState(
|
||||
coroutineScope = coroutineScope,
|
||||
monthState = monthState,
|
||||
listState = listState,
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
if (weekDaysScrollEnabled.not()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth(),
|
||||
content = { weekHeader(daysOfWeek) },
|
||||
)
|
||||
}
|
||||
val pagerCount = remember(monthState.minMonth, monthState.maxMonth) {
|
||||
ChronoUnit.MONTHS.between(monthState.minMonth, monthState.maxMonth).toInt() + 1
|
||||
}
|
||||
LazyRow(
|
||||
modifier = modifier.testTag("MonthPager"),
|
||||
state = listState,
|
||||
flingBehavior = flingBehavior,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
items(
|
||||
count = pagerCount,
|
||||
key = { index ->
|
||||
monthListState.getMonthForPage(index).let { "${it.year}-${it.monthValue}" }
|
||||
}
|
||||
) { index ->
|
||||
MonthContent(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
showAdjacentMonths = showAdjacentMonths,
|
||||
selectionState = selectionState,
|
||||
currentMonth = monthListState.getMonthForPage(index),
|
||||
today = today,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
daysOfWeek = daysOfWeek,
|
||||
dayContent = dayContent,
|
||||
weekHeader = weekHeader,
|
||||
monthContainer = monthContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T : SelectionState> MonthContent(
|
||||
showAdjacentMonths: Boolean,
|
||||
selectionState: T,
|
||||
currentMonth: YearMonth,
|
||||
daysOfWeek: List<DayOfWeek>,
|
||||
today: LocalDate,
|
||||
modifier: Modifier = Modifier,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
|
||||
weekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
|
||||
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit,
|
||||
) {
|
||||
Column {
|
||||
if (weekDaysScrollEnabled) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.wrapContentHeight(),
|
||||
content = { weekHeader(daysOfWeek) },
|
||||
)
|
||||
}
|
||||
monthContainer { paddingValues ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
currentMonth.getWeeks(
|
||||
includeAdjacentMonths = showAdjacentMonths,
|
||||
firstDayOfTheWeek = daysOfWeek.first(),
|
||||
today = today,
|
||||
).forEach { week ->
|
||||
WeekRow(
|
||||
weekDays = week,
|
||||
selectionState = selectionState,
|
||||
dayContent = dayContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSnapperApi::class)
|
||||
internal val coerceSnapIndex: (SnapperLayoutInfo, startIndex: Int, targetIndex: Int) -> Int =
|
||||
{ _, startIndex, targetIndex ->
|
||||
targetIndex
|
||||
.coerceIn(startIndex - 1, startIndex + 1)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package io.github.boguszpawlowski.composecalendar.month
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import io.github.boguszpawlowski.composecalendar.header.MonthState
|
||||
import io.github.boguszpawlowski.composecalendar.util.throttleOnOffset
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.YearMonth
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Stable
|
||||
@SuppressLint("NewApi")
|
||||
internal class MonthListState(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val monthState: MonthState,
|
||||
private val listState: LazyListState,
|
||||
) {
|
||||
|
||||
private val currentFirstVisibleMonth by derivedStateOf {
|
||||
getMonthForPage(listState.firstVisibleItemIndex)
|
||||
}
|
||||
|
||||
init {
|
||||
snapshotFlow { monthState.currentMonth }.onEach { month ->
|
||||
moveToMonth(month)
|
||||
}.launchIn(coroutineScope)
|
||||
|
||||
with(listState) {
|
||||
snapshotFlow { currentFirstVisibleMonth }
|
||||
.throttleOnOffset()
|
||||
.onEach { newMonth ->
|
||||
monthState.currentMonth = newMonth
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
fun getMonthForPage(index: Int): YearMonth =
|
||||
monthState.minMonth.plusMonths(index.toLong())
|
||||
|
||||
private fun moveToMonth(month: YearMonth) {
|
||||
if (month == currentFirstVisibleMonth) return
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(ChronoUnit.MONTHS.between(monthState.minMonth, month).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MonthListState
|
||||
|
||||
if (monthState != other.monthState) return false
|
||||
if (listState != other.listState) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = monthState.hashCode()
|
||||
result = 31 * result + listState.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun YearMonth.minus(other: YearMonth) =
|
||||
ChronoUnit.MONTHS.between(other, this)
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.github.boguszpawlowski.composecalendar.selection
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.boguszpawlowski.composecalendar.util.addOrRemoveIfExists
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* Helper class for calculating new selection, when using a [DynamicSelectionState] approach.
|
||||
* @param date clicked on date
|
||||
* @param selection current selection
|
||||
* @param selectionMode current selection mode
|
||||
* @returns new selection in a form of a list of local dates.
|
||||
*/
|
||||
@SuppressLint("NewApi")
|
||||
public object DynamicSelectionHandler {
|
||||
public fun calculateNewSelection(
|
||||
date: LocalDate,
|
||||
selection: List<LocalDate>,
|
||||
selectionMode: SelectionMode,
|
||||
): List<LocalDate> = when (selectionMode) {
|
||||
SelectionMode.None -> emptyList()
|
||||
SelectionMode.Single -> if (date == selection.firstOrNull()) {
|
||||
emptyList()
|
||||
} else {
|
||||
listOf(date)
|
||||
}
|
||||
SelectionMode.Multiple -> selection.addOrRemoveIfExists(date)
|
||||
SelectionMode.Period -> when {
|
||||
date.isBefore(selection.startOrMax()) -> listOf(date)
|
||||
date.isAfter(selection.startOrMax()) -> selection.fillUpTo(date)
|
||||
date == selection.startOrMax() -> emptyList()
|
||||
else -> selection
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.github.boguszpawlowski.composecalendar.selection
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import java.time.LocalDate
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
internal fun Collection<LocalDate>.startOrMax() = firstOrNull() ?: LocalDate.MAX
|
||||
|
||||
internal fun Collection<LocalDate>.endOrNull() = drop(1).lastOrNull()
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
internal fun Collection<LocalDate>.fillUpTo(date: LocalDate) =
|
||||
(0..date.toEpochDay() - first().toEpochDay()).map {
|
||||
first().plusDays(it)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.github.boguszpawlowski.composecalendar.selection
|
||||
|
||||
/**
|
||||
* Selection modes for the [DynamicSelectionState]
|
||||
* None - no selection allowed
|
||||
* Single - only one date selected
|
||||
* Multiple - multiple dates can be selected
|
||||
* Period - period can be selected
|
||||
*/
|
||||
public enum class SelectionMode {
|
||||
None,
|
||||
Single,
|
||||
Multiple,
|
||||
Period,
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package io.github.boguszpawlowski.composecalendar.selection
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
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.setValue
|
||||
import java.time.LocalDate
|
||||
|
||||
@Stable
|
||||
public interface SelectionState {
|
||||
public fun isDateSelected(date: LocalDate): Boolean = false
|
||||
public fun onDateSelected(date: LocalDate) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Class that enables for dynamically changing selection modes in the runtime. Depending on the mode, selection changes differently.
|
||||
* Mode can be varied by setting desired [SelectionMode] in the [selectionMode] mutable property.
|
||||
* @param confirmSelectionChange return false from this callback to veto the selection change
|
||||
*/
|
||||
@Stable
|
||||
@SuppressLint("NewApi")
|
||||
public class DynamicSelectionState(
|
||||
private val confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
|
||||
selection: List<LocalDate>,
|
||||
selectionMode: SelectionMode,
|
||||
) : SelectionState {
|
||||
|
||||
private var _selection by mutableStateOf(selection)
|
||||
private var _selectionMode by mutableStateOf(selectionMode)
|
||||
|
||||
public var selection: List<LocalDate>
|
||||
get() = _selection
|
||||
set(value) {
|
||||
if (value != selection && confirmSelectionChange(value)) {
|
||||
_selection = value
|
||||
}
|
||||
}
|
||||
|
||||
public var selectionMode: SelectionMode
|
||||
get() = _selectionMode
|
||||
set(value) {
|
||||
if (value != selectionMode) {
|
||||
_selection = emptyList()
|
||||
_selectionMode = value
|
||||
}
|
||||
}
|
||||
|
||||
override fun isDateSelected(date: LocalDate): Boolean = selection.contains(date)
|
||||
|
||||
override fun onDateSelected(date: LocalDate) {
|
||||
selection = DynamicSelectionHandler.calculateNewSelection(date, selection, selectionMode)
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
@Suppress("FunctionName", "UNCHECKED_CAST") // Factory function
|
||||
fun Saver(
|
||||
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean,
|
||||
): Saver<DynamicSelectionState, Any> =
|
||||
listSaver(
|
||||
save = { raw ->
|
||||
listOf(raw.selectionMode, raw.selection.map { it.toString() })
|
||||
},
|
||||
restore = { restored ->
|
||||
DynamicSelectionState(
|
||||
confirmSelectionChange = confirmSelectionChange,
|
||||
selectionMode = restored[0] as SelectionMode,
|
||||
selection = (restored[1] as? List<String>)?.map { LocalDate.parse(it) }.orEmpty(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public object EmptySelectionState : SelectionState {
|
||||
override fun isDateSelected(date: LocalDate): Boolean = false
|
||||
|
||||
override fun onDateSelected(date: LocalDate): Unit = Unit
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.github.boguszpawlowski.composecalendar.util
|
||||
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
|
||||
internal fun Collection<LocalDate>.addOrRemoveIfExists(date: LocalDate) =
|
||||
if (contains(date)) {
|
||||
this - date
|
||||
} else {
|
||||
this + date
|
||||
}
|
||||
|
||||
internal infix fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (value - other.value)) % 7
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package io.github.boguszpawlowski.composecalendar.util
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
context(LazyListState) internal fun <T> Flow<T>.throttleOnOffset() =
|
||||
combine(
|
||||
snapshotFlow { firstVisibleItemScrollOffset }
|
||||
) { newMonth, offset ->
|
||||
newMonth to (offset <= MinimalOffsetForEmit)
|
||||
}.filter { (_, shouldUpdate) ->
|
||||
shouldUpdate
|
||||
}.map { (newValue, _) -> newValue }
|
||||
|
||||
private const val MinimalOffsetForEmit = 10
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.github.boguszpawlowski.composecalendar.util
|
||||
|
||||
import java.time.YearMonth
|
||||
|
||||
internal operator fun YearMonth.dec() = this.minusMonths(1)
|
||||
|
||||
internal operator fun YearMonth.inc() = this.plusMonths(1)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import java.time.DayOfWeek
|
||||
import java.time.format.TextStyle.SHORT
|
||||
import java.util.Locale
|
||||
import kotlin.DeprecationLevel.WARNING
|
||||
|
||||
@Composable
|
||||
@Deprecated(
|
||||
level = WARNING,
|
||||
replaceWith = ReplaceWith(
|
||||
"DefaultDaysOfWeekHeader(daysOfWeek, modifier)",
|
||||
"io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader",
|
||||
),
|
||||
message = "Replace with DefaultDaysOfWeekHeader, DefaultWeekHeader will be removed in future versions"
|
||||
)
|
||||
@SuppressLint("NewApi")
|
||||
public fun DefaultWeekHeader(
|
||||
daysOfWeek: List<DayOfWeek>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
daysOfWeek.forEach { dayOfWeek ->
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()),
|
||||
modifier = modifier
|
||||
.weight(1f)
|
||||
.wrapContentHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@SuppressLint("NewApi")
|
||||
public fun DefaultDaysOfWeekHeader(
|
||||
daysOfWeek: List<DayOfWeek>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier = modifier) {
|
||||
daysOfWeek.forEach { dayOfWeek ->
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()),
|
||||
modifier = modifier
|
||||
.weight(1f)
|
||||
.wrapContentHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun <T> Array<T>.rotateRight(n: Int): List<T> = takeLast(n) + dropLast(n)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.boguszpawlowski.composecalendar.selection.fillUpTo
|
||||
import io.github.boguszpawlowski.composecalendar.util.daysUntil
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.time.temporal.WeekFields
|
||||
import java.util.Locale
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public data class Week(
|
||||
val days: List<LocalDate>,
|
||||
) {
|
||||
|
||||
init {
|
||||
require(days.size == DaysInAWeek)
|
||||
}
|
||||
|
||||
internal constructor(firstDay: LocalDate) : this(
|
||||
listOf(firstDay).fillUpTo(firstDay.plusDays((DaysInAWeek - 1).toLong()))
|
||||
)
|
||||
|
||||
public val start: LocalDate get() = days.first()
|
||||
|
||||
public val end: LocalDate get() = days.last()
|
||||
|
||||
public val yearMonth: YearMonth = YearMonth.of(start.year, start.month)
|
||||
|
||||
public operator fun inc(): Week = plusWeeks(1)
|
||||
|
||||
public operator fun dec(): Week = plusWeeks(-1)
|
||||
|
||||
public operator fun compareTo(other: Week): Int = start.compareTo(other.start)
|
||||
|
||||
public fun minusWeeks(value: Long): Week = plusWeeks(-value)
|
||||
|
||||
public fun plusWeeks(value: Long): Week = copy(days = days.map { it.plusWeeks(value) })
|
||||
|
||||
public companion object {
|
||||
public fun now(firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek): Week {
|
||||
val today = LocalDate.now()
|
||||
val offset = today.dayOfWeek.daysUntil(firstDayOfWeek)
|
||||
val firstDay = today.minusDays(offset.toLong())
|
||||
|
||||
return Week(firstDay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public fun ChronoUnit.between(first: Week, other: Week): Int =
|
||||
between(first.start, other.start).toInt()
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.github.boguszpawlowski.composecalendar.day.Day
|
||||
|
||||
@Immutable
|
||||
internal data class WeekDays(
|
||||
val isFirstWeekOfTheMonth: Boolean = false,
|
||||
val days: List<Day>,
|
||||
)
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import io.github.boguszpawlowski.composecalendar.header.WeekState
|
||||
import io.github.boguszpawlowski.composecalendar.util.throttleOnOffset
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@Stable
|
||||
@SuppressLint("NewApi")
|
||||
internal class WeekListState(
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val weekState: WeekState,
|
||||
private val listState: LazyListState,
|
||||
) {
|
||||
|
||||
private val currentlyFirstVisibleMonth by derivedStateOf {
|
||||
getWeekForPage(listState.firstVisibleItemIndex)
|
||||
}
|
||||
|
||||
init {
|
||||
snapshotFlow { weekState.currentWeek }.onEach { week ->
|
||||
moveToWeek(week)
|
||||
}.launchIn(coroutineScope)
|
||||
|
||||
with(listState) {
|
||||
snapshotFlow { currentlyFirstVisibleMonth }
|
||||
.throttleOnOffset()
|
||||
.onEach { newMonth ->
|
||||
weekState.currentWeek = newMonth
|
||||
}.launchIn(coroutineScope)
|
||||
}
|
||||
}
|
||||
|
||||
fun getWeekForPage(index: Int): Week =
|
||||
weekState.minWeek.plusWeeks(index.toLong())
|
||||
|
||||
private fun moveToWeek(week: Week) {
|
||||
if (week == currentlyFirstVisibleMonth) return
|
||||
coroutineScope.launch {
|
||||
listState.animateScrollToItem(ChronoUnit.WEEKS.between(weekState.minWeek, week))
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as WeekListState
|
||||
|
||||
if (weekState != other.weekState) return false
|
||||
if (listState != other.listState) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = weekState.hashCode()
|
||||
result = 31 * result + listState.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.rememberSplineBasedDecay
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import dev.chrisbanes.snapper.ExperimentalSnapperApi
|
||||
import dev.chrisbanes.snapper.SnapOffsets
|
||||
import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults
|
||||
import dev.chrisbanes.snapper.rememberSnapperFlingBehavior
|
||||
import io.github.boguszpawlowski.composecalendar.day.DayState
|
||||
import io.github.boguszpawlowski.composecalendar.day.WeekDay
|
||||
import io.github.boguszpawlowski.composecalendar.header.WeekState
|
||||
import io.github.boguszpawlowski.composecalendar.month.coerceSnapIndex
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@OptIn(ExperimentalSnapperApi::class)
|
||||
@Composable
|
||||
@Suppress("LongMethod")
|
||||
@SuppressLint("NewApi")
|
||||
internal fun <T : SelectionState> WeekPager(
|
||||
initialWeek: Week,
|
||||
selectionState: T,
|
||||
weekState: WeekState,
|
||||
daysOfWeek: List<DayOfWeek>,
|
||||
today: LocalDate,
|
||||
modifier: Modifier = Modifier,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val initialFirstVisibleItemIndex = remember(initialWeek, weekState.minWeek) {
|
||||
ChronoUnit.WEEKS.between(weekState.minWeek, initialWeek)
|
||||
}
|
||||
val listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
|
||||
)
|
||||
val flingBehavior = rememberSnapperFlingBehavior(
|
||||
lazyListState = listState,
|
||||
snapOffsetForItem = SnapOffsets.Start,
|
||||
springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec,
|
||||
decayAnimationSpec = rememberSplineBasedDecay(),
|
||||
snapIndex = coerceSnapIndex,
|
||||
)
|
||||
|
||||
val weekListState = remember {
|
||||
WeekListState(
|
||||
coroutineScope = coroutineScope,
|
||||
weekState = weekState,
|
||||
listState = listState,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (weekDaysScrollEnabled.not()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth(),
|
||||
content = { daysOfWeekHeader(daysOfWeek) },
|
||||
)
|
||||
}
|
||||
val pagerCount = remember(weekState.minWeek, weekState.maxWeek) {
|
||||
ChronoUnit.WEEKS.between(weekState.minWeek, weekState.maxWeek) + 1
|
||||
}
|
||||
LazyRow(
|
||||
modifier = modifier.testTag("WeekPager"),
|
||||
state = listState,
|
||||
flingBehavior = flingBehavior,
|
||||
verticalAlignment = Alignment.Top,
|
||||
) {
|
||||
items(
|
||||
count = pagerCount,
|
||||
key = { index -> weekListState.getWeekForPage(index).start.let { "${it.month}-${it.dayOfMonth}" } },
|
||||
) { index ->
|
||||
WeekContent(
|
||||
modifier = Modifier.fillParentMaxWidth(),
|
||||
daysOfWeek = daysOfWeek,
|
||||
weekDays = weekListState.getWeekForPage(index).getWeekDays(today = today),
|
||||
selectionState = selectionState,
|
||||
weekDaysScrollEnabled = weekDaysScrollEnabled,
|
||||
dayContent = dayContent,
|
||||
daysOfWeekHeader = daysOfWeekHeader,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun <T : SelectionState> WeekContent(
|
||||
selectionState: T,
|
||||
weekDays: WeekDays,
|
||||
daysOfWeek: List<DayOfWeek>,
|
||||
modifier: Modifier = Modifier,
|
||||
weekDaysScrollEnabled: Boolean = true,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
|
||||
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (weekDaysScrollEnabled) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth(),
|
||||
content = { daysOfWeekHeader(daysOfWeek) },
|
||||
)
|
||||
}
|
||||
WeekRow(
|
||||
weekDays = weekDays,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
selectionState = selectionState,
|
||||
dayContent = dayContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Week.getWeekDays(today: LocalDate): WeekDays {
|
||||
val weekDays = days.map {
|
||||
WeekDay(
|
||||
date = it,
|
||||
isCurrentDay = it == today,
|
||||
isFromCurrentMonth = true,
|
||||
)
|
||||
}
|
||||
|
||||
return WeekDays(
|
||||
isFirstWeekOfTheMonth = false,
|
||||
days = weekDays,
|
||||
)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.github.boguszpawlowski.composecalendar.day.DayState
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
|
||||
|
||||
@Composable
|
||||
internal fun <T : SelectionState> WeekRow(
|
||||
weekDays: WeekDays,
|
||||
selectionState: T,
|
||||
modifier: Modifier = Modifier,
|
||||
dayContent: @Composable BoxScope.(DayState<T>) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
horizontalArrangement = if (weekDays.isFirstWeekOfTheMonth) Arrangement.End else Arrangement.Start
|
||||
) {
|
||||
weekDays.days.forEachIndexed { index, day ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(1f / (7 - index))
|
||||
) {
|
||||
dayContent(DayState(day, selectionState))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.github.boguszpawlowski.composecalendar.day.WeekDay
|
||||
import io.github.boguszpawlowski.composecalendar.util.daysUntil
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
|
||||
internal const val DaysInAWeek = 7
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
internal fun YearMonth.getWeeks(
|
||||
includeAdjacentMonths: Boolean,
|
||||
firstDayOfTheWeek: DayOfWeek,
|
||||
today: LocalDate,
|
||||
): List<WeekDays> {
|
||||
val daysLength = lengthOfMonth()
|
||||
|
||||
val starOffset = atDay(1).dayOfWeek daysUntil firstDayOfTheWeek
|
||||
val endOffset =
|
||||
DaysInAWeek - (atDay(daysLength).dayOfWeek daysUntil firstDayOfTheWeek) - 1
|
||||
|
||||
return (1 - starOffset..daysLength + endOffset).chunked(DaysInAWeek).mapIndexed { index, days ->
|
||||
WeekDays(
|
||||
isFirstWeekOfTheMonth = index == 0,
|
||||
days = days.mapNotNull { dayOfMonth ->
|
||||
val (date, isFromCurrentMonth) = when (dayOfMonth) {
|
||||
in Int.MIN_VALUE..0 -> if (includeAdjacentMonths) {
|
||||
val previousMonth = this.minusMonths(1)
|
||||
previousMonth.atDay(previousMonth.lengthOfMonth() + dayOfMonth) to false
|
||||
} else {
|
||||
return@mapNotNull null
|
||||
}
|
||||
in 1..daysLength -> atDay(dayOfMonth) to true
|
||||
else -> if (includeAdjacentMonths) {
|
||||
val previousMonth = this.plusMonths(1)
|
||||
previousMonth.atDay(dayOfMonth - daysLength) to false
|
||||
} else {
|
||||
return@mapNotNull null
|
||||
}
|
||||
}
|
||||
|
||||
WeekDay(
|
||||
date = date,
|
||||
isFromCurrentMonth = isFromCurrentMonth,
|
||||
isCurrentDay = date.equals(today),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
@file:Suppress("UnderscoresInNumericLiterals")
|
||||
|
||||
package io.github.boguszpawlowski.composecalendar.selection
|
||||
|
||||
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple
|
||||
import io.kotest.assertions.throwables.shouldNotThrowAny
|
||||
import io.kotest.core.spec.style.ShouldSpec
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.shouldBe
|
||||
import java.time.LocalDate
|
||||
import java.time.Month.APRIL
|
||||
|
||||
internal class SelectionStateTest : ShouldSpec({
|
||||
|
||||
val yesterday = LocalDate.of(2020, APRIL, 9)
|
||||
val today = LocalDate.of(2020, APRIL, 10)
|
||||
val tomorrow = LocalDate.of(2020, APRIL, 11)
|
||||
|
||||
context("Selection state with SelectionMode.None") {
|
||||
should("not change selection after new value arrives") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None)
|
||||
|
||||
state.onDateSelected(LocalDate.now())
|
||||
|
||||
state.selection shouldBe emptyList()
|
||||
}
|
||||
|
||||
should("be able to change if mode has been changed") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None)
|
||||
|
||||
state.selectionMode = SelectionMode.Single
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection shouldBe listOf(today)
|
||||
}
|
||||
}
|
||||
|
||||
context("Selection state with SelectionMode.Single") {
|
||||
should("change state to single after day is selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
|
||||
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection shouldBe listOf(today)
|
||||
}
|
||||
|
||||
should("change state to none when same day is selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection shouldBe emptyList()
|
||||
}
|
||||
|
||||
should("change to other day when selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(tomorrow)
|
||||
|
||||
state.selection shouldBe listOf(tomorrow)
|
||||
}
|
||||
|
||||
should("not be mutable after selection mode is changed to None") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)
|
||||
|
||||
state.selectionMode = SelectionMode.None
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection shouldBe emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
context("Selection state with SelectionMode.Multiple") {
|
||||
should("allow for multiple days selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(tomorrow)
|
||||
|
||||
state.selection shouldContainExactly listOf(
|
||||
today,
|
||||
tomorrow
|
||||
)
|
||||
}
|
||||
|
||||
should("switch selection off once day is selected second time") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(tomorrow)
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection shouldContainExactly listOf(tomorrow)
|
||||
}
|
||||
}
|
||||
|
||||
context("Selection state with SelectionMode.Period") {
|
||||
should("allow for period of days selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(tomorrow)
|
||||
|
||||
state.selection.first() shouldBe today
|
||||
state.selection.last() shouldBe tomorrow
|
||||
}
|
||||
|
||||
should("switch selection off once start day is selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(tomorrow)
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection shouldBe emptyList()
|
||||
}
|
||||
should("change end date once the date selected is between start and the end") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
|
||||
|
||||
state.onDateSelected(yesterday)
|
||||
state.onDateSelected(tomorrow)
|
||||
state.onDateSelected(today)
|
||||
|
||||
state.selection.first() shouldBe yesterday
|
||||
state.selection.last() shouldBe today
|
||||
}
|
||||
should("change start day once day before start is selected") {
|
||||
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)
|
||||
|
||||
state.onDateSelected(today)
|
||||
state.onDateSelected(tomorrow)
|
||||
state.onDateSelected(yesterday)
|
||||
|
||||
state.selection.first() shouldBe yesterday
|
||||
state.selection.endOrNull() shouldBe null
|
||||
}
|
||||
}
|
||||
|
||||
context("Selection State interface default values") {
|
||||
val myInterfaceWithNoMethods = object : SelectionState {}
|
||||
|
||||
should("Default isDateSelected to false") {
|
||||
myInterfaceWithNoMethods.isDateSelected(today) shouldBe false
|
||||
}
|
||||
|
||||
should("Have a default implementation that doesn't throw an exception for onDateSelected") {
|
||||
shouldNotThrowAny {
|
||||
myInterfaceWithNoMethods.onDateSelected(today)
|
||||
}
|
||||
}
|
||||
}
|
||||
context("Selection State with confirm state change callback") {
|
||||
var nextVetoResult = false
|
||||
val initialSelection = LocalDate.of(1999, 10, 12)
|
||||
val newSelection = initialSelection.plusDays(1)
|
||||
|
||||
val selectionState = DynamicSelectionState(
|
||||
confirmSelectionChange = { nextVetoResult },
|
||||
selection = listOf(initialSelection),
|
||||
selectionMode = Multiple,
|
||||
)
|
||||
should("Not change the selection when change is vetoed") {
|
||||
selectionState.onDateSelected(newSelection)
|
||||
|
||||
selectionState.selection shouldBe listOf(initialSelection)
|
||||
}
|
||||
should("Change the selection when change is not vetoed") {
|
||||
nextVetoResult = true
|
||||
|
||||
selectionState.onDateSelected(newSelection)
|
||||
|
||||
selectionState.selection shouldBe listOf(initialSelection, newSelection)
|
||||
}
|
||||
}
|
||||
})
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import io.kotest.assertions.asClue
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.ShouldSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import java.time.LocalDate
|
||||
|
||||
internal class WeekTest : ShouldSpec({
|
||||
|
||||
val today = LocalDate.of(2000, 10, 2)
|
||||
|
||||
context("Secondary constructor") {
|
||||
val week = Week(firstDay = today)
|
||||
|
||||
should("properly create an instance of week") {
|
||||
week.asClue {
|
||||
it.days.first() shouldBe today
|
||||
it.days.last() shouldBe today.plusDays(6)
|
||||
it.days.size shouldBe 7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("Incrementing") {
|
||||
val week = Week(firstDay = today).inc()
|
||||
|
||||
should("shift the days by 7") {
|
||||
week.asClue {
|
||||
it.days.first() shouldBe today.plusWeeks(1)
|
||||
it.days.last() shouldBe today.plusDays(6).plusWeeks(1)
|
||||
it.days.size shouldBe 7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("Decrementing") {
|
||||
val week = Week(firstDay = today).dec()
|
||||
|
||||
should("shift the days by -7") {
|
||||
week.asClue {
|
||||
it.days.first() shouldBe today.plusWeeks(-1)
|
||||
it.days.last() shouldBe today.plusDays(6).plusWeeks(-1)
|
||||
it.days.size shouldBe 7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("Adding a week") {
|
||||
val week = Week(firstDay = today).plusWeeks(7)
|
||||
|
||||
should("add correct number of days") {
|
||||
week.asClue {
|
||||
it.days.first() shouldBe today.plusWeeks(7)
|
||||
it.days.last() shouldBe today.plusDays(6).plusWeeks(7)
|
||||
it.days.size shouldBe 7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("Constructing a week with invalid number of days") {
|
||||
should("throw") {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
Week(List(18) { LocalDate.now() })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
@file:Suppress("UnderscoresInNumericLiterals")
|
||||
|
||||
package io.github.boguszpawlowski.composecalendar.week
|
||||
|
||||
import io.kotest.core.spec.style.ShouldSpec
|
||||
import io.kotest.matchers.collections.shouldContainAll
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.collections.shouldHaveSize
|
||||
import io.kotest.matchers.shouldBe
|
||||
import java.time.DayOfWeek.MONDAY
|
||||
import java.time.DayOfWeek.SATURDAY
|
||||
import java.time.DayOfWeek.SUNDAY
|
||||
import java.time.LocalDate
|
||||
import java.time.YearMonth
|
||||
|
||||
internal class WeekUtilTest : ShouldSpec({
|
||||
|
||||
val today = LocalDate.of(2020, 9, 15)
|
||||
val month = YearMonth.of(2020, 9)
|
||||
val previousMonth = YearMonth.of(2020, 8)
|
||||
val nextMonth = YearMonth.of(2020, 10)
|
||||
|
||||
context("Extracting weeks without adjacent months") {
|
||||
val includeAdjacentMonths = false
|
||||
should("return days only from current month") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
|
||||
val days = weeks.flatMap { it.days }
|
||||
|
||||
days.map { it.date }.forEach {
|
||||
it.isFromMonth(month) shouldBe true
|
||||
it.isFromMonth(previousMonth) shouldBe false
|
||||
it.isFromMonth(nextMonth) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
should("return all days from the month") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
|
||||
val days = weeks.flatMap { it.days }.map { it.date }
|
||||
|
||||
val daysLength = month.lengthOfMonth()
|
||||
val daysFromCurrentMonth = (1..daysLength).map { month.atDay(it) }
|
||||
|
||||
days shouldContainExactly daysFromCurrentMonth
|
||||
}
|
||||
|
||||
should("return days properly split to weeks") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
|
||||
|
||||
weeks[0].days shouldHaveSize 6
|
||||
weeks[1].days shouldHaveSize 7
|
||||
weeks[2].days shouldHaveSize 7
|
||||
weeks[3].days shouldHaveSize 7
|
||||
weeks[4].days shouldHaveSize 3
|
||||
|
||||
weeks shouldHaveSize 5
|
||||
}
|
||||
|
||||
should("return days properly split to weeks when first day is Saturday") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, SATURDAY, today)
|
||||
|
||||
weeks[0].days shouldHaveSize 4
|
||||
weeks[1].days shouldHaveSize 7
|
||||
weeks[2].days shouldHaveSize 7
|
||||
weeks[3].days shouldHaveSize 7
|
||||
weeks[4].days shouldHaveSize 5
|
||||
|
||||
weeks shouldHaveSize 5
|
||||
}
|
||||
|
||||
should("return days properly split to weeks when first day is Sunday ") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, SUNDAY, today)
|
||||
|
||||
weeks[0].days shouldHaveSize 5
|
||||
weeks[1].days shouldHaveSize 7
|
||||
weeks[2].days shouldHaveSize 7
|
||||
weeks[3].days shouldHaveSize 7
|
||||
weeks[4].days shouldHaveSize 4
|
||||
|
||||
weeks shouldHaveSize 5
|
||||
}
|
||||
}
|
||||
|
||||
context("Extracting weeks with adjacent months") {
|
||||
val includeAdjacentMonths = true
|
||||
should("return days from current and previous months") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
|
||||
val days = weeks.flatMap { it.days }
|
||||
|
||||
days.map { it.date }.forEachIndexed { index, day ->
|
||||
when (index) {
|
||||
0 -> {
|
||||
day.isFromMonth(month) shouldBe false
|
||||
day.isFromMonth(previousMonth) shouldBe true
|
||||
day.isFromMonth(nextMonth) shouldBe false
|
||||
}
|
||||
in 1..30 -> {
|
||||
day.isFromMonth(month) shouldBe true
|
||||
day.isFromMonth(previousMonth) shouldBe false
|
||||
day.isFromMonth(nextMonth) shouldBe false
|
||||
}
|
||||
else -> {
|
||||
day.isFromMonth(month) shouldBe false
|
||||
day.isFromMonth(previousMonth) shouldBe false
|
||||
day.isFromMonth(nextMonth) shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
should("return all days from the month") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
|
||||
val days = weeks.flatMap { it.days }.map { it.date }
|
||||
|
||||
val daysLength = month.lengthOfMonth()
|
||||
val endOffset = 6 - month.atEndOfMonth().dayOfWeek.ordinal
|
||||
val startOffset = month.atDay(1).dayOfWeek.ordinal - 1
|
||||
val daysFromCurrentMonth = (1..daysLength).map { month.atDay(it) }
|
||||
val daysFromNextMonth = (1..endOffset).map { nextMonth.atDay(it) }
|
||||
val daysFromPreviousMonth =
|
||||
(previousMonth.lengthOfMonth() - startOffset..previousMonth.lengthOfMonth()).map {
|
||||
previousMonth.atDay(it)
|
||||
}
|
||||
|
||||
days shouldContainAll daysFromCurrentMonth + daysFromNextMonth + daysFromPreviousMonth
|
||||
}
|
||||
|
||||
should("return days properly split to weeks") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today)
|
||||
|
||||
weeks[0].days shouldHaveSize 7
|
||||
weeks[1].days shouldHaveSize 7
|
||||
weeks[2].days shouldHaveSize 7
|
||||
weeks[3].days shouldHaveSize 7
|
||||
weeks[4].days shouldHaveSize 7
|
||||
|
||||
weeks shouldHaveSize 5
|
||||
}
|
||||
|
||||
should("return days properly split to weeks when first day is Sunday") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, SUNDAY, today)
|
||||
|
||||
weeks[0].days shouldHaveSize 7
|
||||
weeks[1].days shouldHaveSize 7
|
||||
weeks[2].days shouldHaveSize 7
|
||||
weeks[3].days shouldHaveSize 7
|
||||
weeks[4].days shouldHaveSize 7
|
||||
|
||||
weeks shouldHaveSize 5
|
||||
}
|
||||
|
||||
should("return days properly split to weeks when first day is Saturday") {
|
||||
val weeks = month.getWeeks(includeAdjacentMonths, SATURDAY, today)
|
||||
|
||||
weeks[0].days shouldHaveSize 7
|
||||
weeks[1].days shouldHaveSize 7
|
||||
weeks[2].days shouldHaveSize 7
|
||||
weeks[3].days shouldHaveSize 7
|
||||
weeks[4].days shouldHaveSize 7
|
||||
|
||||
weeks shouldHaveSize 5
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private fun LocalDate.isFromMonth(yearMonth: YearMonth) =
|
||||
month == yearMonth.month && year == yearMonth.year
|
||||
Reference in New Issue
Block a user