This commit is contained in:
coco
2026-07-16 09:43:06 +08:00
parent 95aa018b8b
commit 63bb06ff63
95 changed files with 6004 additions and 0 deletions
+71
View File
@@ -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/
+6
View File
@@ -0,0 +1,6 @@
# Default ignored files
/*
!codeStyles/
!fileTemplates/
!file.template.settings.xml
!.gitignore
+133
View File
@@ -0,0 +1,133 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="IMPORT_NESTED_CLASSES" value="true" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+201
View File
@@ -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.
+250
View File
@@ -0,0 +1,250 @@
<img src="https://user-images.githubusercontent.com/36514058/154865567-3deef00d-f43e-4286-aa81-2924b81c650d.svg" width="500"/>
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.
![Github Actions](https://github.com/boguszpawlowski/composecalendar/actions/workflows/publish-snapshot.yml/badge.svg?branch=main)
[<img src="https://img.shields.io/maven-central/v/io.github.boguszpawlowski.composecalendar/composecalendar.svg?label=release%20version"/>](https://search.maven.org/search?q=g:io.github.boguszpawlowski.composecalendar)
[<img src="https://img.shields.io/nexus/s/https/s01.oss.sonatype.org/io.github.boguszpawlowski.composecalendar/composecalendar.svg?label=snapshot%20version"/>](https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/boguszpawlowski/composecalendar/)
[<img src="https://androidweekly.net/issues/issue-501/badge"/>](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:<latest-version>"
// separate artifact with utilities for working with kotlinx-datetime
implementation "io.github.boguszpawlowski.composecalendar:kotlinx-datetime:<latest-version>"
}
```
Snapshots are available on [Sonatypes 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()
}
```
<img src="https://github.com/boguszpawlowski/ComposeCalendar/blob/main/blob/screenshot_1.jpg" width="260">
> :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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+115
View File
@@ -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<KotlinCompile> {
kotlinOptions {
jvmTarget = "17"
freeCompilerArgs = freeCompilerArgs + listOf(
"-progressive",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlin.contracts.ExperimentalContracts",
)
}
}
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
}
dependencies {
detekt(DetektLib.Formatting)
detekt(DetektLib.Cli)
}
apiValidation {
ignoredProjects.add("sample")
}
tasks {
withType<Detekt> {
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<DependencyUpdatesTask> {
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)
}
+22
View File
@@ -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"))
}
@@ -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<Project> {
@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
}
}
}
}
@@ -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"
}
+294
View File
@@ -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
+27
View File
@@ -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
Binary file not shown.
@@ -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
+249
View File
@@ -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" "$@"
+92
View File
@@ -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
@@ -0,0 +1 @@
/build
@@ -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 <init> (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;
}
@@ -0,0 +1,15 @@
plugins {
kotlin(Kotlin.JvmPluginId)
id(MavenPublish.PluginId)
}
kotlin {
explicitApi()
}
dependencies {
api(Kotlin.DateTime)
testImplementation(Kotest.Assertions)
testImplementation(Kotest.RunnerJunit5)
}
@@ -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
@@ -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)
@@ -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
@@ -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
}
@@ -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<DateTimeException> {
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
}
}
}
})
+1
View File
@@ -0,0 +1 @@
/build
+256
View File
@@ -0,0 +1,256 @@
public final class io/github/boguszpawlowski/composecalendar/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
public fun <init> ()V
}
public final class io/github/boguszpawlowski/composecalendar/CalendarKt {
public static final fun Calendar (Lio/github/boguszpawlowski/composecalendar/CalendarState;Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
public static final fun SelectableCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
public static final fun StaticCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
public static final fun rememberCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
public static final fun rememberSelectableCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
}
public final class io/github/boguszpawlowski/composecalendar/CalendarState {
public static final field $stable I
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
public final fun getMonthState ()Lio/github/boguszpawlowski/composecalendar/header/MonthState;
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
}
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt;
public static field lambda-1 Lkotlin/jvm/functions/Function4;
public static field lambda-10 Lkotlin/jvm/functions/Function4;
public static field lambda-11 Lkotlin/jvm/functions/Function4;
public static field lambda-12 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function4;
public static field lambda-3 Lkotlin/jvm/functions/Function4;
public static field lambda-4 Lkotlin/jvm/functions/Function3;
public static field lambda-5 Lkotlin/jvm/functions/Function4;
public static field lambda-6 Lkotlin/jvm/functions/Function4;
public static field lambda-7 Lkotlin/jvm/functions/Function4;
public static field lambda-8 Lkotlin/jvm/functions/Function3;
public static field lambda-9 Lkotlin/jvm/functions/Function4;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-10$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-11$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-12$library_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
}
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt;
public static field lambda-1 Lkotlin/jvm/functions/Function4;
public static field lambda-2 Lkotlin/jvm/functions/Function4;
public static field lambda-3 Lkotlin/jvm/functions/Function4;
public static field lambda-4 Lkotlin/jvm/functions/Function4;
public static field lambda-5 Lkotlin/jvm/functions/Function4;
public static field lambda-6 Lkotlin/jvm/functions/Function4;
public static field lambda-7 Lkotlin/jvm/functions/Function4;
public static field lambda-8 Lkotlin/jvm/functions/Function4;
public static field lambda-9 Lkotlin/jvm/functions/Function4;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function4;
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
}
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarKt {
public static final fun SelectableWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
public static final fun StaticWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;ZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
public static final fun WeekCalendar (Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;Landroidx/compose/ui/Modifier;Ljava/time/LocalDate;Ljava/time/DayOfWeek;ZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
public static final fun rememberSelectableWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
public static final fun rememberWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
}
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarState {
public static final field $stable I
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
public final fun getWeekState ()Lio/github/boguszpawlowski/composecalendar/header/WeekState;
}
public abstract interface class io/github/boguszpawlowski/composecalendar/day/Day {
public abstract fun getDate ()Ljava/time/LocalDate;
public abstract fun isCurrentDay ()Z
public abstract fun isFromCurrentMonth ()Z
}
public final class io/github/boguszpawlowski/composecalendar/day/DayState : io/github/boguszpawlowski/composecalendar/day/Day {
public static final field $stable I
public fun <init> (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
public final fun component2 ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
public final fun copy (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/day/DayState;Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
public fun equals (Ljava/lang/Object;)Z
public fun getDate ()Ljava/time/LocalDate;
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
public fun hashCode ()I
public fun isCurrentDay ()Z
public fun isFromCurrentMonth ()Z
public fun toString ()Ljava/lang/String;
}
public final class io/github/boguszpawlowski/composecalendar/day/DefaultDayKt {
public static final fun DefaultDay-t6yy7ic (Lio/github/boguszpawlowski/composecalendar/day/DayState;Landroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
}
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt {
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt;
public static field lambda-1 Lkotlin/jvm/functions/Function2;
public static field lambda-2 Lkotlin/jvm/functions/Function2;
public fun <init> ()V
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
}
public final class io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeaderKt {
public static final fun DefaultMonthHeader (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}
public final class io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeaderKt {
public static final fun DefaultWeekHeader (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}
public abstract interface class io/github/boguszpawlowski/composecalendar/header/MonthState {
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/MonthState$Companion;
public abstract fun getCurrentMonth ()Ljava/time/YearMonth;
public abstract fun getMaxMonth ()Ljava/time/YearMonth;
public abstract fun getMinMonth ()Ljava/time/YearMonth;
public abstract fun setCurrentMonth (Ljava/time/YearMonth;)V
public abstract fun setMaxMonth (Ljava/time/YearMonth;)V
public abstract fun setMinMonth (Ljava/time/YearMonth;)V
}
public final class io/github/boguszpawlowski/composecalendar/header/MonthState$Companion {
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
}
public final class io/github/boguszpawlowski/composecalendar/header/MonthStateKt {
public static final fun MonthState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/header/MonthState;
}
public abstract interface class io/github/boguszpawlowski/composecalendar/header/WeekState {
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/WeekState$Companion;
public abstract fun getCurrentWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public abstract fun getMaxWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public abstract fun getMinWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public abstract fun setCurrentWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
public abstract fun setMaxWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
public abstract fun setMinWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
}
public final class io/github/boguszpawlowski/composecalendar/header/WeekState$Companion {
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
}
public final class io/github/boguszpawlowski/composecalendar/header/WeekStateKt {
public static final fun WeekState (Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)Lio/github/boguszpawlowski/composecalendar/header/WeekState;
}
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler {
public static final field $stable I
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler;
public final fun calculateNewSelection (Ljava/time/LocalDate;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)Ljava/util/List;
}
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public static final field $stable I
public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getSelection ()Ljava/util/List;
public final fun getSelectionMode ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public fun isDateSelected (Ljava/time/LocalDate;)Z
public fun onDateSelected (Ljava/time/LocalDate;)V
public final fun setSelection (Ljava/util/List;)V
public final fun setSelectionMode (Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
}
public final class io/github/boguszpawlowski/composecalendar/selection/EmptySelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public static final field $stable I
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/EmptySelectionState;
public fun isDateSelected (Ljava/time/LocalDate;)Z
public fun onDateSelected (Ljava/time/LocalDate;)V
}
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionMode : java/lang/Enum {
public static final field Multiple Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static final field None Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static final field Period Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static final field Single Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public static fun values ()[Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
}
public abstract interface class io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public abstract fun isDateSelected (Ljava/time/LocalDate;)Z
public abstract fun onDateSelected (Ljava/time/LocalDate;)V
}
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionState$DefaultImpls {
public static fun isDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)Z
public static fun onDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)V
}
public final class io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeaderKt {
public static final fun DefaultDaysOfWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
public static final fun DefaultWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}
public final class io/github/boguszpawlowski/composecalendar/week/Week {
public static final field $stable I
public static final field Companion Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;
public fun <init> (Ljava/util/List;)V
public final fun compareTo (Lio/github/boguszpawlowski/composecalendar/week/Week;)I
public final fun component1 ()Ljava/util/List;
public final fun copy (Ljava/util/List;)Lio/github/boguszpawlowski/composecalendar/week/Week;
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
public final fun dec ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public fun equals (Ljava/lang/Object;)Z
public final fun getDays ()Ljava/util/List;
public final fun getEnd ()Ljava/time/LocalDate;
public final fun getStart ()Ljava/time/LocalDate;
public final fun getYearMonth ()Ljava/time/YearMonth;
public fun hashCode ()I
public final fun inc ()Lio/github/boguszpawlowski/composecalendar/week/Week;
public final fun minusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
public final fun plusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
public fun toString ()Ljava/lang/String;
}
public final class io/github/boguszpawlowski/composecalendar/week/Week$Companion {
public final fun now (Ljava/time/DayOfWeek;)Lio/github/boguszpawlowski/composecalendar/week/Week;
public static synthetic fun now$default (Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;Ljava/time/DayOfWeek;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
}
public final class io/github/boguszpawlowski/composecalendar/week/WeekKt {
public static final fun between (Ljava/time/temporal/ChronoUnit;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)I
}
+30
View File
@@ -0,0 +1,30 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id(Android.LibraryPluginId)
kotlin(Kotlin.AndroidPluginId)
id("common-android-plugin")
id(MavenPublish.PluginId)
}
android {
namespace = "io.github.boguszpawlowski.composecalendar"
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xexplicit-api=strict" + "-Xcontext-receivers"
}
}
}
dependencies {
implementation(Compose.Ui)
implementation(Compose.UiTooling)
implementation(Compose.AccompanistPager)
implementation(Compose.Foundation)
implementation(Compose.FoundationLayout)
implementation(Compose.Material)
implementation(Timber.Core)
testImplementation(Kotest.Assertions)
testImplementation(Kotest.RunnerJunit5)
}
@@ -0,0 +1,5 @@
POM_ARTIFACT_ID=composecalendar
POM_DESCRIPTION=Library for handling the Calendar view in Jetpack Compose.
POM_PACKAGING=aar
SONATYPE_HOST=S01
RELEASE_SIGNING_ENABLED=true
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,286 @@
@file:Suppress("MatchingDeclarationName")
package io.github.boguszpawlowski.composecalendar
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.day.DefaultDay
import io.github.boguszpawlowski.composecalendar.header.DefaultMonthHeader
import io.github.boguszpawlowski.composecalendar.header.MonthState
import io.github.boguszpawlowski.composecalendar.month.MonthContent
import io.github.boguszpawlowski.composecalendar.month.MonthPager
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek
import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader
import io.github.boguszpawlowski.composecalendar.week.rotateRight
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.WeekFields
import java.util.Locale
/**
* State of the calendar composable
*
* @property monthState currently showed month
* @property selectionState handler for the calendar's selection
*/
@Stable
public class CalendarState<T : SelectionState>(
public val monthState: MonthState,
public val selectionState: T,
)
/**
* [Calendar] implementation using a [DynamicSelectionState] as a selection handler.
*
* * Basic usage:
* ```
* @Composable
* fun MainScreen() {
* SelectableCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
* @param weekDaysScrollEnabled if the week days should be scrollable
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param monthHeader header for showing the current month and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
* @param monthContainer container composable for all the days in current month
*/
@Composable
@SuppressLint("NewApi")
public fun SelectableCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
showAdjacentMonths: Boolean = true,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
calendarState: CalendarState<DynamicSelectionState> = rememberSelectableCalendarState(),
dayContent: @Composable BoxScope.(DayState<DynamicSelectionState>) -> Unit = { DefaultDay(it) },
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
Box { content(PaddingValues()) }
},
) {
Calendar(
modifier = modifier,
firstDayOfWeek = firstDayOfWeek,
today = today,
showAdjacentMonths = showAdjacentMonths,
horizontalSwipeEnabled = horizontalSwipeEnabled,
weekDaysScrollEnabled = weekDaysScrollEnabled,
calendarState = calendarState,
dayContent = dayContent,
monthHeader = monthHeader,
daysOfWeekHeader = daysOfWeekHeader,
monthContainer = monthContainer
)
}
/**
* [Calendar] implementation without any mechanism for the selection.
*
* Basic usage:
* ```
* @Composable
* fun MainScreen() {
* StaticCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
* @param weekDaysScrollEnabled if the week days should be scrollable
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param monthHeader header for showing the current month and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
* @param monthContainer container composable for all the days in current month
*/
@Composable
@SuppressLint("NewApi")
public fun StaticCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
showAdjacentMonths: Boolean = true,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
calendarState: CalendarState<EmptySelectionState> = rememberCalendarState(),
dayContent: @Composable BoxScope.(DayState<EmptySelectionState>) -> Unit = { DefaultDay(it) },
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
Box { content(PaddingValues()) }
},
) {
Calendar(
modifier = modifier,
firstDayOfWeek = firstDayOfWeek,
today = today,
showAdjacentMonths = showAdjacentMonths,
horizontalSwipeEnabled = horizontalSwipeEnabled,
weekDaysScrollEnabled = weekDaysScrollEnabled,
calendarState = calendarState,
dayContent = dayContent,
monthHeader = monthHeader,
daysOfWeekHeader = daysOfWeekHeader,
monthContainer = monthContainer
)
}
/**
* Composable for showing a calendar. The calendar state has to be provided by the call site. If you
* want to use built-in implementation, check out:
* [SelectableCalendar] - calendar composable handling selection that can be changed at runtime
* [StaticCalendar] - calendar without any mechanism for selection
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param showAdjacentMonths whenever to show or hide the days from adjacent months
* @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param monthHeader header for showing the current month and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
* @param monthContainer container composable for all the days in current month
*/
@Composable
@SuppressLint("NewApi")
public fun <T : SelectionState> Calendar(
calendarState: CalendarState<T>,
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
showAdjacentMonths: Boolean = true,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit = { DefaultDay(it) },
monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) },
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content ->
Box { content(PaddingValues()) }
},
) {
val initialMonth = remember { calendarState.monthState.currentMonth }
val daysOfWeek = remember(firstDayOfWeek) {
DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal)
}
Column(
modifier = modifier,
) {
monthHeader(calendarState.monthState)
if (horizontalSwipeEnabled) {
MonthPager(
initialMonth = initialMonth,
showAdjacentMonths = showAdjacentMonths,
monthState = calendarState.monthState,
selectionState = calendarState.selectionState,
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
daysOfWeek = daysOfWeek,
dayContent = dayContent,
weekHeader = daysOfWeekHeader,
monthContainer = monthContainer,
)
} else {
MonthContent(
modifier = Modifier.fillMaxWidth(),
currentMonth = calendarState.monthState.currentMonth,
showAdjacentMonths = showAdjacentMonths,
selectionState = calendarState.selectionState,
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
daysOfWeek = daysOfWeek,
dayContent = dayContent,
weekHeader = daysOfWeekHeader,
monthContainer = monthContainer,
)
}
}
}
/**
* Helper function for providing a [CalendarState] implementation with selection mechanism.
*
* @param initialMonth initially rendered month
* @param initialSelection initial selection of the composable
* @param initialSelectionMode initial mode of the selection
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
* @param minMonth first month that can be shown
* @param maxMonth last month that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberSelectableCalendarState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange),
maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange),
initialSelection: List<LocalDate> = emptyList(),
initialSelectionMode: SelectionMode = SelectionMode.Single,
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
selectionState: DynamicSelectionState = rememberSaveable(
saver = DynamicSelectionState.Saver(confirmSelectionChange),
) {
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
},
): CalendarState<DynamicSelectionState> = remember { CalendarState(monthState, selectionState) }
/**
* Helper function for providing a [CalendarState] implementation without a selection mechanism.
*
* @param initialMonth initially rendered month
* @param minMonth first month that can be shown
* @param maxMonth last month that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberCalendarState(
initialMonth: YearMonth = YearMonth.now(),
minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange),
maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange),
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(
initialMonth = initialMonth,
minMonth = minMonth,
maxMonth = maxMonth
)
},
): CalendarState<EmptySelectionState> = remember { CalendarState(monthState, EmptySelectionState) }
internal const val DefaultCalendarPagerRange = 10_000L
@@ -0,0 +1,264 @@
@file:Suppress("MatchingDeclarationName", "LongParameterList")
package io.github.boguszpawlowski.composecalendar
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.day.DefaultDay
import io.github.boguszpawlowski.composecalendar.header.WeekState
import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import io.github.boguszpawlowski.composecalendar.selection.SelectionMode
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek
import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader
import io.github.boguszpawlowski.composecalendar.week.Week
import io.github.boguszpawlowski.composecalendar.week.WeekContent
import io.github.boguszpawlowski.composecalendar.week.WeekPager
import io.github.boguszpawlowski.composecalendar.week.getWeekDays
import io.github.boguszpawlowski.composecalendar.week.rotateRight
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.WeekFields
import java.util.Locale
import io.github.boguszpawlowski.composecalendar.header.DefaultWeekHeader as DefaultProperWeekHeader
/**
* State of the week calendar composable
*
* @property weekState currently showed week
* @property selectionState handler for the calendar's selection
*/
@Stable
public class WeekCalendarState<T : SelectionState>(
public val weekState: WeekState,
public val selectionState: T,
)
/**
* [WeekCalendar] implementation using a [DynamicSelectionState] as a selection handler.
*
* * Basic usage:
* ```
* @Composable
* fun MainScreen() {
* SelectableWeekCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
* @param weekDaysScrollEnabled if the week days should be scrollable
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param weekHeader header for showing the current week and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
*/
@Composable
public fun SelectableWeekCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
calendarState: WeekCalendarState<DynamicSelectionState> = rememberSelectableWeekCalendarState(),
dayContent: @Composable BoxScope.(DayState<DynamicSelectionState>) -> Unit = { DefaultDay(it) },
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
DefaultProperWeekHeader(it)
},
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
) {
WeekCalendar(
modifier = modifier,
calendarState = calendarState,
today = today,
horizontalSwipeEnabled = horizontalSwipeEnabled,
firstDayOfWeek = firstDayOfWeek,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
weekHeader = weekHeader,
daysOfWeekHeader = daysOfWeekHeader,
)
}
/**
* [WeekCalendar] implementation without any mechanism for the selection.
*
* * Basic usage:
* ```
* @Composable
* fun MainScreen() {
* StaticWeekCalendar()
* }
* ```
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param weekHeader header for showing the current week and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
*/
@Composable
@SuppressLint("NewApi")
public fun StaticWeekCalendar(
modifier: Modifier = Modifier,
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
today: LocalDate = LocalDate.now(),
horizontalSwipeEnabled: Boolean = true,
calendarState: WeekCalendarState<EmptySelectionState> = rememberWeekCalendarState(),
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<EmptySelectionState>) -> Unit = { DefaultDay(it) },
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
DefaultProperWeekHeader(it)
},
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
) {
WeekCalendar(
modifier = modifier,
calendarState = calendarState,
today = today,
horizontalSwipeEnabled = horizontalSwipeEnabled,
firstDayOfWeek = firstDayOfWeek,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
weekHeader = weekHeader,
daysOfWeekHeader = daysOfWeekHeader,
)
}
/**
* Composable for showing a week calendar. The calendar state has to be provided by the call site. If you
* want to use built-in implementation, check out:
* [SelectableWeekCalendar] - calendar composable handling selection that can be changed at runtime
* [StaticWeekCalendar] - calendar without any mechanism for selection
*
* @param modifier
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param today current day, defaults to [LocalDate.now]
* @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe
* @param calendarState state of the composable
* @param dayContent composable rendering the current day
* @param weekHeader header for showing the current week and controls for changing it
* @param daysOfWeekHeader header for showing captions for each day of week
*/
@Composable
@SuppressLint("NewApi")
public fun <T : SelectionState> WeekCalendar(
calendarState: WeekCalendarState<T>,
modifier: Modifier = Modifier,
today: LocalDate = LocalDate.now(),
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
horizontalSwipeEnabled: Boolean = true,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit = { DefaultDay(it) },
weekHeader: @Composable ColumnScope.(WeekState) -> Unit = {
DefaultProperWeekHeader(it)
},
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit = { DefaultDaysOfWeekHeader(it) },
) {
val initialWeek = remember { calendarState.weekState.currentWeek }
val daysOfWeek = remember(firstDayOfWeek) {
DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal)
}
Column(
modifier = modifier,
) {
weekHeader(calendarState.weekState)
if (horizontalSwipeEnabled) {
WeekPager(
initialWeek = initialWeek,
daysOfWeek = daysOfWeek,
weekState = calendarState.weekState,
selectionState = calendarState.selectionState,
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
daysOfWeekHeader = daysOfWeekHeader,
)
} else {
WeekContent(
daysOfWeek = daysOfWeek,
weekDays = calendarState.weekState.currentWeek.getWeekDays(today),
selectionState = calendarState.selectionState,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
daysOfWeekHeader = daysOfWeekHeader,
)
}
}
}
/**
* Helper function for providing a [WeekCalendarState] implementation with selection mechanism.
*
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param initialWeek initially rendered month
* @param initialSelection initial selection of the composable
* @param initialSelectionMode initial mode of the selection
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
* @param minWeek first week that can be shown
* @param maxWeek last week that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberSelectableWeekCalendarState(
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
initialWeek: Week = Week.now(firstDayOfWeek),
minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange),
maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange),
initialSelection: List<LocalDate> = emptyList(),
initialSelectionMode: SelectionMode = SelectionMode.Single,
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) {
WeekState(
initialWeek = initialWeek,
minWeek = minWeek,
maxWeek = maxWeek,
)
},
selectionState: DynamicSelectionState = rememberSaveable(
saver = DynamicSelectionState.Saver(confirmSelectionChange),
) {
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
},
): WeekCalendarState<DynamicSelectionState> =
remember { WeekCalendarState(weekState, selectionState) }
/**
* Helper function for providing a [WeekCalendarState] implementation without a selection mechanism.
*
* @param firstDayOfWeek first day of a week, defaults to current locale's
* @param initialWeek initially rendered week
* @param minWeek first week that can be shown
* @param maxWeek last week that can be shown
*/
@Composable
@SuppressLint("NewApi")
public fun rememberWeekCalendarState(
firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek,
initialWeek: Week = Week.now(firstDayOfWeek),
minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange),
maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange),
weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) {
WeekState(
initialWeek = initialWeek,
minWeek = minWeek,
maxWeek = maxWeek,
)
},
): WeekCalendarState<EmptySelectionState> =
remember { WeekCalendarState(weekState, EmptySelectionState) }
@@ -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
}
@@ -0,0 +1,16 @@
package io.github.boguszpawlowski.composecalendar.day
import androidx.compose.runtime.Stable
import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
public typealias NonSelectableDayState = DayState<EmptySelectionState>
/**
* Contains information about current selection as well as date of rendered day
*/
@Stable
public data class DayState<T : SelectionState>(
private val day: Day,
val selectionState: T,
) : Day by day
@@ -0,0 +1,63 @@
package io.github.boguszpawlowski.composecalendar.day
import android.annotation.SuppressLint
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import java.time.LocalDate
/**
* Default implementation for day content. It supports different appearance for days from
* current and adjacent month, as well as current day and selected day
*
* @param selectionColor color of the border, when day is selected
* @param currentDayColor color of content for the current date
* @param onClick callback for interacting with day clicks
*/
@Composable
@SuppressLint("NewApi")
public fun <T : SelectionState> DefaultDay(
state: DayState<T>,
modifier: Modifier = Modifier,
selectionColor: Color = MaterialTheme.colors.secondary,
currentDayColor: Color = MaterialTheme.colors.primary,
onClick: (LocalDate) -> Unit = {},
) {
val date = state.date
val selectionState = state.selectionState
val isSelected = selectionState.isDateSelected(date)
Card(
modifier = modifier
.aspectRatio(1f)
.padding(2.dp),
elevation = if (state.isFromCurrentMonth) 4.dp else 0.dp,
border = if (state.isCurrentDay) BorderStroke(1.dp, currentDayColor) else null,
contentColor = if (isSelected) selectionColor else contentColorFor(
backgroundColor = MaterialTheme.colors.surface
)
) {
Box(
modifier = Modifier.clickable {
onClick(date)
selectionState.onDateSelected(date)
},
contentAlignment = Alignment.Center,
) {
Text(text = date.dayOfMonth.toString())
}
}
}
@@ -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
@@ -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
)
}
}
}
@@ -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",
)
}
}
@@ -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",
)
}
}
@@ -0,0 +1,76 @@
package io.github.boguszpawlowski.composecalendar.header
import android.annotation.SuppressLint
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.setValue
import java.time.YearMonth
@Suppress("FunctionName") // Factory function
public fun MonthState(
initialMonth: YearMonth,
minMonth: YearMonth,
maxMonth: YearMonth,
): MonthState = MonthStateImpl(initialMonth, minMonth, maxMonth)
@Stable
public interface MonthState {
public var currentMonth: YearMonth
public var minMonth: YearMonth
public var maxMonth: YearMonth
public companion object {
public fun Saver(): Saver<MonthState, Any> = mapSaver(
save = { monthState ->
mapOf(
"currentMonth" to monthState.currentMonth.toString(),
"minMonth" to monthState.minMonth.toString(),
"maxMonth" to monthState.maxMonth.toString(),
)
},
restore = { restoreMap ->
MonthState(
YearMonth.parse(restoreMap["currentMonth"] as String),
YearMonth.parse(restoreMap["minMonth"] as String),
YearMonth.parse(restoreMap["maxMonth"] as String),
)
}
)
}
}
@Stable
@SuppressLint("NewApi")
private class MonthStateImpl(
initialMonth: YearMonth,
minMonth: YearMonth,
maxMonth: YearMonth,
) : MonthState {
private var _currentMonth by mutableStateOf(initialMonth)
private var _minMonth by mutableStateOf(minMonth)
private var _maxMonth by mutableStateOf(maxMonth)
override var currentMonth: YearMonth
get() = _currentMonth
set(value) {
_currentMonth = value
}
override var minMonth: YearMonth
get() = _minMonth
set(value) {
if (value > _maxMonth) return
_minMonth = value
}
override var maxMonth: YearMonth
get() = _maxMonth
set(value) {
if (value < _minMonth) return
_maxMonth = value
}
}
@@ -0,0 +1,86 @@
package io.github.boguszpawlowski.composecalendar.header
import android.annotation.SuppressLint
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver
import androidx.compose.runtime.setValue
import io.github.boguszpawlowski.composecalendar.week.Week
import java.time.LocalDate
@Suppress("FunctionName") // Factory function
public fun WeekState(
initialWeek: Week,
minWeek: Week,
maxWeek: Week,
): WeekState = WeekStateImpl(
initialWeek = initialWeek,
minWeek = minWeek,
maxWeek = maxWeek,
)
@Stable
@SuppressLint("NewApi")
public interface WeekState {
public var currentWeek: Week
public var minWeek: Week
public var maxWeek: Week
public companion object {
@Suppress("FunctionName") // Factory function
public fun Saver(): Saver<WeekState, Any> = mapSaver(
save = { weekState ->
mapOf(
CurrentWeekKey to weekState.currentWeek.days[0].toString(),
MinWeekKey to weekState.minWeek.days[0].toString(),
MaxWeekKey to weekState.maxWeek.days[0].toString(),
)
},
restore = { restoreMap ->
WeekState(
initialWeek = Week(firstDay = LocalDate.parse(restoreMap[CurrentWeekKey] as String)),
minWeek = Week(firstDay = LocalDate.parse(restoreMap[MinWeekKey] as String)),
maxWeek = Week(firstDay = LocalDate.parse(restoreMap[MaxWeekKey] as String)),
)
}
)
private const val CurrentWeekKey = "CurrentWeek"
private const val MinWeekKey = "MinWeek"
private const val MaxWeekKey = "MaxWeek"
}
}
@Stable
private class WeekStateImpl(
initialWeek: Week,
minWeek: Week,
maxWeek: Week,
) : WeekState {
private var _currentWeek by mutableStateOf(initialWeek)
private var _minWeek by mutableStateOf(minWeek)
private var _maxWeek by mutableStateOf(maxWeek)
override var currentWeek: Week
get() = _currentWeek
set(value) {
_currentWeek = value
}
override var minWeek: Week
get() = _minWeek
set(value) {
if (value > _maxWeek) return
_minWeek = value
}
override var maxWeek: Week
get() = _maxWeek
set(value) {
if (value < _minWeek) return
_maxWeek = value
}
}
@@ -0,0 +1,162 @@
package io.github.boguszpawlowski.composecalendar.month
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import dev.chrisbanes.snapper.ExperimentalSnapperApi
import dev.chrisbanes.snapper.SnapOffsets
import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults
import dev.chrisbanes.snapper.SnapperLayoutInfo
import dev.chrisbanes.snapper.rememberSnapperFlingBehavior
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.header.MonthState
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import io.github.boguszpawlowski.composecalendar.week.WeekRow
import io.github.boguszpawlowski.composecalendar.week.getWeeks
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit
@OptIn(ExperimentalSnapperApi::class)
@Composable
@Suppress("LongMethod", "NewApi")
internal fun <T : SelectionState> MonthPager(
initialMonth: YearMonth,
showAdjacentMonths: Boolean,
selectionState: T,
monthState: MonthState,
daysOfWeek: List<DayOfWeek>,
today: LocalDate,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
weekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val initialFirstVisibleItemIndex = remember(initialMonth, monthState.minMonth) {
ChronoUnit.MONTHS.between(monthState.minMonth, initialMonth).toInt()
}
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val flingBehavior = rememberSnapperFlingBehavior(
lazyListState = listState,
snapOffsetForItem = SnapOffsets.Start,
springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec,
decayAnimationSpec = rememberSplineBasedDecay(),
snapIndex = coerceSnapIndex,
)
val monthListState = remember {
MonthListState(
coroutineScope = coroutineScope,
monthState = monthState,
listState = listState,
)
}
Column(modifier = Modifier.fillMaxWidth()) {
if (weekDaysScrollEnabled.not()) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
content = { weekHeader(daysOfWeek) },
)
}
val pagerCount = remember(monthState.minMonth, monthState.maxMonth) {
ChronoUnit.MONTHS.between(monthState.minMonth, monthState.maxMonth).toInt() + 1
}
LazyRow(
modifier = modifier.testTag("MonthPager"),
state = listState,
flingBehavior = flingBehavior,
verticalAlignment = Alignment.Top,
) {
items(
count = pagerCount,
key = { index ->
monthListState.getMonthForPage(index).let { "${it.year}-${it.monthValue}" }
}
) { index ->
MonthContent(
modifier = Modifier.fillParentMaxWidth(),
showAdjacentMonths = showAdjacentMonths,
selectionState = selectionState,
currentMonth = monthListState.getMonthForPage(index),
today = today,
weekDaysScrollEnabled = weekDaysScrollEnabled,
daysOfWeek = daysOfWeek,
dayContent = dayContent,
weekHeader = weekHeader,
monthContainer = monthContainer
)
}
}
}
}
@Composable
internal fun <T : SelectionState> MonthContent(
showAdjacentMonths: Boolean,
selectionState: T,
currentMonth: YearMonth,
daysOfWeek: List<DayOfWeek>,
today: LocalDate,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
weekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit,
) {
Column {
if (weekDaysScrollEnabled) {
Box(
modifier = modifier
.wrapContentHeight(),
content = { weekHeader(daysOfWeek) },
)
}
monthContainer { paddingValues ->
Column(
modifier = modifier
.padding(paddingValues)
) {
currentMonth.getWeeks(
includeAdjacentMonths = showAdjacentMonths,
firstDayOfTheWeek = daysOfWeek.first(),
today = today,
).forEach { week ->
WeekRow(
weekDays = week,
selectionState = selectionState,
dayContent = dayContent,
)
}
}
}
}
}
@OptIn(ExperimentalSnapperApi::class)
internal val coerceSnapIndex: (SnapperLayoutInfo, startIndex: Int, targetIndex: Int) -> Int =
{ _, startIndex, targetIndex ->
targetIndex
.coerceIn(startIndex - 1, startIndex + 1)
}
@@ -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)
@@ -0,0 +1,35 @@
package io.github.boguszpawlowski.composecalendar.selection
import android.annotation.SuppressLint
import io.github.boguszpawlowski.composecalendar.util.addOrRemoveIfExists
import java.time.LocalDate
/**
* Helper class for calculating new selection, when using a [DynamicSelectionState] approach.
* @param date clicked on date
* @param selection current selection
* @param selectionMode current selection mode
* @returns new selection in a form of a list of local dates.
*/
@SuppressLint("NewApi")
public object DynamicSelectionHandler {
public fun calculateNewSelection(
date: LocalDate,
selection: List<LocalDate>,
selectionMode: SelectionMode,
): List<LocalDate> = when (selectionMode) {
SelectionMode.None -> emptyList()
SelectionMode.Single -> if (date == selection.firstOrNull()) {
emptyList()
} else {
listOf(date)
}
SelectionMode.Multiple -> selection.addOrRemoveIfExists(date)
SelectionMode.Period -> when {
date.isBefore(selection.startOrMax()) -> listOf(date)
date.isAfter(selection.startOrMax()) -> selection.fillUpTo(date)
date == selection.startOrMax() -> emptyList()
else -> selection
}
}
}
@@ -0,0 +1,15 @@
package io.github.boguszpawlowski.composecalendar.selection
import android.annotation.SuppressLint
import java.time.LocalDate
@SuppressLint("NewApi")
internal fun Collection<LocalDate>.startOrMax() = firstOrNull() ?: LocalDate.MAX
internal fun Collection<LocalDate>.endOrNull() = drop(1).lastOrNull()
@SuppressLint("NewApi")
internal fun Collection<LocalDate>.fillUpTo(date: LocalDate) =
(0..date.toEpochDay() - first().toEpochDay()).map {
first().plusDays(it)
}
@@ -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,
}
@@ -0,0 +1,83 @@
package io.github.boguszpawlowski.composecalendar.selection
import android.annotation.SuppressLint
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.setValue
import java.time.LocalDate
@Stable
public interface SelectionState {
public fun isDateSelected(date: LocalDate): Boolean = false
public fun onDateSelected(date: LocalDate) { }
}
/**
* Class that enables for dynamically changing selection modes in the runtime. Depending on the mode, selection changes differently.
* Mode can be varied by setting desired [SelectionMode] in the [selectionMode] mutable property.
* @param confirmSelectionChange return false from this callback to veto the selection change
*/
@Stable
@SuppressLint("NewApi")
public class DynamicSelectionState(
private val confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
selection: List<LocalDate>,
selectionMode: SelectionMode,
) : SelectionState {
private var _selection by mutableStateOf(selection)
private var _selectionMode by mutableStateOf(selectionMode)
public var selection: List<LocalDate>
get() = _selection
set(value) {
if (value != selection && confirmSelectionChange(value)) {
_selection = value
}
}
public var selectionMode: SelectionMode
get() = _selectionMode
set(value) {
if (value != selectionMode) {
_selection = emptyList()
_selectionMode = value
}
}
override fun isDateSelected(date: LocalDate): Boolean = selection.contains(date)
override fun onDateSelected(date: LocalDate) {
selection = DynamicSelectionHandler.calculateNewSelection(date, selection, selectionMode)
}
internal companion object {
@Suppress("FunctionName", "UNCHECKED_CAST") // Factory function
fun Saver(
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean,
): Saver<DynamicSelectionState, Any> =
listSaver(
save = { raw ->
listOf(raw.selectionMode, raw.selection.map { it.toString() })
},
restore = { restored ->
DynamicSelectionState(
confirmSelectionChange = confirmSelectionChange,
selectionMode = restored[0] as SelectionMode,
selection = (restored[1] as? List<String>)?.map { LocalDate.parse(it) }.orEmpty(),
)
}
)
}
}
@Immutable
public object EmptySelectionState : SelectionState {
override fun isDateSelected(date: LocalDate): Boolean = false
override fun onDateSelected(date: LocalDate): Unit = Unit
}
@@ -0,0 +1,13 @@
package io.github.boguszpawlowski.composecalendar.util
import java.time.DayOfWeek
import java.time.LocalDate
internal fun Collection<LocalDate>.addOrRemoveIfExists(date: LocalDate) =
if (contains(date)) {
this - date
} else {
this + date
}
internal infix fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (value - other.value)) % 7
@@ -0,0 +1,19 @@
package io.github.boguszpawlowski.composecalendar.util
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.snapshotFlow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
context(LazyListState) internal fun <T> Flow<T>.throttleOnOffset() =
combine(
snapshotFlow { firstVisibleItemScrollOffset }
) { newMonth, offset ->
newMonth to (offset <= MinimalOffsetForEmit)
}.filter { (_, shouldUpdate) ->
shouldUpdate
}.map { (newValue, _) -> newValue }
private const val MinimalOffsetForEmit = 10
@@ -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)
@@ -0,0 +1,61 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import java.time.DayOfWeek
import java.time.format.TextStyle.SHORT
import java.util.Locale
import kotlin.DeprecationLevel.WARNING
@Composable
@Deprecated(
level = WARNING,
replaceWith = ReplaceWith(
"DefaultDaysOfWeekHeader(daysOfWeek, modifier)",
"io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader",
),
message = "Replace with DefaultDaysOfWeekHeader, DefaultWeekHeader will be removed in future versions"
)
@SuppressLint("NewApi")
public fun DefaultWeekHeader(
daysOfWeek: List<DayOfWeek>,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
daysOfWeek.forEach { dayOfWeek ->
Text(
textAlign = TextAlign.Center,
text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()),
modifier = modifier
.weight(1f)
.wrapContentHeight()
)
}
}
}
@Composable
@SuppressLint("NewApi")
public fun DefaultDaysOfWeekHeader(
daysOfWeek: List<DayOfWeek>,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
daysOfWeek.forEach { dayOfWeek ->
Text(
textAlign = TextAlign.Center,
text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()),
modifier = modifier
.weight(1f)
.wrapContentHeight()
)
}
}
}
internal fun <T> Array<T>.rotateRight(n: Int): List<T> = takeLast(n) + dropLast(n)
@@ -0,0 +1,55 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import io.github.boguszpawlowski.composecalendar.selection.fillUpTo
import io.github.boguszpawlowski.composecalendar.util.daysUntil
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit
import java.time.temporal.WeekFields
import java.util.Locale
@SuppressLint("NewApi")
public data class Week(
val days: List<LocalDate>,
) {
init {
require(days.size == DaysInAWeek)
}
internal constructor(firstDay: LocalDate) : this(
listOf(firstDay).fillUpTo(firstDay.plusDays((DaysInAWeek - 1).toLong()))
)
public val start: LocalDate get() = days.first()
public val end: LocalDate get() = days.last()
public val yearMonth: YearMonth = YearMonth.of(start.year, start.month)
public operator fun inc(): Week = plusWeeks(1)
public operator fun dec(): Week = plusWeeks(-1)
public operator fun compareTo(other: Week): Int = start.compareTo(other.start)
public fun minusWeeks(value: Long): Week = plusWeeks(-value)
public fun plusWeeks(value: Long): Week = copy(days = days.map { it.plusWeeks(value) })
public companion object {
public fun now(firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek): Week {
val today = LocalDate.now()
val offset = today.dayOfWeek.daysUntil(firstDayOfWeek)
val firstDay = today.minusDays(offset.toLong())
return Week(firstDay)
}
}
}
@SuppressLint("NewApi")
public fun ChronoUnit.between(first: Week, other: Week): Int =
between(first.start, other.start).toInt()
@@ -0,0 +1,10 @@
package io.github.boguszpawlowski.composecalendar.week
import androidx.compose.runtime.Immutable
import io.github.boguszpawlowski.composecalendar.day.Day
@Immutable
internal data class WeekDays(
val isFirstWeekOfTheMonth: Boolean = false,
val days: List<Day>,
)
@@ -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
}
}
@@ -0,0 +1,150 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import androidx.compose.animation.rememberSplineBasedDecay
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import dev.chrisbanes.snapper.ExperimentalSnapperApi
import dev.chrisbanes.snapper.SnapOffsets
import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults
import dev.chrisbanes.snapper.rememberSnapperFlingBehavior
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.day.WeekDay
import io.github.boguszpawlowski.composecalendar.header.WeekState
import io.github.boguszpawlowski.composecalendar.month.coerceSnapIndex
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.temporal.ChronoUnit
@OptIn(ExperimentalSnapperApi::class)
@Composable
@Suppress("LongMethod")
@SuppressLint("NewApi")
internal fun <T : SelectionState> WeekPager(
initialWeek: Week,
selectionState: T,
weekState: WeekState,
daysOfWeek: List<DayOfWeek>,
today: LocalDate,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val initialFirstVisibleItemIndex = remember(initialWeek, weekState.minWeek) {
ChronoUnit.WEEKS.between(weekState.minWeek, initialWeek)
}
val listState = rememberLazyListState(
initialFirstVisibleItemIndex = initialFirstVisibleItemIndex,
)
val flingBehavior = rememberSnapperFlingBehavior(
lazyListState = listState,
snapOffsetForItem = SnapOffsets.Start,
springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec,
decayAnimationSpec = rememberSplineBasedDecay(),
snapIndex = coerceSnapIndex,
)
val weekListState = remember {
WeekListState(
coroutineScope = coroutineScope,
weekState = weekState,
listState = listState,
)
}
Column(
modifier = modifier,
) {
if (weekDaysScrollEnabled.not()) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
content = { daysOfWeekHeader(daysOfWeek) },
)
}
val pagerCount = remember(weekState.minWeek, weekState.maxWeek) {
ChronoUnit.WEEKS.between(weekState.minWeek, weekState.maxWeek) + 1
}
LazyRow(
modifier = modifier.testTag("WeekPager"),
state = listState,
flingBehavior = flingBehavior,
verticalAlignment = Alignment.Top,
) {
items(
count = pagerCount,
key = { index -> weekListState.getWeekForPage(index).start.let { "${it.month}-${it.dayOfMonth}" } },
) { index ->
WeekContent(
modifier = Modifier.fillParentMaxWidth(),
daysOfWeek = daysOfWeek,
weekDays = weekListState.getWeekForPage(index).getWeekDays(today = today),
selectionState = selectionState,
weekDaysScrollEnabled = weekDaysScrollEnabled,
dayContent = dayContent,
daysOfWeekHeader = daysOfWeekHeader,
)
}
}
}
}
@Composable
internal fun <T : SelectionState> WeekContent(
selectionState: T,
weekDays: WeekDays,
daysOfWeek: List<DayOfWeek>,
modifier: Modifier = Modifier,
weekDaysScrollEnabled: Boolean = true,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit,
daysOfWeekHeader: @Composable BoxScope.(List<DayOfWeek>) -> Unit,
) {
Column(
modifier = modifier,
) {
if (weekDaysScrollEnabled) {
Box(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
content = { daysOfWeekHeader(daysOfWeek) },
)
}
WeekRow(
weekDays = weekDays,
modifier = Modifier.fillMaxWidth(),
selectionState = selectionState,
dayContent = dayContent,
)
}
}
internal fun Week.getWeekDays(today: LocalDate): WeekDays {
val weekDays = days.map {
WeekDay(
date = it,
isCurrentDay = it == today,
isFromCurrentMonth = true,
)
}
return WeekDays(
isFirstWeekOfTheMonth = false,
days = weekDays,
)
}
@@ -0,0 +1,35 @@
package io.github.boguszpawlowski.composecalendar.week
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.github.boguszpawlowski.composecalendar.day.DayState
import io.github.boguszpawlowski.composecalendar.selection.SelectionState
@Composable
internal fun <T : SelectionState> WeekRow(
weekDays: WeekDays,
selectionState: T,
modifier: Modifier = Modifier,
dayContent: @Composable BoxScope.(DayState<T>) -> Unit
) {
Row(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = if (weekDays.isFirstWeekOfTheMonth) Arrangement.End else Arrangement.Start
) {
weekDays.days.forEachIndexed { index, day ->
Box(
modifier = Modifier.fillMaxWidth(1f / (7 - index))
) {
dayContent(DayState(day, selectionState))
}
}
}
}
@@ -0,0 +1,52 @@
package io.github.boguszpawlowski.composecalendar.week
import android.annotation.SuppressLint
import io.github.boguszpawlowski.composecalendar.day.WeekDay
import io.github.boguszpawlowski.composecalendar.util.daysUntil
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.YearMonth
internal const val DaysInAWeek = 7
@SuppressLint("NewApi")
internal fun YearMonth.getWeeks(
includeAdjacentMonths: Boolean,
firstDayOfTheWeek: DayOfWeek,
today: LocalDate,
): List<WeekDays> {
val daysLength = lengthOfMonth()
val starOffset = atDay(1).dayOfWeek daysUntil firstDayOfTheWeek
val endOffset =
DaysInAWeek - (atDay(daysLength).dayOfWeek daysUntil firstDayOfTheWeek) - 1
return (1 - starOffset..daysLength + endOffset).chunked(DaysInAWeek).mapIndexed { index, days ->
WeekDays(
isFirstWeekOfTheMonth = index == 0,
days = days.mapNotNull { dayOfMonth ->
val (date, isFromCurrentMonth) = when (dayOfMonth) {
in Int.MIN_VALUE..0 -> if (includeAdjacentMonths) {
val previousMonth = this.minusMonths(1)
previousMonth.atDay(previousMonth.lengthOfMonth() + dayOfMonth) to false
} else {
return@mapNotNull null
}
in 1..daysLength -> atDay(dayOfMonth) to true
else -> if (includeAdjacentMonths) {
val previousMonth = this.plusMonths(1)
previousMonth.atDay(dayOfMonth - daysLength) to false
} else {
return@mapNotNull null
}
}
WeekDay(
date = date,
isFromCurrentMonth = isFromCurrentMonth,
isCurrentDay = date.equals(today),
)
}
)
}
}
@@ -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)
}
}
})
@@ -0,0 +1,68 @@
package io.github.boguszpawlowski.composecalendar.week
import io.kotest.assertions.asClue
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import java.time.LocalDate
internal class WeekTest : ShouldSpec({
val today = LocalDate.of(2000, 10, 2)
context("Secondary constructor") {
val week = Week(firstDay = today)
should("properly create an instance of week") {
week.asClue {
it.days.first() shouldBe today
it.days.last() shouldBe today.plusDays(6)
it.days.size shouldBe 7
}
}
}
context("Incrementing") {
val week = Week(firstDay = today).inc()
should("shift the days by 7") {
week.asClue {
it.days.first() shouldBe today.plusWeeks(1)
it.days.last() shouldBe today.plusDays(6).plusWeeks(1)
it.days.size shouldBe 7
}
}
}
context("Decrementing") {
val week = Week(firstDay = today).dec()
should("shift the days by -7") {
week.asClue {
it.days.first() shouldBe today.plusWeeks(-1)
it.days.last() shouldBe today.plusDays(6).plusWeeks(-1)
it.days.size shouldBe 7
}
}
}
context("Adding a week") {
val week = Week(firstDay = today).plusWeeks(7)
should("add correct number of days") {
week.asClue {
it.days.first() shouldBe today.plusWeeks(7)
it.days.last() shouldBe today.plusDays(6).plusWeeks(7)
it.days.size shouldBe 7
}
}
}
context("Constructing a week with invalid number of days") {
should("throw") {
shouldThrow<IllegalArgumentException> {
Week(List(18) { LocalDate.now() })
}
}
}
})
@@ -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
+2
View File
@@ -0,0 +1,2 @@
/build
google-services.json
+60
View File
@@ -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)
}
+21
View File
@@ -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
@@ -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")
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ComposeCalendar">
<activity
android:name="io.github.boguszpawlowski.composecalendar.sample.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -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<DayOfWeek>) {
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()
}
}
}
@@ -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<MonthSelectionState, Any> = 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<MonthSelectionState> = remember { CalendarState(monthState, selectionState) }
private val LocalDate.yearMonth: YearMonth
get() = YearMonth.of(year, month)
@@ -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<LocalDate>()) }
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<DynamicSelectionState>,
) {
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<LocalDate>) -> Unit,
dayContent: @Composable BoxScope.(KotlinDayState<DynamicSelectionState>) -> 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<T : SelectionState>(
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())
@@ -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")
}
}
}
@@ -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<EmptySelectionState>,
) {
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<EmptySelectionState>,
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<EmptySelectionState>,
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<EmptySelectionState>,
) {
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<EmptySelectionState>,
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<EmptySelectionState>,
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
)
}
}
@@ -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,
)
}
@@ -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(),
)
}
@@ -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<DynamicSelectionState>,
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<LocalDate>())
val recipesFlow = MutableStateFlow(getRecipes())
val selectedRecipesPriceFlow = recipesFlow.combine(selectionFlow) { recipes, selection ->
recipes.filter { it.date in selection }.sumOf { it.price }
}
fun onSelectionChanged(selection: List<LocalDate>) {
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()
}
}
@@ -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,
)
}
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.boguszpawlowski.composecalendar.MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ComposeCalendar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Compose Calendar</string>
</resources>
@@ -0,0 +1,16 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ComposeCalendar" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>
+4
View File
@@ -0,0 +1,4 @@
rootProject.name = "composecalendar"
include(":sample")
include(":library")
include(":kotlinx-datetime")