diff --git a/ComposeCalendar/.gitignore b/ComposeCalendar/.gitignore
new file mode 100644
index 000000000..14a4e7e33
--- /dev/null
+++ b/ComposeCalendar/.gitignore
@@ -0,0 +1,71 @@
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+# Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
diff --git a/ComposeCalendar/.idea/.gitignore b/ComposeCalendar/.idea/.gitignore
new file mode 100644
index 000000000..e9dc156a8
--- /dev/null
+++ b/ComposeCalendar/.idea/.gitignore
@@ -0,0 +1,6 @@
+# Default ignored files
+/*
+!codeStyles/
+!fileTemplates/
+!file.template.settings.xml
+!.gitignore
diff --git a/ComposeCalendar/.idea/codeStyles/Project.xml b/ComposeCalendar/.idea/codeStyles/Project.xml
new file mode 100644
index 000000000..55e76659b
--- /dev/null
+++ b/ComposeCalendar/.idea/codeStyles/Project.xml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml b/ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 000000000..79ee123c2
--- /dev/null
+++ b/ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ComposeCalendar/LICENSE b/ComposeCalendar/LICENSE
new file mode 100644
index 000000000..4ad935286
--- /dev/null
+++ b/ComposeCalendar/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2024 Bogusz Pawłowski
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/ComposeCalendar/README.md b/ComposeCalendar/README.md
new file mode 100644
index 000000000..68b23a599
--- /dev/null
+++ b/ComposeCalendar/README.md
@@ -0,0 +1,250 @@
+
+
+Compose Calendar is a composable handling all complexity of rendering calendar component and date selection.
+Due to flexibility provided by slot API's, you can decide how the calendar will look like, the library will handle proper calendar elements arrangement and it's state.
+
+
+[
](https://search.maven.org/search?q=g:io.github.boguszpawlowski.composecalendar)
+[
](https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/boguszpawlowski/composecalendar/)
+[
](https://androidweekly.net/issues/issue-501/)
+## Setup
+Library is available on Maven Central repository.
+```kotlin
+ // module-level build.gradle
+ dependecies {
+ implementation "io.github.boguszpawlowski.composecalendar:composecalendar:"
+
+ // separate artifact with utilities for working with kotlinx-datetime
+ implementation "io.github.boguszpawlowski.composecalendar:kotlinx-datetime:"
+ }
+```
+Snapshots are available on [Sonatype’s snapshots repository](https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/boguszpawlowski/composecalendar/).
+
+### 1.3.0 Changes
+From version 1.3.0 library no longer uses `coreLibraryDesugaring` itself. If you are using it in project supporting min SDK < 26, you need to add it to prevent runtime crashes.
+```kotlin
+ // app-level build.gradle
+ android {
+ compileOptions {
+ coreLibraryDesugaringEnabled true
+ }
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ }
+ dependencies {
+ coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
+ }
+```
+
+## Supported features
+- Selection (single, multiple or a range of days)
+- Chose day as first day of week
+- Showing/hiding adjacent months
+- Month and week headers
+- Customizable month container
+- Fully customizable day content
+- Horizontal swipe for changing a current month
+- Month / Week mode
+- Min / Max Month (or Week)
+
+## Basic Usage
+
+### Static calendar
+To show the basic version of the calendar, without any kind of selection mechanism, you can simply use the `StaticCalendar` composable without passing any parameters:
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ StaticCalendar()
+ }
+
+```
+This chunk will render the calendar with default components for each day, and also month and week headers.
+See the `StaticCalendarSample` file for a full example. For showing a week calendar, you can similarly use `StaticWeekCalendar`:
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ StaticWeekCalendar()
+ }
+
+```
+
+
+
+> :exclamation: By default, at first the calendar will show current month. If you want to start with some different date, you have to pass an `initialMonth` parameter to the initial state of the calendar. See [Initial State section](#initial-state)
+
+### Selectable calendar
+Calendar with a mechanism for selection. The default implementation uses `DynamicSelectionState` (see [Dynamic Selection section](#dynamic-selection-state)) which allows to change `SelectionMode` in the runtime.
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ SelectableCalendar()
+ }
+
+```
+By the default, after changing the selection mode, selection is cleared.
+See the `SelectableCalendarSample` file for a full example. For showing a week calendar, you can similarly use `SelectableWeekCalendar`:
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ SelectableWeekCalendar()
+ }
+
+```
+See the `WeekCalendarSample` file for a full example.
+
+https://user-images.githubusercontent.com/36514058/126049987-685a7b81-1596-48a4-95a1-c342f6c796bf.mp4
+
+> :exclamation: If you want to define your own selection behavior, please check out the [Custom Selection section](#custom-selection) and/or `CustomSelectionSample`.
+
+### Calendar with custom components
+For the customization you should pass your own composable functions as day content, moth header etc.:
+```kotlin
+
+ @Composable
+ fun MyDay(dayState: DayState) {
+ Text(dayState.date.dayOfMonth.toString())
+ }
+
+ @Composable
+ fun MainScreen() {
+ StaticCalendar(
+ dayContent = { dayState -> MyDay(dayState) }
+ )
+ }
+
+```
+The same you can do for every customizable element:
+- Day content - responsible for single day content
+- Month header - responsible for showing the current month (and by default for changing the current month)
+- Week header - responsible for showing the names of week days.
+- Month container - wrapping the month content, it defaults to a plain `Box`, but can be any layout.
+
+The `Calendar` composable accepts a `Modifier` for simple customization of the overall appearance.
+See the `CustomComponentsSample` for a full example.
+
+### Custom selection
+As the selection state is represented by an interface, you can provide your own implementation, to suit your
+use-case. E.g:
+```kotlin
+ class MonthSelectionState(
+ initialSelection: YearMonth? = null,
+ ) : SelectionState {
+ private var selection by mutableStateOf(initialSelection)
+
+ override fun isDateSelected(date: LocalDate): Boolean =
+ date.yearMonth == selection
+
+ override fun onDateSelected(date: LocalDate) {
+ selection = if (date.yearMonth == selection) null else date.yearMonth
+ }
+ }
+```
+To use the defined selection state, you have to pass it into a generic version of `Calendar` composable.
+This chunk is an implementation that will select all days in a clicked day's month. For a full example
+please check out `CustomSelectionSample` file.
+
+### Calendar properties customization
+Apart from rendering your own components inside the calendar, you can modify it by passing different properties.:
+- `showAdjacentMonths` - whenever to render days from adjacent months. Defaults to `true`.
+- `firstDayOfWeek` - you can pass the `DayOfWeek` which you want you week to start with. It defaults to the first day of week of the `Locale.default()`.
+- `horizontalScrollEnabled` - a Boolean flag which enables month to be changed by a horizontal swipe. Defaults to `true`.
+- `minMonth` - a `YearMonth` object representing the minimum month that can be shown in the calendar. By default there is no minimum month.
+- `maxMonth` - a `YearMonth` object representing the maximum month that can be shown in the calendar. By default there is no maximum month.
+
+> :exclamation: You cannot set `minMonth` to be lower than `maxMonth` and vice versa. If you do so, the calendar state won't change.
+
+Apart from this, `Calendar` you can pass a `Modifier` object like in any other composable.
+
+## State
+Calendar composable holds its state as an `CalendarState` object, which consists of 2 properties.
+- `MonthState` - current value of the presented month.
+- `SelectionState` - current value of the selection.
+
+Both properties are represented by interfaces, so the default implementation can be overwritten if needed.
+The calendar state is leveraging Compose saving mechanism, so that the state will survive any configuration change, or the process death.
+
+### Initial state
+Initial state for the static calendar is provided by the `rememberCalendarState()` function. If you need to change the initial conditions, you can pass the params to it:
+
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ StaticCalendar(
+ calendarState = rememberCalendarState(
+ initialMonth = YearMonth.now().plusYears(1),
+ )
+ )
+ }
+
+```
+In case of the selectable calendar, the state has additional parameters, used to calculate the initial selection:
+
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ SelectableCalendar(
+ calendarState = rememberSelectableCalendarState(
+ initialMonth = YearMonth.now().plusYears(1),
+ initialSelection = listOf(LocalDate.parse("20-01-2020")),
+ initialSelectionMode = SelectionMode.Period,
+ )
+ )
+ }
+
+```
+
+### State hoisting
+In case you need to react to the state changes, or change the state from the outside of the composable,
+you need to hoist the state out of the `Calendar` composable:
+
+```kotlin
+
+ @Composable
+ fun MainScreen() {
+ val calendarState = rememberCalendarState()
+ StaticCalendar(calendarState = calendarState)
+
+ // now you can manipulate the state from scope of this composable
+ calendarState.monthState.currentMonth = YearMonth.of(2020, 5)
+ }
+
+```
+
+### Dynamic Selection State
+By default, the `SelectableCalendar` is using a `DynamicSelectionState` implementation of `SelectionState`. The selection is kept as a list of `LocalDate` objects. For a purpose of flexibility, `DynamicSelectionState` allows for 4 different selection modes, each one varying how the selection is changing after interacting with the calendar. Furthermore, selection mode can be changed in the runtime, for some specific use-cases.
+Selection modes are represented by `SelectionMode` enum, with following values:
+- `None` - no selection allowed - selection will always be an empty list.
+- `Single` - only single day is selectable - selection will contain one or zero days selected.
+- `Multiple` - a list of dates can be selected.
+- `Period` - selectable period - implemented by `start` and `end` dates. - selection will contain all dates between start and the end date.
+This implementation of SelectionState also allows for handling side-effects and vetoing the state change via `confirmSelectionChange` callback.
+
+## Week Calendar
+Apart from the default calendar, there is also a week calendar, which shows a single week at a time. It can be used in the same way as the default calendar, and has the same customization options.
+
+## KotlinX DateTime
+As the core of the library is built on `java.time` library, on Android SDK < 26 it requires to use [core libary desugaring](https://developer.android.com/studio/write/java8-support) to access it's API.
+As a result it's features may be unavailable to some project built around different date-time libraries (e.g. kotlinx-datetime). Although the project wont be migrating from `java.time`, as it's the best suited for it, there is a separate `kotlinx-datetime` artifact for those who need to use the library from a codebase based on it. It doesn't consist of a separate version of `ComposeCalendar` features, but offers a small bunch of utilities, that will enable you to create your own wrapper, as briefly presented in `KotlinDateTimeSample`. If the provided functionality, doesn't match your use-case, please submit an issue.
+
+## License
+
+ Copyright 2024 Bogusz Pawłowski
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/ComposeCalendar/blob/screenshot_1.jpg b/ComposeCalendar/blob/screenshot_1.jpg
new file mode 100644
index 000000000..3bb37214f
Binary files /dev/null and b/ComposeCalendar/blob/screenshot_1.jpg differ
diff --git a/ComposeCalendar/build.gradle.kts b/ComposeCalendar/build.gradle.kts
new file mode 100644
index 000000000..057fb2192
--- /dev/null
+++ b/ComposeCalendar/build.gradle.kts
@@ -0,0 +1,115 @@
+import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
+import io.gitlab.arturbosch.detekt.Detekt
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+ id(DetektLib.PluginId) version DetektLib.Version
+ id(GradleVersions.PluginId) version GradleVersions.Version
+ id(GrGit.PluginId) version GrGit.Version
+ id(Shipkit.AutoVersion.PluginId) version Shipkit.AutoVersion.Version
+ id(Shipkit.Changelog.PluginId) version Shipkit.Changelog.Version
+ id(Shipkit.GithubRelease.PluginId) version Shipkit.GithubRelease.Version
+ id(Kotlin.CompatibilityPlugin) version Kotlin.CompatibilityPluginVersion
+}
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+ dependencies {
+ classpath(Android.GradlePlugin)
+ classpath(Kotlin.GradlePlugin)
+ classpath(Kotlin.DokkaGradlePlugin)
+ classpath(DetektLib.Plugin)
+ classpath(GradleVersions.Plugin)
+ classpath(MavenPublish.GradlePlugin)
+ classpath(Firebase.GoogleServicesPlugin)
+ }
+}
+
+allprojects {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ google()
+ mavenLocal()
+ }
+
+ tasks.withType {
+ kotlinOptions {
+ jvmTarget = "17"
+ freeCompilerArgs = freeCompilerArgs + listOf(
+ "-progressive",
+ "-Xopt-in=kotlin.RequiresOptIn",
+ "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
+ "-Xuse-experimental=kotlin.contracts.ExperimentalContracts",
+ )
+ }
+ }
+
+ tasks.withType {
+ useJUnitPlatform()
+ testLogging {
+ events("passed", "skipped", "failed")
+ }
+ }
+}
+
+dependencies {
+ detekt(DetektLib.Formatting)
+ detekt(DetektLib.Cli)
+}
+
+apiValidation {
+ ignoredProjects.add("sample")
+}
+
+tasks {
+ withType {
+ parallel = true
+ config.setFrom(rootProject.file("detekt-config.yml"))
+ setSource(files(projectDir))
+ exclude(subprojects.map { "${it.buildDir.relativeTo(rootDir).path}/" })
+ exclude("**/.gradle/**")
+ reports {
+ xml {
+ required.set(true)
+ outputLocation.set(file("build/reports/detekt/detekt-results.xml"))
+ }
+ html.required.set(false)
+ txt.required.set(false)
+ }
+ }
+
+ register("check") {
+ group = "Verification"
+ description = "Allows to attach Detekt to the root project."
+ }
+
+ withType {
+ rejectVersionIf {
+ isNonStable(candidate.version) && !isNonStable(currentVersion)
+ }
+ }
+
+ withType(org.shipkit.changelog.GenerateChangelogTask::class) {
+ previousRevision = project.ext["shipkit-auto-version.previous-tag"] as String?
+ githubToken = System.getenv("GITHUB_TOKEN")
+ repository = "boguszpawlowski/composecalendar"
+ }
+
+ withType(org.shipkit.github.release.GithubReleaseTask::class) {
+ dependsOn(named("generateChangelog"))
+ repository = "boguszpawlowski/composecalendar"
+ changelog = named("generateChangelog").get().outputs.files.singleFile
+ githubToken = System.getenv("GITHUB_TOKEN")
+ newTagRevision = System.getenv("GITHUB_SHA")
+ }
+}
+
+fun isNonStable(version: String): Boolean {
+ val regex = "^[0-9,.v-]+(-r)?$".toRegex()
+ return !regex.matches(version)
+}
diff --git a/ComposeCalendar/buildSrc/build.gradle.kts b/ComposeCalendar/buildSrc/build.gradle.kts
new file mode 100644
index 000000000..d8ab500d1
--- /dev/null
+++ b/ComposeCalendar/buildSrc/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+ google()
+}
+
+gradlePlugin {
+ plugins {
+ register("common-android-plugin") {
+ id = "common-android-plugin"
+ implementationClass = "CommonAndroidPlugin"
+ }
+ }
+}
+
+dependencies {
+ implementation("com.android.tools.build:gradle:8.2.0")
+ implementation(kotlin("gradle-plugin", "1.9.20"))
+}
diff --git a/ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt b/ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt
new file mode 100644
index 000000000..a4c3e2d1d
--- /dev/null
+++ b/ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt
@@ -0,0 +1,32 @@
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.JavaVersion.VERSION_17
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+
+class CommonAndroidPlugin : Plugin {
+
+ @Suppress("LongMethod")
+ override fun apply(target: Project) {
+ val androidExtension = target.extensions.getByName("android")
+
+ (androidExtension as? BaseExtension)?.apply {
+ compileSdkVersion(AndroidSdk.Compile)
+ defaultConfig {
+ minSdk = AndroidSdk.Min
+ targetSdk = AndroidSdk.Target
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility = VERSION_17
+ targetCompatibility = VERSION_17
+ }
+
+ buildFeatures.compose = true
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = Compose.CompilerVersion
+ }
+ }
+ }
+}
diff --git a/ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt b/ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt
new file mode 100644
index 000000000..16c4ae3cf
--- /dev/null
+++ b/ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt
@@ -0,0 +1,141 @@
+@file:Suppress("ObjectPropertyNaming", "ClassNaming", "UnderscoresInNumericLiterals")
+object MavenPublish {
+ const val PluginId = "com.vanniktech.maven.publish"
+ const val GradlePlugin = "com.vanniktech:gradle-maven-publish-plugin:0.27.0"
+}
+
+object AndroidSdk {
+ const val Min = 21
+ const val Compile = 33
+ const val Target = Compile
+}
+
+object Kotlin {
+ const val Version = "1.9.21"
+ const val CompatibilityPluginVersion = "0.13.2"
+
+ const val GradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$Version"
+ const val DokkaGradlePlugin = "org.jetbrains.dokka:dokka-gradle-plugin:1.9.10"
+
+ const val AndroidPluginId = "android"
+ const val JvmPluginId = "jvm"
+
+ const val CompatibilityPlugin = "org.jetbrains.kotlinx.binary-compatibility-validator"
+ const val CompatibilityPluginId = "binary-compatibility-validator"
+
+ const val DesugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.4"
+ const val DateTime = "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0"
+}
+
+object Android {
+ const val GradlePlugin = "com.android.tools.build:gradle:8.2.0"
+
+ const val ApplicationPluginId = "com.android.application"
+ const val LibraryPluginId = "com.android.library"
+}
+
+object GradleVersions {
+ const val Version = "0.42.0"
+
+ const val PluginId = "com.github.ben-manes.versions"
+ const val Plugin = "com.github.ben-manes:gradle-versions-plugin:$Version"
+}
+
+object GrGit {
+ const val Version = "5.0.0"
+
+ const val PluginId = "org.ajoberstar.grgit"
+}
+
+object Shipkit {
+ object AutoVersion {
+ const val Version = "1.1.19"
+
+ const val PluginId = "org.shipkit.shipkit-auto-version"
+ }
+
+ object Changelog {
+ const val Version = "1.1.15"
+
+ const val PluginId = "org.shipkit.shipkit-changelog"
+ }
+
+ object GithubRelease {
+ const val Version = "1.1.15"
+
+ const val PluginId = "org.shipkit.shipkit-github-release"
+ }
+}
+
+object AndroidX {
+ const val Version = "1.0.0"
+ const val LifecycleVersion = "2.2.0"
+
+ const val AppCompat = "androidx.appcompat:appcompat:1.6.1"
+ const val ComposeActivity = "androidx.activity:activity-compose:1.6.1"
+}
+
+object Material {
+ const val Core = "com.google.android.material:material:1.5.0"
+}
+
+object DetektLib {
+ const val Version = "1.23.4"
+
+ const val PluginId = "io.gitlab.arturbosch.detekt"
+ const val Plugin = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$Version"
+
+ const val Formatting = "io.gitlab.arturbosch.detekt:detekt-formatting:$Version"
+ const val Cli = "io.gitlab.arturbosch.detekt:detekt-cli:$Version"
+}
+
+object Timber {
+ const val Version = "5.0.1"
+ const val Core = "com.jakewharton.timber:timber:$Version"
+}
+
+object Compose {
+ const val Version = "1.3.1"
+ const val CompilerVersion = "1.5.5"
+ const val AccompanistVersion = "0.25.1"
+
+ const val Runtime = "androidx.compose.runtime:runtime:$Version"
+ const val Compiler = "androidx.compose.compiler:compiler:$CompilerVersion"
+ const val Foundation = "androidx.compose.foundation:foundation:$Version"
+ const val FoundationLayout = "androidx.compose.foundation:foundation-layout:$Version"
+ const val Material = "androidx.compose.material:material:$Version"
+ const val Ui = "androidx.compose.ui:ui:$Version"
+ const val UiTooling = "androidx.compose.ui:ui-tooling:$Version"
+ const val AccompanistPager = "com.google.accompanist:accompanist-pager:$AccompanistVersion"
+ const val Navigation = "androidx.navigation:navigation-compose:2.5.0-alpha03"
+}
+
+object Debug {
+ const val LeakCanary = "com.squareup.leakcanary:leakcanary-android:2.7"
+}
+
+object Hyperion {
+ private const val Version = "0.9.34"
+ const val Core = "com.willowtreeapps.hyperion:hyperion-core:$Version"
+ const val Crash = "com.willowtreeapps.hyperion:hyperion-crash:$Version"
+ const val GeigerCounter = "com.willowtreeapps.hyperion:hyperion-geiger-counter:$Version"
+ const val Measurement = "com.willowtreeapps.hyperion:hyperion-measurement:$Version"
+}
+
+object Kotest {
+ const val Version = "5.4.2"
+
+ const val RunnerJunit5 = "io.kotest:kotest-runner-junit5-jvm:$Version"
+
+ const val Assertions = "io.kotest:kotest-assertions-core-jvm:$Version"
+}
+
+object ComposeTest {
+ const val Core = "androidx.compose.ui:ui-test-junit4:${Compose.Version}"
+ const val Manifest = "androidx.compose.ui:ui-test-manifest:${Compose.Version}"
+}
+
+object Firebase {
+ const val Bom = "com.google.firebase:firebase-bom:30.0.0"
+ const val GoogleServicesPlugin = "com.google.gms:google-services:4.3.10"
+}
diff --git a/ComposeCalendar/detekt-config.yml b/ComposeCalendar/detekt-config.yml
new file mode 100644
index 000000000..92c2fccc9
--- /dev/null
+++ b/ComposeCalendar/detekt-config.yml
@@ -0,0 +1,294 @@
+build:
+ maxIssues: 0
+ weights:
+
+complexity:
+ active: true
+ ComplexCondition:
+ active: true
+ threshold: 3
+ ComplexInterface:
+ active: true
+ ComplexMethod:
+ active: true
+ threshold: 10
+ ignoreSingleWhenExpression: true
+ LargeClass:
+ active: true
+ threshold: 400
+ LongMethod:
+ active: true
+ threshold: 40
+ LongParameterList:
+ active: true
+ functionThreshold: 10
+ constructorThreshold: 5
+ ignoreAnnotated: [ 'Composable' ]
+ NestedBlockDepth:
+ active: true
+ StringLiteralDuplication:
+ active: false
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: true
+ methodNames: 'toString,hashCode,equals,finalize'
+ InstanceOfCheckForException:
+ active: true
+ NotImplementedDeclaration:
+ active: true
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ SwallowedException:
+ active: true
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ ThrowingNewInstanceOfSameException:
+ active: true
+
+formatting:
+ active: true
+ ChainWrapping:
+ active: true
+ CommentSpacing:
+ active: true
+ ImportOrdering:
+ active: true
+ Indentation:
+ active: true
+ indentSize: 2
+ continuationIndentSize: 4
+ NoBlankLineBeforeRbrace:
+ active: true
+ NoConsecutiveBlankLines:
+ active: true
+ NoLineBreakBeforeAssignment:
+ active: true
+ NoMultipleSpaces:
+ active: true
+ NoSemicolons:
+ active: true
+ NoTrailingSpaces:
+ active: true
+ ParameterListWrapping:
+ active: true
+ indentSize: 2
+ SpacingAroundColon:
+ active: true
+ SpacingAroundComma:
+ active: true
+ SpacingAroundCurly:
+ active: true
+ SpacingAroundKeyword:
+ active: true
+ SpacingAroundOperators:
+ active: true
+ SpacingAroundRangeOperator:
+ active: true
+ StringTemplate:
+ active: true
+
+naming:
+ active: true
+ ClassNaming:
+ active: true
+ classPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$'
+ ConstructorParameterNaming:
+ active: true
+ parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ privateParameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ EnumNaming:
+ active: true
+ enumEntryPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$'
+ FunctionNaming:
+ active: false
+ functionPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ ignoreAnnotated: [ 'Composable' ]
+ FunctionParameterNaming:
+ active: true
+ parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ MatchingDeclarationName:
+ active: true
+ MemberNameEqualsClassName:
+ active: true
+ ObjectPropertyNaming:
+ active: false
+ constantPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$'
+ propertyPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$'
+ privatePropertyPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$'
+ PackageNaming:
+ active: true
+ packagePattern: '^[a-z]+(\.[a-z][a-zA-Z]*)*$'
+ TopLevelPropertyNaming:
+ active: false
+ constantPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ propertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ VariableNaming:
+ active: true
+ variablePattern: '^[a-z]+(?:[A-Z][a-z]+)*$'
+ privateVariablePattern: '^[_a-z]+(?:[A-Z][a-z]+)*$'
+
+performance:
+ active: true
+ ForEachOnRange:
+ active: true
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ DuplicateCaseInWhenExpression:
+ active: true
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: false
+ EqualsWithHashCodeExist:
+ active: true
+ ExplicitGarbageCollectionCall:
+ active: true
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ MissingWhenCase:
+ active: true
+ RedundantElseInWhen:
+ active: true
+ UnconditionalJumpStatementInLoop:
+ active: true
+ UnreachableCode:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: true
+ CollapsibleIfStatements:
+ active: true
+ DataClassShouldBeImmutable:
+ active: true
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: true
+ ExplicitItLambdaParameter:
+ active: true
+ ForbiddenVoid:
+ active: true
+ LibraryCodeMustSpecifyReturnType:
+ active: true
+ LoopWithTooManyJumpStatements:
+ active: false
+ maxJumpCount: 1
+ MaxLineLength:
+ active: true
+ maxLineLength: 120
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: true
+ MayBeConst:
+ active: true
+ ModifierOrder:
+ active: true
+ NewLineAtEndOfFile:
+ active: true
+ NoTabs:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalWhenBraces:
+ active: true
+ PreferToOverPairSyntax:
+ active: true
+ ProtectedMemberInFinalClass:
+ active: true
+ RedundantVisibilityModifierRule:
+ active: true
+ excludes: ['**/library/**', '**/kotlinx-datetime/**']
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: true
+ SpacingBetweenPackageAndImports:
+ active: true
+ TrailingWhitespace:
+ active: true
+ UnderscoresInNumericLiterals:
+ active: false
+ acceptableDecimalLength: 3
+ UnnecessaryAbstractClass:
+ active: true
+ UnnecessaryApply:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryLet:
+ active: true
+ UnnecessaryParentheses:
+ active: true
+ UntilInsteadOfRangeTo:
+ active: true
+ UnusedImports:
+ active: true
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: false # Due to compose preview
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckOrError:
+ active: true
+ UseIfInsteadOfWhen:
+ active: false
+ UseRequire:
+ active: true
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ WildcardImport:
+ active: true
diff --git a/ComposeCalendar/gradle.properties b/ComposeCalendar/gradle.properties
new file mode 100644
index 000000000..269fd8b11
--- /dev/null
+++ b/ComposeCalendar/gradle.properties
@@ -0,0 +1,27 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+android.enableJetifier=true
+kotlin.code.style=official
+
+POM_NAME=Compose Calendar
+GROUP=io.github.boguszpawlowski.composecalendar
+POM_URL=https://github.com/boguszpawlowski/ComposeCalendar
+POM_SCM_URL=https://github.com/boguszpawlowski/ComposeCalendar
+POM_SCM_CONNECTION=git@github.com:boguszpawlowski/ComposeCalendar.git
+POM_SCM_DEV_CONNECTION=git@github.com:boguszpawlowski/ComposeCalendar.git
+POM_LICENCE_NAME=The Apache Software License, Version 2.0
+POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENCE_DIST=repo
+POM_DEVELOPER_ID=boguszp
+POM_DEVELOPER_NAME=Bogusz Pawlowski
+android.defaults.buildfeatures.buildconfig=true
+android.nonTransitiveRClass=false
+android.nonFinalResIds=false
diff --git a/ComposeCalendar/gradle/dependencies.gradle b/ComposeCalendar/gradle/dependencies.gradle
new file mode 100644
index 000000000..e69de29bb
diff --git a/ComposeCalendar/gradle/wrapper/gradle-wrapper.jar b/ComposeCalendar/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..d64cd4917
Binary files /dev/null and b/ComposeCalendar/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/ComposeCalendar/gradle/wrapper/gradle-wrapper.properties b/ComposeCalendar/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..1af9e0930
--- /dev/null
+++ b/ComposeCalendar/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/ComposeCalendar/gradlew b/ComposeCalendar/gradlew
new file mode 100644
index 000000000..1aa94a426
--- /dev/null
+++ b/ComposeCalendar/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/ComposeCalendar/gradlew.bat b/ComposeCalendar/gradlew.bat
new file mode 100644
index 000000000..93e3f59f1
--- /dev/null
+++ b/ComposeCalendar/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/ComposeCalendar/kotlinx-datetime/.gitignore b/ComposeCalendar/kotlinx-datetime/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api b/ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api
new file mode 100644
index 000000000..c5e8ba1a5
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api
@@ -0,0 +1,37 @@
+public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/ConvertersKt {
+ public static final fun toJavaYearMonth (Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;)Ljava/time/YearMonth;
+ public static final fun toKotlinYearMonth (Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+}
+
+public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensionsKt {
+ public static final fun getFirstDayOfWeek (Ljava/util/Locale;)Ljava/time/DayOfWeek;
+ public static final fun now (Lkotlinx/datetime/LocalDate$Companion;)Lkotlinx/datetime/LocalDate;
+}
+
+public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth {
+ public static final field Companion Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth$Companion;
+ public synthetic fun (ILjava/time/Month;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun component1 ()I
+ public final fun component2 ()Ljava/time/Month;
+ public final fun copy (ILjava/time/Month;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;ILjava/time/Month;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun dec ()Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getMonth ()Ljava/time/Month;
+ public final fun getYear ()I
+ public fun hashCode ()I
+ public final fun inc ()Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun minus (ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun minus (JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun plus (ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun plus (JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth$Companion {
+ public final fun now ()Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun of (II)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun of (ILjava/time/Month;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+ public final fun parse (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;
+}
+
diff --git a/ComposeCalendar/kotlinx-datetime/build.gradle.kts b/ComposeCalendar/kotlinx-datetime/build.gradle.kts
new file mode 100644
index 000000000..4c35546f9
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+ kotlin(Kotlin.JvmPluginId)
+ id(MavenPublish.PluginId)
+}
+
+kotlin {
+ explicitApi()
+}
+
+dependencies {
+ api(Kotlin.DateTime)
+
+ testImplementation(Kotest.Assertions)
+ testImplementation(Kotest.RunnerJunit5)
+}
diff --git a/ComposeCalendar/kotlinx-datetime/gradle.properties b/ComposeCalendar/kotlinx-datetime/gradle.properties
new file mode 100644
index 000000000..5b55af44f
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/gradle.properties
@@ -0,0 +1,5 @@
+POM_ARTIFACT_ID=kotlinx-datetime
+POM_DESCRIPTION=A set of utilities supporting usage of kotlin datetime instead of java.time.
+POM_PACKAGING=aar
+SONATYPE_HOST=S01
+RELEASE_SIGNING_ENABLED=true
diff --git a/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt
new file mode 100644
index 000000000..04f2cc336
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt
@@ -0,0 +1,5 @@
+package io.github.boguszpawlowski.composecalendar.kotlinxDateTime
+
+public fun YearMonth.toJavaYearMonth(): java.time.YearMonth = java.time.YearMonth.of(year, month)
+
+public fun java.time.YearMonth.toKotlinYearMonth(): YearMonth = YearMonth.of(year, month)
diff --git a/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt
new file mode 100644
index 000000000..7887914b4
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt
@@ -0,0 +1,15 @@
+package io.github.boguszpawlowski.composecalendar.kotlinxDateTime
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.DayOfWeek
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
+import java.time.temporal.WeekFields
+import java.util.Locale
+
+public fun LocalDate.Companion.now(): LocalDate =
+ Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
+
+public val Locale.firstDayOfWeek: DayOfWeek
+ get() = WeekFields.of(this).firstDayOfWeek
diff --git a/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt
new file mode 100644
index 000000000..eb2a8e8b9
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt
@@ -0,0 +1,79 @@
+package io.github.boguszpawlowski.composecalendar.kotlinxDateTime
+
+import kotlinx.datetime.DateTimeUnit
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.Month
+import java.time.temporal.ChronoField.YEAR
+
+/**
+ * Kotlin implementation of `YearMonth` available in java.time, since `KotlinxDateTime` doesn't
+ * include a substitute for it: https://github.com/Kotlin/kotlinx-datetime/issues/168
+ *
+ * For construction use factory methods 'of' provided in companion object
+ */
+@Suppress("DataClassPrivateConstructor")
+public data class YearMonth private constructor(
+ val year: Int,
+ val month: Month,
+) {
+
+ /**
+ * Increment by one month
+ */
+ public operator fun inc(): YearMonth = plus(1, DateTimeUnit.MONTH)
+
+ /**
+ * Decrement by one month
+ */
+ public operator fun dec(): YearMonth = minus(1, DateTimeUnit.MONTH)
+
+ /**
+ * Add specified amount of months to current date
+ */
+ public fun plus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth =
+ plus(value.toLong(), unit)
+
+ /**
+ * Subtract specified amount of months from current date
+ */
+ public fun minus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth =
+ plus(-value.toLong(), unit)
+
+ public fun minus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth =
+ plus(-value, unit)
+
+ public fun plus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth {
+ val monthsToAdd = value * unit.months
+ if (monthsToAdd == 0L) {
+ return this
+ }
+ val monthCount = year * 12L + (month.value - 1)
+ val calcMonths = monthCount + monthsToAdd
+
+ val newYear = YEAR.checkValidIntValue(calcMonths.floorDiv(12))
+ val newMonth = calcMonths.floorMod(12) + 1
+ return of(newYear, newMonth.toInt())
+ }
+
+ override fun toString(): String = toJavaYearMonth().toString()
+
+ public companion object {
+ public fun of(year: Int, month: Int): YearMonth = YearMonth(year, Month.of(month))
+
+ public fun of(year: Int, month: Month): YearMonth = YearMonth(year, month)
+
+ public fun parse(value: String): YearMonth =
+ java.time.YearMonth.parse(value).toKotlinYearMonth()
+
+ public fun now(): YearMonth {
+ val today = LocalDate.now()
+
+ return of(today.year, today.month.value)
+ }
+ }
+}
+
+private fun Long.floorMod(other: Long): Long = when (other) {
+ 0L -> this
+ else -> this - floorDiv(other) * other
+}
diff --git a/ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt b/ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt
new file mode 100644
index 000000000..6de0bf6b8
--- /dev/null
+++ b/ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt
@@ -0,0 +1,128 @@
+package io.github.boguszpawlowski.composecalendar.kotlinxDateTime
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.core.spec.style.ShouldSpec
+import io.kotest.data.Headers3
+import io.kotest.data.Row3
+import io.kotest.data.forAll
+import io.kotest.data.table
+import io.kotest.matchers.shouldBe
+import kotlinx.datetime.DateTimeUnit
+import java.time.DateTimeException
+
+internal class YearMonthTest : ShouldSpec({
+
+ context("Year Month") {
+ should("overflow a year if adding months") {
+ val yearMonth = YearMonth.of(2002, 11)
+ val expectedResult = YearMonth.of(2003, 1)
+
+ val result = yearMonth.plus(2, DateTimeUnit.MONTH)
+
+ result shouldBe expectedResult
+ }
+ should("add years if adding months") {
+ val yearMonth = YearMonth.of(2002, 11)
+ val expectedResult = YearMonth.of(2202, 11)
+
+ val result = yearMonth.plus(2, DateTimeUnit.CENTURY)
+
+ result shouldBe expectedResult
+ }
+ should("subtract years if subtracting months") {
+ val yearMonth = YearMonth.of(2002, 1)
+ val expectedResult = YearMonth.of(2001, 11)
+
+ val result = yearMonth.minus(2, DateTimeUnit.MONTH)
+
+ result shouldBe expectedResult
+ }
+ should("subtract year if subtracting months") {
+ val yearMonth = YearMonth.of(2002, 11)
+ val expectedResult = YearMonth.of(2000, 11)
+
+ val result = yearMonth.minus(2, DateTimeUnit.YEAR)
+
+ result shouldBe expectedResult
+ }
+ should("throw an error if months greater than 12") {
+ shouldThrow {
+ YearMonth.of(1222, 13)
+ }
+ }
+ should("correctly add months") {
+ forAll(
+ table(
+ Headers3("starting month", "months to add", "expected month"),
+ Row3(1, 2, 3),
+ Row3(12, 24, 12),
+ Row3(11, 2, 1),
+ Row3(1, 14, 3),
+ Row3(6, 7, 1),
+ Row3(10, 20, 6),
+ )
+ ) { startingMonth: Int, monthsToAdd: Int, expectedMonth: Int ->
+ val yearMonth = YearMonth.of(2000, startingMonth)
+
+ val result = yearMonth.plus(monthsToAdd, DateTimeUnit.MONTH).month.value
+
+ result shouldBe expectedMonth
+ }
+ }
+ should("correctly add years") {
+ forAll(
+ table(
+ Headers3("starting month", "months to add", "expected year"),
+ Row3(1, 2, 2000),
+ Row3(12, 24, 2002),
+ Row3(11, 2, 2001),
+ Row3(1, 14, 2001),
+ Row3(6, 7, 2001),
+ Row3(10, 20, 2002),
+ )
+ ) { startingMonth: Int, monthsToAdd: Int, expectedYear: Int ->
+ val yearMonth = YearMonth.of(2000, startingMonth)
+
+ val result = yearMonth.plus(monthsToAdd, DateTimeUnit.MONTH).year
+
+ result shouldBe expectedYear
+ }
+ }
+ context("when decrementing") {
+ should("subtract one month") {
+ val yearMonth = YearMonth.of(2002, 11)
+ val expectedResult = YearMonth.of(2002, 10)
+
+ val result = yearMonth.dec()
+
+ result shouldBe expectedResult
+ }
+ should("overflow the year when january") {
+ val yearMonth = YearMonth.of(2002, 1)
+ val expectedResult = YearMonth.of(2001, 12)
+
+ val result = yearMonth.dec()
+
+ result shouldBe expectedResult
+ }
+ }
+ context("when incrementing") {
+ should("add one month") {
+ val yearMonth = YearMonth.of(2002, 11)
+ val expectedResult = YearMonth.of(2002, 12)
+
+ val result = yearMonth.inc()
+
+ result shouldBe expectedResult
+ }
+ should("overflow the year when december") {
+ val yearMonth = YearMonth.of(2002, 12)
+ val expectedResult = YearMonth.of(2003, 1)
+
+ val result = yearMonth.inc()
+
+ result shouldBe expectedResult
+ }
+ }
+ }
+})
diff --git a/ComposeCalendar/library/.gitignore b/ComposeCalendar/library/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/ComposeCalendar/library/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/ComposeCalendar/library/api/library.api b/ComposeCalendar/library/api/library.api
new file mode 100644
index 000000000..9c12ac395
--- /dev/null
+++ b/ComposeCalendar/library/api/library.api
@@ -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 ()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 (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 ()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 ()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 (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 (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 ()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 ()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 (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
+ public synthetic fun (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 (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
+}
+
diff --git a/ComposeCalendar/library/build.gradle.kts b/ComposeCalendar/library/build.gradle.kts
new file mode 100644
index 000000000..864e35c15
--- /dev/null
+++ b/ComposeCalendar/library/build.gradle.kts
@@ -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 {
+ 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)
+}
diff --git a/ComposeCalendar/library/gradle.properties b/ComposeCalendar/library/gradle.properties
new file mode 100644
index 000000000..9a193f6c3
--- /dev/null
+++ b/ComposeCalendar/library/gradle.properties
@@ -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
diff --git a/ComposeCalendar/library/src/main/AndroidManifest.xml b/ComposeCalendar/library/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..8bdb7e14b
--- /dev/null
+++ b/ComposeCalendar/library/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt
new file mode 100644
index 000000000..b8f335b00
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt
@@ -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(
+ 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 = rememberSelectableCalendarState(),
+ dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) },
+ monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 = rememberCalendarState(),
+ dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) },
+ monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 Calendar(
+ calendarState: CalendarState,
+ 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) -> Unit = { DefaultDay(it) },
+ monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 = emptyList(),
+ initialSelectionMode: SelectionMode = SelectionMode.Single,
+ confirmSelectionChange: (newValue: List) -> 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 = 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 = remember { CalendarState(monthState, EmptySelectionState) }
+
+internal const val DefaultCalendarPagerRange = 10_000L
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt
new file mode 100644
index 000000000..28da35a51
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt
@@ -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(
+ 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 = rememberSelectableWeekCalendarState(),
+ dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) },
+ weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
+ DefaultProperWeekHeader(it)
+ },
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 = rememberWeekCalendarState(),
+ weekDaysScrollEnabled: Boolean = true,
+ dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) },
+ weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
+ DefaultProperWeekHeader(it)
+ },
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 WeekCalendar(
+ calendarState: WeekCalendarState,
+ modifier: Modifier = Modifier,
+ today: LocalDate = LocalDate.now(),
+ firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
+ horizontalSwipeEnabled: Boolean = true,
+ weekDaysScrollEnabled: Boolean = true,
+ dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) },
+ weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
+ DefaultProperWeekHeader(it)
+ },
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 = emptyList(),
+ initialSelectionMode: SelectionMode = SelectionMode.Single,
+ confirmSelectionChange: (newValue: List) -> 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 =
+ 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 =
+ remember { WeekCalendarState(weekState, EmptySelectionState) }
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt
new file mode 100644
index 000000000..e476ce8b7
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt
@@ -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
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt
new file mode 100644
index 000000000..a10a29967
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt
@@ -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
+
+/**
+ * Contains information about current selection as well as date of rendered day
+ */
+@Stable
+public data class DayState(
+ private val day: Day,
+ val selectionState: T,
+) : Day by day
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt
new file mode 100644
index 000000000..2afd0d4ba
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt
@@ -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 DefaultDay(
+ state: DayState,
+ 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())
+ }
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt
new file mode 100644
index 000000000..7ac0002e9
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt
@@ -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
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt
new file mode 100644
index 000000000..61b27449c
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt
@@ -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
+ )
+ }
+ }
+ }
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt
new file mode 100644
index 000000000..653587cee
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt
@@ -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",
+ )
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt
new file mode 100644
index 000000000..cb99137b2
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt
@@ -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",
+ )
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt
new file mode 100644
index 000000000..293479896
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt
@@ -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 = 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
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt
new file mode 100644
index 000000000..f64182a4d
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt
@@ -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 = 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
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt
new file mode 100644
index 000000000..2d02ed18c
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt
@@ -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 MonthPager(
+ initialMonth: YearMonth,
+ showAdjacentMonths: Boolean,
+ selectionState: T,
+ monthState: MonthState,
+ daysOfWeek: List,
+ today: LocalDate,
+ modifier: Modifier = Modifier,
+ weekDaysScrollEnabled: Boolean = true,
+ dayContent: @Composable BoxScope.(DayState) -> Unit,
+ weekHeader: @Composable BoxScope.(List) -> 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 MonthContent(
+ showAdjacentMonths: Boolean,
+ selectionState: T,
+ currentMonth: YearMonth,
+ daysOfWeek: List,
+ today: LocalDate,
+ modifier: Modifier = Modifier,
+ weekDaysScrollEnabled: Boolean = true,
+ dayContent: @Composable BoxScope.(DayState) -> Unit,
+ weekHeader: @Composable BoxScope.(List) -> 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)
+ }
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt
new file mode 100644
index 000000000..d85fe2448
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt
@@ -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)
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt
new file mode 100644
index 000000000..d3f244e36
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt
@@ -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,
+ selectionMode: SelectionMode,
+ ): List = 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
+ }
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt
new file mode 100644
index 000000000..0fdb7060d
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt
@@ -0,0 +1,15 @@
+package io.github.boguszpawlowski.composecalendar.selection
+
+import android.annotation.SuppressLint
+import java.time.LocalDate
+
+@SuppressLint("NewApi")
+internal fun Collection.startOrMax() = firstOrNull() ?: LocalDate.MAX
+
+internal fun Collection.endOrNull() = drop(1).lastOrNull()
+
+@SuppressLint("NewApi")
+internal fun Collection.fillUpTo(date: LocalDate) =
+ (0..date.toEpochDay() - first().toEpochDay()).map {
+ first().plusDays(it)
+ }
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt
new file mode 100644
index 000000000..7a1aeee3b
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt
@@ -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,
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt
new file mode 100644
index 000000000..79448f5a7
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt
@@ -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) -> Boolean = { true },
+ selection: List,
+ selectionMode: SelectionMode,
+) : SelectionState {
+
+ private var _selection by mutableStateOf(selection)
+ private var _selectionMode by mutableStateOf(selectionMode)
+
+ public var selection: List
+ 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) -> Boolean,
+ ): Saver =
+ 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)?.map { LocalDate.parse(it) }.orEmpty(),
+ )
+ }
+ )
+ }
+}
+
+@Immutable
+public object EmptySelectionState : SelectionState {
+ override fun isDateSelected(date: LocalDate): Boolean = false
+
+ override fun onDateSelected(date: LocalDate): Unit = Unit
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt
new file mode 100644
index 000000000..76154cf8d
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt
@@ -0,0 +1,13 @@
+package io.github.boguszpawlowski.composecalendar.util
+
+import java.time.DayOfWeek
+import java.time.LocalDate
+
+internal fun Collection.addOrRemoveIfExists(date: LocalDate) =
+ if (contains(date)) {
+ this - date
+ } else {
+ this + date
+ }
+
+internal infix fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (value - other.value)) % 7
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt
new file mode 100644
index 000000000..07c576d95
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt
@@ -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 Flow.throttleOnOffset() =
+ combine(
+ snapshotFlow { firstVisibleItemScrollOffset }
+ ) { newMonth, offset ->
+ newMonth to (offset <= MinimalOffsetForEmit)
+ }.filter { (_, shouldUpdate) ->
+ shouldUpdate
+ }.map { (newValue, _) -> newValue }
+
+private const val MinimalOffsetForEmit = 10
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt
new file mode 100644
index 000000000..f3479b044
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt
@@ -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)
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt
new file mode 100644
index 000000000..b5d7cdb8f
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt
@@ -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,
+ 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,
+ 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 Array.rotateRight(n: Int): List = takeLast(n) + dropLast(n)
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt
new file mode 100644
index 000000000..815cedc26
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt
@@ -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,
+) {
+
+ 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()
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt
new file mode 100644
index 000000000..374e175d5
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt
@@ -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,
+)
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt
new file mode 100644
index 000000000..3dbc66da2
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt
@@ -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
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt
new file mode 100644
index 000000000..5354d0e63
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt
@@ -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 WeekPager(
+ initialWeek: Week,
+ selectionState: T,
+ weekState: WeekState,
+ daysOfWeek: List,
+ today: LocalDate,
+ modifier: Modifier = Modifier,
+ weekDaysScrollEnabled: Boolean = true,
+ dayContent: @Composable BoxScope.(DayState) -> Unit,
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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 WeekContent(
+ selectionState: T,
+ weekDays: WeekDays,
+ daysOfWeek: List,
+ modifier: Modifier = Modifier,
+ weekDaysScrollEnabled: Boolean = true,
+ dayContent: @Composable BoxScope.(DayState) -> Unit,
+ daysOfWeekHeader: @Composable BoxScope.(List) -> 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,
+ )
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt
new file mode 100644
index 000000000..61881e178
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt
@@ -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 WeekRow(
+ weekDays: WeekDays,
+ selectionState: T,
+ modifier: Modifier = Modifier,
+ dayContent: @Composable BoxScope.(DayState) -> 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))
+ }
+ }
+ }
+}
diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt
new file mode 100644
index 000000000..e926e14e8
--- /dev/null
+++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt
@@ -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 {
+ 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),
+ )
+ }
+ )
+ }
+}
diff --git a/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt
new file mode 100644
index 000000000..75ca49c09
--- /dev/null
+++ b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt
@@ -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)
+ }
+ }
+})
diff --git a/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt
new file mode 100644
index 000000000..c59c4fcf6
--- /dev/null
+++ b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt
@@ -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 {
+ Week(List(18) { LocalDate.now() })
+ }
+ }
+ }
+})
diff --git a/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt
new file mode 100644
index 000000000..17ce731f0
--- /dev/null
+++ b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt
@@ -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
diff --git a/ComposeCalendar/sample/.gitignore b/ComposeCalendar/sample/.gitignore
new file mode 100644
index 000000000..a2def884f
--- /dev/null
+++ b/ComposeCalendar/sample/.gitignore
@@ -0,0 +1,2 @@
+/build
+google-services.json
diff --git a/ComposeCalendar/sample/build.gradle.kts b/ComposeCalendar/sample/build.gradle.kts
new file mode 100644
index 000000000..7734761e4
--- /dev/null
+++ b/ComposeCalendar/sample/build.gradle.kts
@@ -0,0 +1,60 @@
+import org.ajoberstar.grgit.Grgit
+
+plugins {
+ id(Android.ApplicationPluginId)
+ kotlin(Kotlin.AndroidPluginId)
+ id("common-android-plugin")
+}
+
+val commitsCount = Grgit.open(mapOf("dir" to rootDir)).log().size
+
+android {
+ namespace = "io.github.boguszpawlowski.composecalendar.sample"
+ defaultConfig {
+ versionCode = commitsCount
+ versionName = "0.0.1"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ packaging {
+ resources {
+ excludes.addAll(listOf("META-INF/AL2.0", "META-INF/LGPL2.1"))
+ }
+ }
+
+// compileOptions {
+// isCoreLibraryDesugaringEnabled = true
+// }
+}
+
+dependencies {
+// coreLibraryDesugaring(Kotlin.DesugarJdkLibs)
+
+// implementation(project(":library"))
+ implementation(project(":kotlinx-datetime"))
+
+ implementation("io.github.boguszpawlowski.composecalendar:composecalendar:1.3.0")
+
+ implementation(Material.Core)
+
+ implementation(AndroidX.AppCompat)
+ implementation(AndroidX.ComposeActivity)
+
+ implementation(Compose.Runtime)
+ implementation(Compose.Navigation)
+ implementation(Compose.Ui)
+ implementation(Compose.UiTooling)
+ implementation(Compose.Foundation)
+ implementation(Compose.FoundationLayout)
+ implementation(Compose.Material)
+ implementation(Timber.Core)
+
+ debugImplementation(Debug.LeakCanary)
+ debugImplementation(Hyperion.Core)
+ debugImplementation(Hyperion.Crash)
+ debugImplementation(Hyperion.GeigerCounter)
+ debugImplementation(Hyperion.Measurement)
+ debugImplementation(ComposeTest.Manifest)
+
+ androidTestImplementation(ComposeTest.Core)
+}
diff --git a/ComposeCalendar/sample/proguard-rules.pro b/ComposeCalendar/sample/proguard-rules.pro
new file mode 100644
index 000000000..2f9dc5a47
--- /dev/null
+++ b/ComposeCalendar/sample/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.kts.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt b/ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt
new file mode 100644
index 000000000..b468e3fea
--- /dev/null
+++ b/ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt
@@ -0,0 +1,50 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeLeft
+import androidx.compose.ui.test.swipeRight
+import io.github.boguszpawlowski.composecalendar.StaticCalendar
+import io.github.boguszpawlowski.composecalendar.rememberCalendarState
+import org.junit.Rule
+import org.junit.Test
+import java.time.YearMonth
+
+internal class ScrollBehaviorTest {
+
+ private val initialMonth = YearMonth.of(2012, 5)
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ @Test
+ fun monthChangeTest() {
+ composeTestRule.setContent {
+ StaticCalendar(
+ calendarState = rememberCalendarState(initialMonth = initialMonth)
+ )
+ }
+
+ composeTestRule.onNodeWithTag("Decrement").performClick()
+ composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("April")
+ composeTestRule.onNodeWithTag("Increment").performClick()
+ composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("May")
+ }
+
+ @Test
+ fun scrollMonthChangeTest() {
+ composeTestRule.setContent {
+ StaticCalendar(
+ calendarState = rememberCalendarState(initialMonth = initialMonth)
+ )
+ }
+
+ composeTestRule.onNodeWithTag("MonthPager").performTouchInput { swipeLeft() }
+ composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("June")
+ composeTestRule.onNodeWithTag("MonthPager").performTouchInput { swipeRight() }
+ composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("May")
+ }
+}
diff --git a/ComposeCalendar/sample/src/main/AndroidManifest.xml b/ComposeCalendar/sample/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..67df51341
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/AndroidManifest.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt
new file mode 100644
index 000000000..4a77f4907
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt
@@ -0,0 +1,104 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.IconButton
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import io.github.boguszpawlowski.composecalendar.StaticCalendar
+import io.github.boguszpawlowski.composecalendar.day.NonSelectableDayState
+import io.github.boguszpawlowski.composecalendar.header.MonthState
+import java.time.DayOfWeek
+import java.time.DayOfWeek.SUNDAY
+import java.time.format.TextStyle.NARROW
+import java.util.Locale
+
+@Composable
+fun CustomComponentsSample() {
+ StaticCalendar(
+ modifier = Modifier.animateContentSize(),
+ showAdjacentMonths = false,
+ firstDayOfWeek = SUNDAY,
+ monthContainer = { MonthContainer(it) },
+ dayContent = { DayContent(dayState = it) },
+ daysOfWeekHeader = { WeekHeader(daysOfWeek = it) },
+ monthHeader = { MonthHeader(monthState = it) },
+ )
+}
+
+@Composable
+private fun DayContent(dayState: NonSelectableDayState) {
+ Text(
+ text = dayState.date.dayOfMonth.toString(),
+ modifier = Modifier.fillMaxWidth(),
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.h6,
+ )
+}
+
+@Composable
+private fun WeekHeader(daysOfWeek: List) {
+ Row {
+ daysOfWeek.forEach { dayOfWeek ->
+ Text(
+ textAlign = TextAlign.Center,
+ text = dayOfWeek.getDisplayName(NARROW, Locale.ROOT),
+ modifier = Modifier
+ .weight(1f)
+ .wrapContentHeight()
+ )
+ }
+ }
+}
+
+@Composable
+private fun MonthHeader(monthState: MonthState) {
+ Row {
+ Text(monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h3)
+ Text(monthState.currentMonth.month.name, style = MaterialTheme.typography.h3)
+ IconButton(onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) }) {
+ Image(
+ imageVector = Icons.Default.Star,
+ colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
+ contentDescription = "Next",
+ )
+ }
+ }
+}
+
+@Composable
+private fun MonthContainer(content: @Composable (PaddingValues) -> Unit) {
+ Card(
+ elevation = 0.dp,
+ shape = RoundedCornerShape(10.dp),
+ border = BorderStroke(1.dp, Color.LightGray),
+ content = { content(PaddingValues(4.dp)) },
+ )
+}
+
+@Preview
+@Composable
+private fun CustomComponentsPreview() {
+ MaterialTheme {
+ Surface {
+ CustomComponentsSample()
+ }
+ }
+}
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt
new file mode 100644
index 000000000..47bbe09a9
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt
@@ -0,0 +1,66 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import io.github.boguszpawlowski.composecalendar.Calendar
+import io.github.boguszpawlowski.composecalendar.CalendarState
+import io.github.boguszpawlowski.composecalendar.header.MonthState
+import io.github.boguszpawlowski.composecalendar.selection.SelectionState
+import java.time.LocalDate
+import java.time.YearMonth
+
+@Composable
+fun CustomSelectionSample() {
+ Calendar(calendarState = rememberMonthSelectionState())
+}
+
+private class MonthSelectionState(
+ initialSelection: YearMonth? = null,
+) : SelectionState {
+
+ private var selection by mutableStateOf(initialSelection)
+
+ override fun isDateSelected(date: LocalDate): Boolean =
+ date.yearMonth == selection
+
+ override fun onDateSelected(date: LocalDate) {
+ selection = if (date.yearMonth == selection) null else date.yearMonth
+ }
+
+ companion object {
+ @Suppress("FunctionName") // Factory function
+ fun Saver(): Saver = Saver(
+ save = { it.selection?.toString() },
+ restore = { restored ->
+ val selection = (restored as? String)?.let { YearMonth.parse(it) }
+ MonthSelectionState(selection)
+ }
+ )
+ }
+}
+
+@Composable
+private fun rememberMonthSelectionState(
+ initialMonth: YearMonth = YearMonth.now(),
+ minMonth: YearMonth = initialMonth.minusMonths(10000),
+ maxMonth: YearMonth = initialMonth.plusMonths(10000),
+ initialSelection: YearMonth? = null,
+ monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
+ MonthState(
+ initialMonth = initialMonth,
+ minMonth = minMonth,
+ maxMonth = maxMonth
+ )
+ },
+ selectionState: MonthSelectionState = rememberSaveable(saver = MonthSelectionState.Saver()) {
+ MonthSelectionState(initialSelection = initialSelection)
+ }
+): CalendarState = remember { CalendarState(monthState, selectionState) }
+
+private val LocalDate.yearMonth: YearMonth
+ get() = YearMonth.of(year, month)
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt
new file mode 100644
index 000000000..bdbc3bef1
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt
@@ -0,0 +1,103 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import io.github.boguszpawlowski.composecalendar.SelectableCalendar
+import io.github.boguszpawlowski.composecalendar.kotlinxDateTime.now
+import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState
+import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
+import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple
+import io.github.boguszpawlowski.composecalendar.selection.SelectionState
+import kotlinx.datetime.LocalDate
+import kotlinx.datetime.toJavaLocalDate
+import kotlinx.datetime.toKotlinLocalDate
+
+@Composable
+fun KotlinXDateTimeSample() {
+ var selection by remember { mutableStateOf(emptyList()) }
+
+ Column {
+ DateTimeCalendar(
+ today = LocalDate.now(),
+ onSelectionChanged = { selection = it },
+ dayContent = { DayContent(dayState = it) },
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = "Selection: ${selection.joinToString { it.toString() }}",
+ style = MaterialTheme.typography.h6,
+ )
+ }
+}
+
+@Composable
+fun BoxScope.DayContent(
+ dayState: KotlinDayState,
+) {
+ val isSelected = dayState.selectionState.isDateSelected(dayState.date)
+
+ Text(
+ text = dayState.date.dayOfMonth.toString(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.Center)
+ .aspectRatio(1f)
+ .clickable {
+ dayState.selectionState.onDateSelected(dayState.date)
+ },
+ color = if (isSelected) Color.Red else Color.Unspecified,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.h6,
+ )
+}
+
+@Composable
+fun DateTimeCalendar(
+ today: LocalDate,
+ onSelectionChanged: (List) -> Unit,
+ dayContent: @Composable BoxScope.(KotlinDayState) -> Unit,
+) {
+ SelectableCalendar(
+ calendarState = rememberSelectableCalendarState(
+ confirmSelectionChange = { selection -> onSelectionChanged(selection.map { it.toKotlinLocalDate() }); true },
+ initialSelectionMode = Multiple,
+ ),
+ today = today.toJavaLocalDate(),
+ showAdjacentMonths = false,
+ dayContent = { dayState ->
+ dayContent(
+ KotlinDayState(
+ date = dayState.date.toKotlinLocalDate(),
+ isCurrentDay = dayState.isCurrentDay,
+ selectionState = dayState.selectionState,
+ )
+ )
+ }
+ )
+}
+
+data class KotlinDayState(
+ val date: LocalDate,
+ val isCurrentDay: Boolean,
+ val selectionState: T,
+)
+
+private fun SelectionState.isDateSelected(date: LocalDate) = isDateSelected(date.toJavaLocalDate())
+private fun SelectionState.onDateSelected(date: LocalDate) = onDateSelected(date.toJavaLocalDate())
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt
new file mode 100644
index 000000000..2a883777b
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt
@@ -0,0 +1,113 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material.darkColors
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import io.github.boguszpawlowski.composecalendar.BuildConfig
+import timber.log.Timber
+import timber.log.Timber.DebugTree
+
+class MainActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (BuildConfig.DEBUG) {
+ Timber.plant(DebugTree())
+ }
+ setContent {
+ MainScreen()
+ }
+ }
+}
+
+@Composable
+fun MainScreen() {
+ MaterialTheme(
+ colors = if (isSystemInDarkTheme()) darkColors() else lightColors()
+ ) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ ) {
+ val navController = rememberNavController()
+
+ NavHost(navController = navController, startDestination = "main") {
+ composable("main") { MainMenu(navController = navController) }
+ composable("static") { StaticCalendarSample() }
+ composable("week") { WeekCalendarSample() }
+ composable("selection") { SelectableCalendarSample() }
+ composable("components") { CustomComponentsSample() }
+ composable("custom_selection") { CustomSelectionSample() }
+ composable("viewmodel") { ViewModelSample() }
+ composable("kotlinx_datetime") { KotlinXDateTimeSample() }
+ composable("min_max_month") { MinMaxCalendarSample() }
+ }
+ }
+ }
+}
+
+@Composable
+fun MainMenu(navController: NavController) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center
+ ) {
+ Button(onClick = { navController.navigate("static") }) {
+ Text(text = "Static Calendar")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("selection") }) {
+ Text(text = "Selectable Calendar")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("week") }) {
+ Text(text = "Week Calendar")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("components") }) {
+ Text(text = "Custom Components")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("custom_selection") }) {
+ Text(text = "Custom Selection")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("viewmodel") }) {
+ Text(text = "ViewModel")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("kotlinx_datetime") }) {
+ Text(text = "Kotlinx DateTime")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+
+ Button(onClick = { navController.navigate("min_max_month") }) {
+ Text(text = "Min Max Month")
+ }
+ }
+}
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt
new file mode 100644
index 000000000..03128ea4d
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt
@@ -0,0 +1,268 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import io.github.boguszpawlowski.composecalendar.CalendarState
+import io.github.boguszpawlowski.composecalendar.StaticCalendar
+import io.github.boguszpawlowski.composecalendar.StaticWeekCalendar
+import io.github.boguszpawlowski.composecalendar.WeekCalendarState
+import io.github.boguszpawlowski.composecalendar.rememberCalendarState
+import io.github.boguszpawlowski.composecalendar.rememberWeekCalendarState
+import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
+import io.github.boguszpawlowski.composecalendar.week.Week
+import java.time.YearMonth
+import java.time.format.DateTimeFormatter
+
+@Composable
+fun MinMaxCalendarSample() {
+ val calendarState = rememberCalendarState(
+ initialMonth = YearMonth.now(),
+ minMonth = YearMonth.now(),
+ maxMonth = YearMonth.now()
+ )
+
+ val weekCalendarState = rememberWeekCalendarState(
+ initialWeek = Week.now(),
+ minWeek = Week.now(),
+ maxWeek = Week.now(),
+ )
+
+ Column(
+ Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ ) {
+ StaticCalendar(calendarState = calendarState)
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ MinMaxControls(calendarState = calendarState)
+
+ Spacer(modifier = Modifier.height(40.dp))
+
+ StaticWeekCalendar(calendarState = weekCalendarState)
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ WeekMinMaxControls(calendarState = weekCalendarState)
+ }
+}
+
+@Composable
+private fun MinMaxControls(
+ calendarState: CalendarState,
+) {
+ val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM") }
+ Text(
+ text = "Calendar Min Month",
+ style = MaterialTheme.typography.h5,
+ )
+ MinControls(
+ calendarState = calendarState,
+ dateFormatter = dateFormatter
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ text = "Calendar Max Month",
+ style = MaterialTheme.typography.h5,
+ )
+ MaxControls(
+ calendarState = calendarState,
+ dateFormatter = dateFormatter
+ )
+}
+
+@Composable
+private fun MinControls(
+ calendarState: CalendarState,
+ dateFormatter: DateTimeFormatter,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.monthState.minMonth = calendarState.monthState.minMonth.minusMonths(1L)
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "-",
+ textAlign = TextAlign.Center
+ )
+ val minYearMonthText = remember(calendarState.monthState.minMonth) {
+ dateFormatter.format(calendarState.monthState.minMonth)
+ }
+ Text(text = minYearMonthText)
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.monthState.minMonth = calendarState.monthState.minMonth.plusMonths(1L)
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "+",
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+@Composable
+private fun MaxControls(
+ calendarState: CalendarState,
+ dateFormatter: DateTimeFormatter,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.minusMonths(1L)
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "-",
+ textAlign = TextAlign.Center
+ )
+ val maxYearMonthText = remember(calendarState.monthState.maxMonth) {
+ dateFormatter.format(calendarState.monthState.maxMonth)
+ }
+ Text(text = maxYearMonthText)
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.plusMonths(1L)
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "+",
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+@Composable
+private fun WeekMinMaxControls(
+ calendarState: WeekCalendarState,
+) {
+ val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd") }
+ Text(
+ text = "Calendar Min Week",
+ style = MaterialTheme.typography.h5,
+ )
+ WeekMinControls(
+ calendarState = calendarState,
+ dateFormatter = dateFormatter
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ text = "Calendar Max Week",
+ style = MaterialTheme.typography.h5,
+ )
+ WeekMaxControls(
+ calendarState = calendarState,
+ dateFormatter = dateFormatter
+ )
+}
+
+@Composable
+private fun WeekMinControls(
+ calendarState: WeekCalendarState,
+ dateFormatter: DateTimeFormatter,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.weekState.minWeek = --calendarState.weekState.minWeek
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "-",
+ textAlign = TextAlign.Center
+ )
+ val minYearMonthText = remember(calendarState.weekState.minWeek) {
+ val start = dateFormatter.format(calendarState.weekState.minWeek.start)
+ val end = dateFormatter.format(calendarState.weekState.minWeek.end)
+ "$start - $end"
+ }
+ Text(text = minYearMonthText)
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.weekState.minWeek = ++calendarState.weekState.minWeek
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "+",
+ textAlign = TextAlign.Center
+ )
+ }
+}
+
+@Composable
+private fun WeekMaxControls(
+ calendarState: WeekCalendarState,
+ dateFormatter: DateTimeFormatter,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(5.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.weekState.maxWeek = --calendarState.weekState.maxWeek
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "-",
+ textAlign = TextAlign.Center
+ )
+ val maxYearMonthText = remember(calendarState.weekState.maxWeek) {
+ val start = dateFormatter.format(calendarState.weekState.maxWeek.start)
+ val end = dateFormatter.format(calendarState.weekState.maxWeek.end)
+ "$start - $end"
+ }
+ Text(text = maxYearMonthText)
+ Text(
+ modifier = Modifier
+ .clickable {
+ calendarState.weekState.maxWeek = ++calendarState.weekState.maxWeek
+ }
+ .size(25.dp)
+ .border(1.dp, Color.Black),
+ text = "+",
+ textAlign = TextAlign.Center
+ )
+ }
+}
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt
new file mode 100644
index 000000000..254707dbb
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt
@@ -0,0 +1,57 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.github.boguszpawlowski.composecalendar.SelectableCalendar
+import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState
+import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
+import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
+
+@Composable
+fun SelectableCalendarSample() {
+ val calendarState = rememberSelectableCalendarState()
+
+ Column(
+ Modifier.verticalScroll(rememberScrollState())
+ ) {
+ SelectableCalendar(calendarState = calendarState)
+
+ SelectionControls(selectionState = calendarState.selectionState)
+ }
+}
+
+@Composable
+private fun SelectionControls(
+ selectionState: DynamicSelectionState,
+) {
+ Text(
+ text = "Calendar Selection Mode",
+ style = MaterialTheme.typography.h5,
+ )
+ SelectionMode.values().forEach { selectionMode ->
+ Row(modifier = Modifier.fillMaxWidth()) {
+ RadioButton(
+ selected = selectionState.selectionMode == selectionMode,
+ onClick = { selectionState.selectionMode = selectionMode }
+ )
+ Text(text = selectionMode.name)
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+
+ Text(
+ text = "Selection: ${selectionState.selection.joinToString { it.toString() }}",
+ style = MaterialTheme.typography.h6,
+ )
+}
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt
new file mode 100644
index 000000000..1fe884242
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt
@@ -0,0 +1,17 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.github.boguszpawlowski.composecalendar.StaticCalendar
+
+@Composable
+fun StaticCalendarSample() {
+ StaticCalendar(
+ modifier = Modifier
+ .padding(8.dp)
+ .animateContentSize(),
+ )
+}
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt
new file mode 100644
index 000000000..99dfcd17d
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt
@@ -0,0 +1,199 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.verticalScroll
+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.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import io.github.boguszpawlowski.composecalendar.SelectableCalendar
+import io.github.boguszpawlowski.composecalendar.day.DayState
+import io.github.boguszpawlowski.composecalendar.header.MonthState
+import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState
+import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
+import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Period
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.time.LocalDate
+import java.time.YearMonth
+import kotlin.random.Random
+import kotlin.random.nextLong
+
+/**
+ * In this sample, calendar composable is wired with an ViewModel. It's purpose is to show how to use
+ * the composable in real world use-case, by an example implementation of a calendar
+ * which can display planned recipes along with their prices. The recipes will also be re-fetched from
+ * fake API every time month changes.
+ */
+@Composable
+fun ViewModelSample() {
+ val viewModel = remember { RecipeViewModel() }
+ val recipes by viewModel.recipesFlow.collectAsState()
+ val selectedPrice by viewModel.selectedRecipesPriceFlow.collectAsState(0)
+ val monthState = rememberMonthStateDefault()
+ LaunchedEffect(monthState) {
+ snapshotFlow { monthState.currentMonth }
+ .onEach { viewModel.onMonthChanged(it) }
+ .launchIn(this)
+ }
+ val state = rememberSelectableCalendarState(
+ confirmSelectionChange = { viewModel.onSelectionChanged(it); true },
+ monthState = monthState,
+ initialSelectionMode = Period,
+ )
+ Column(
+ Modifier.verticalScroll(rememberScrollState())
+ ) {
+ SelectableCalendar(
+ calendarState = state,
+ dayContent = { dayState ->
+ RecipeDay(
+ state = dayState,
+ plannedRecipe = recipes.firstOrNull { it.date == dayState.date },
+ )
+ }
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ Text(
+ text = "Selected recipes price: $selectedPrice",
+ style = MaterialTheme.typography.h6,
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+}
+
+@Composable
+private fun rememberMonthStateDefault() = rememberSaveable(saver = MonthState.Saver()) {
+ MonthState(
+ initialMonth = YearMonth.now(),
+ minMonth = YearMonth.now().minusMonths(10000),
+ maxMonth = YearMonth.now().plusMonths(10000),
+ )
+}
+
+/**
+ * Custom implementation of DayContent, which shows a dot if there is an recipe planned for this day.
+ */
+@Composable
+fun RecipeDay(
+ state: DayState,
+ plannedRecipe: PlannedRecipe?,
+ modifier: Modifier = Modifier,
+) {
+ 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, MaterialTheme.colors.primary) else null,
+ contentColor = if (isSelected) MaterialTheme.colors.secondary else contentColorFor(
+ backgroundColor = MaterialTheme.colors.surface
+ )
+ ) {
+ Column(
+ modifier = Modifier.clickable {
+ selectionState.onDateSelected(date)
+ },
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(text = date.dayOfMonth.toString())
+ if (plannedRecipe != null) {
+ Box(
+ modifier = Modifier
+ .size(10.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colors.primary)
+ )
+ Text(
+ text = plannedRecipe.price.toString(),
+ fontSize = 8.sp,
+ )
+ }
+ }
+ }
+}
+
+data class PlannedRecipe(
+ val date: LocalDate,
+ val price: Double,
+)
+
+/**
+ * ViewModel exposing list of our recipes
+ */
+class RecipeViewModel : ViewModel() {
+
+ private val selectionFlow = MutableStateFlow(emptyList())
+ val recipesFlow = MutableStateFlow(getRecipes())
+ val selectedRecipesPriceFlow = recipesFlow.combine(selectionFlow) { recipes, selection ->
+ recipes.filter { it.date in selection }.sumOf { it.price }
+ }
+
+ fun onSelectionChanged(selection: List) {
+ selectionFlow.value = selection
+ }
+
+ fun onMonthChanged(newMonth: YearMonth) = viewModelScope.launch {
+ recipesFlow.value = getRecipesFromApi(startingDay = newMonth.atDay(15))
+ }
+
+ private fun getRecipes(startingDay: LocalDate = LocalDate.now()) = listOf(
+ PlannedRecipe(startingDay.plusDays(Random.nextLong(1L..3L)), 20.0),
+ PlannedRecipe(startingDay.plusDays(Random.nextLong(4L..7L)), 20.0),
+ PlannedRecipe(startingDay.plusDays(Random.nextLong(8L..11L)), 10.0),
+ PlannedRecipe(startingDay.plusDays(Random.nextLong(-5L..-1L)), 25.0),
+ )
+
+ /**
+ * Simulated API call for new recipes
+ */
+ private suspend fun getRecipesFromApi(startingDay: LocalDate) = withContext(Dispatchers.Default) {
+ delay(100)
+ getRecipes(startingDay)
+ }
+}
+
+@Preview
+@Composable
+fun ViewModelSamplePreview() {
+ MaterialTheme {
+ ViewModelSample()
+ }
+}
diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt
new file mode 100644
index 000000000..9420be0cd
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt
@@ -0,0 +1,57 @@
+package io.github.boguszpawlowski.composecalendar.sample
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.RadioButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import io.github.boguszpawlowski.composecalendar.SelectableWeekCalendar
+import io.github.boguszpawlowski.composecalendar.rememberSelectableWeekCalendarState
+import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
+import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
+
+@Composable
+fun WeekCalendarSample() {
+ val calendarState = rememberSelectableWeekCalendarState()
+
+ Column(
+ Modifier.verticalScroll(rememberScrollState())
+ ) {
+ SelectableWeekCalendar(calendarState = calendarState)
+
+ SelectionControls(selectionState = calendarState.selectionState)
+ }
+}
+
+@Composable
+private fun SelectionControls(
+ selectionState: DynamicSelectionState,
+) {
+ Text(
+ text = "Calendar Selection Mode",
+ style = MaterialTheme.typography.h5,
+ )
+ SelectionMode.values().forEach { selectionMode ->
+ Row(modifier = Modifier.fillMaxWidth()) {
+ RadioButton(
+ selected = selectionState.selectionMode == selectionMode,
+ onClick = { selectionState.selectionMode = selectionMode }
+ )
+ Text(text = selectionMode.name)
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ }
+
+ Text(
+ text = "Selection: ${selectionState.selection.joinToString { it.toString() }}",
+ style = MaterialTheme.typography.h6,
+ )
+}
diff --git a/ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 000000000..7706ab9e6
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml b/ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 000000000..07d5da9cb
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/res/layout/activity_main.xml b/ComposeCalendar/sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 000000000..5f7f67a68
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/layout/activity_main.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..6b78462d6
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a571e6009
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..61da551c5
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..c41dd2853
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..db5080a75
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..6dba46dab
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..da31a871c
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..15ac68172
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..b216f2d31
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..f25a41974
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..e96783ccc
Binary files /dev/null and b/ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/ComposeCalendar/sample/src/main/res/values-night/themes.xml b/ComposeCalendar/sample/src/main/res/values-night/themes.xml
new file mode 100644
index 000000000..699a78df2
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ComposeCalendar/sample/src/main/res/values/colors.xml b/ComposeCalendar/sample/src/main/res/values/colors.xml
new file mode 100644
index 000000000..ca1931bca
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/values/colors.xml
@@ -0,0 +1,10 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #FFFFFFFF
+
diff --git a/ComposeCalendar/sample/src/main/res/values/strings.xml b/ComposeCalendar/sample/src/main/res/values/strings.xml
new file mode 100644
index 000000000..5a8769f51
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Compose Calendar
+
diff --git a/ComposeCalendar/sample/src/main/res/values/themes.xml b/ComposeCalendar/sample/src/main/res/values/themes.xml
new file mode 100644
index 000000000..7e3bdf6b0
--- /dev/null
+++ b/ComposeCalendar/sample/src/main/res/values/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/ComposeCalendar/settings.gradle.kts b/ComposeCalendar/settings.gradle.kts
new file mode 100644
index 000000000..6f5d6459f
--- /dev/null
+++ b/ComposeCalendar/settings.gradle.kts
@@ -0,0 +1,4 @@
+rootProject.name = "composecalendar"
+include(":sample")
+include(":library")
+include(":kotlinx-datetime")