到底
@@ -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/
|
||||
@@ -0,0 +1,6 @@
|
||||
# Default ignored files
|
||||
/*
|
||||
!codeStyles/
|
||||
!fileTemplates/
|
||||
!file.template.settings.xml
|
||||
!.gitignore
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
</state>
|
||||
</component>
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
[<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 [Sonatype’s snapshots repository](https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/boguszpawlowski/composecalendar/).
|
||||
|
||||
### 1.3.0 Changes
|
||||
From version 1.3.0 library no longer uses `coreLibraryDesugaring` itself. If you are using it in project supporting min SDK < 26, you need to add it to prevent runtime crashes.
|
||||
```kotlin
|
||||
// app-level build.gradle
|
||||
android {
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
|
||||
}
|
||||
```
|
||||
|
||||
## Supported features
|
||||
- Selection (single, multiple or a range of days)
|
||||
- Chose day as first day of week
|
||||
- Showing/hiding adjacent months
|
||||
- Month and week headers
|
||||
- Customizable month container
|
||||
- Fully customizable day content
|
||||
- Horizontal swipe for changing a current month
|
||||
- Month / Week mode
|
||||
- Min / Max Month (or Week)
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Static calendar
|
||||
To show the basic version of the calendar, without any kind of selection mechanism, you can simply use the `StaticCalendar` composable without passing any parameters:
|
||||
```kotlin
|
||||
|
||||
@Composable
|
||||
fun MainScreen() {
|
||||
StaticCalendar()
|
||||
}
|
||||
|
||||
```
|
||||
This chunk will render the calendar with default components for each day, and also month and week headers.
|
||||
See the `StaticCalendarSample` file for a full example. For showing a week calendar, you can similarly use `StaticWeekCalendar`:
|
||||
```kotlin
|
||||
|
||||
@Composable
|
||||
fun MainScreen() {
|
||||
StaticWeekCalendar()
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
<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.
|
||||
|
After Width: | Height: | Size: 65 KiB |
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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" "$@"
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,256 @@
|
||||
public final class io/github/boguszpawlowski/composecalendar/BuildConfig {
|
||||
public static final field BUILD_TYPE Ljava/lang/String;
|
||||
public static final field DEBUG Z
|
||||
public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String;
|
||||
public fun <init> ()V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/CalendarKt {
|
||||
public static final fun Calendar (Lio/github/boguszpawlowski/composecalendar/CalendarState;Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
public static final fun SelectableCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
public static final fun StaticCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V
|
||||
public static final fun rememberCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
|
||||
public static final fun rememberSelectableCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/CalendarState {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
|
||||
public final fun getMonthState ()Lio/github/boguszpawlowski/composecalendar/header/MonthState;
|
||||
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-10 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-11 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-12 Lkotlin/jvm/functions/Function3;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-3 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-4 Lkotlin/jvm/functions/Function3;
|
||||
public static field lambda-5 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-6 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-7 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-8 Lkotlin/jvm/functions/Function3;
|
||||
public static field lambda-9 Lkotlin/jvm/functions/Function4;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-10$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-11$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-12$library_release ()Lkotlin/jvm/functions/Function3;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function3;
|
||||
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function3;
|
||||
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-3 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-4 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-5 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-6 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-7 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-8 Lkotlin/jvm/functions/Function4;
|
||||
public static field lambda-9 Lkotlin/jvm/functions/Function4;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarKt {
|
||||
public static final fun SelectableWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun StaticWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;ZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun WeekCalendar (Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;Landroidx/compose/ui/Modifier;Ljava/time/LocalDate;Ljava/time/DayOfWeek;ZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun rememberSelectableWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
|
||||
public static final fun rememberWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/WeekCalendarState {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
|
||||
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
public final fun getWeekState ()Lio/github/boguszpawlowski/composecalendar/header/WeekState;
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/day/Day {
|
||||
public abstract fun getDate ()Ljava/time/LocalDate;
|
||||
public abstract fun isCurrentDay ()Z
|
||||
public abstract fun isFromCurrentMonth ()Z
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/day/DayState : io/github/boguszpawlowski/composecalendar/day/Day {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V
|
||||
public final fun component2 ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
public final fun copy (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
|
||||
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/day/DayState;Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/day/DayState;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public fun getDate ()Ljava/time/LocalDate;
|
||||
public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;
|
||||
public fun hashCode ()I
|
||||
public fun isCurrentDay ()Z
|
||||
public fun isFromCurrentMonth ()Z
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/day/DefaultDayKt {
|
||||
public static final fun DefaultDay-t6yy7ic (Lio/github/boguszpawlowski/composecalendar/day/DayState;Landroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function2;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function2;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt {
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt;
|
||||
public static field lambda-1 Lkotlin/jvm/functions/Function2;
|
||||
public static field lambda-2 Lkotlin/jvm/functions/Function2;
|
||||
public fun <init> ()V
|
||||
public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeaderKt {
|
||||
public static final fun DefaultMonthHeader (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeaderKt {
|
||||
public static final fun DefaultWeekHeader (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/header/MonthState {
|
||||
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/MonthState$Companion;
|
||||
public abstract fun getCurrentMonth ()Ljava/time/YearMonth;
|
||||
public abstract fun getMaxMonth ()Ljava/time/YearMonth;
|
||||
public abstract fun getMinMonth ()Ljava/time/YearMonth;
|
||||
public abstract fun setCurrentMonth (Ljava/time/YearMonth;)V
|
||||
public abstract fun setMaxMonth (Ljava/time/YearMonth;)V
|
||||
public abstract fun setMinMonth (Ljava/time/YearMonth;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/MonthState$Companion {
|
||||
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/MonthStateKt {
|
||||
public static final fun MonthState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/header/MonthState;
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/header/WeekState {
|
||||
public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/WeekState$Companion;
|
||||
public abstract fun getCurrentWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public abstract fun getMaxWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public abstract fun getMinWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public abstract fun setCurrentWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
|
||||
public abstract fun setMaxWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
|
||||
public abstract fun setMinWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/WeekState$Companion {
|
||||
public final fun Saver ()Landroidx/compose/runtime/saveable/Saver;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/header/WeekStateKt {
|
||||
public static final fun WeekState (Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)Lio/github/boguszpawlowski/composecalendar/header/WeekState;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler {
|
||||
public static final field $stable I
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler;
|
||||
public final fun calculateNewSelection (Ljava/time/LocalDate;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)Ljava/util/List;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
|
||||
public static final field $stable I
|
||||
public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
|
||||
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun getSelection ()Ljava/util/List;
|
||||
public final fun getSelectionMode ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public fun isDateSelected (Ljava/time/LocalDate;)Z
|
||||
public fun onDateSelected (Ljava/time/LocalDate;)V
|
||||
public final fun setSelection (Ljava/util/List;)V
|
||||
public final fun setSelectionMode (Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/EmptySelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
|
||||
public static final field $stable I
|
||||
public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/EmptySelectionState;
|
||||
public fun isDateSelected (Ljava/time/LocalDate;)Z
|
||||
public fun onDateSelected (Ljava/time/LocalDate;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionMode : java/lang/Enum {
|
||||
public static final field Multiple Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static final field None Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static final field Period Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static final field Single Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static fun getEntries ()Lkotlin/enums/EnumEntries;
|
||||
public static fun valueOf (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
public static fun values ()[Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
|
||||
}
|
||||
|
||||
public abstract interface class io/github/boguszpawlowski/composecalendar/selection/SelectionState {
|
||||
public abstract fun isDateSelected (Ljava/time/LocalDate;)Z
|
||||
public abstract fun onDateSelected (Ljava/time/LocalDate;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/selection/SelectionState$DefaultImpls {
|
||||
public static fun isDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)Z
|
||||
public static fun onDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeaderKt {
|
||||
public static final fun DefaultDaysOfWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
public static final fun DefaultWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/Week {
|
||||
public static final field $stable I
|
||||
public static final field Companion Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;
|
||||
public fun <init> (Ljava/util/List;)V
|
||||
public final fun compareTo (Lio/github/boguszpawlowski/composecalendar/week/Week;)I
|
||||
public final fun component1 ()Ljava/util/List;
|
||||
public final fun copy (Ljava/util/List;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public final fun dec ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public fun equals (Ljava/lang/Object;)Z
|
||||
public final fun getDays ()Ljava/util/List;
|
||||
public final fun getEnd ()Ljava/time/LocalDate;
|
||||
public final fun getStart ()Ljava/time/LocalDate;
|
||||
public final fun getYearMonth ()Ljava/time/YearMonth;
|
||||
public fun hashCode ()I
|
||||
public final fun inc ()Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public final fun minusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public final fun plusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public fun toString ()Ljava/lang/String;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/Week$Companion {
|
||||
public final fun now (Ljava/time/DayOfWeek;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
public static synthetic fun now$default (Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;Ljava/time/DayOfWeek;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week;
|
||||
}
|
||||
|
||||
public final class io/github/boguszpawlowski/composecalendar/week/WeekKt {
|
||||
public static final fun between (Ljava/time/temporal/ChronoUnit;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)I
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id(Android.LibraryPluginId)
|
||||
kotlin(Kotlin.AndroidPluginId)
|
||||
id("common-android-plugin")
|
||||
id(MavenPublish.PluginId)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.github.boguszpawlowski.composecalendar"
|
||||
tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + "-Xexplicit-api=strict" + "-Xcontext-receivers"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(Compose.Ui)
|
||||
implementation(Compose.UiTooling)
|
||||
implementation(Compose.AccompanistPager)
|
||||
implementation(Compose.Foundation)
|
||||
implementation(Compose.FoundationLayout)
|
||||
implementation(Compose.Material)
|
||||
implementation(Timber.Core)
|
||||
|
||||
testImplementation(Kotest.Assertions)
|
||||
testImplementation(Kotest.RunnerJunit5)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
POM_ARTIFACT_ID=composecalendar
|
||||
POM_DESCRIPTION=Library for handling the Calendar view in Jetpack Compose.
|
||||
POM_PACKAGING=aar
|
||||
SONATYPE_HOST=S01
|
||||
RELEASE_SIGNING_ENABLED=true
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
google-services.json
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
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>
|
||||
@@ -0,0 +1,4 @@
|
||||
rootProject.name = "composecalendar"
|
||||
include(":sample")
|
||||
include(":library")
|
||||
include(":kotlinx-datetime")
|
||||