From 63bb06ff630d575506d5b4a6d474e39c1ea25261 Mon Sep 17 00:00:00 2001 From: coco <2927951@qq.com> Date: Thu, 16 Jul 2026 09:43:06 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=B0=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ComposeCalendar/.gitignore | 71 +++++ ComposeCalendar/.idea/.gitignore | 6 + ComposeCalendar/.idea/codeStyles/Project.xml | 133 ++++++++ .../.idea/codeStyles/codeStyleConfig.xml | 5 + ComposeCalendar/LICENSE | 201 ++++++++++++ ComposeCalendar/README.md | 250 +++++++++++++++ ComposeCalendar/blob/screenshot_1.jpg | Bin 0 -> 66663 bytes ComposeCalendar/build.gradle.kts | 115 +++++++ ComposeCalendar/buildSrc/build.gradle.kts | 22 ++ .../src/main/kotlin/CommonAndroidPlugin.kt | 32 ++ .../buildSrc/src/main/kotlin/Dependencies.kt | 141 +++++++++ ComposeCalendar/detekt-config.yml | 294 ++++++++++++++++++ ComposeCalendar/gradle.properties | 27 ++ ComposeCalendar/gradle/dependencies.gradle | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43462 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + ComposeCalendar/gradlew | 249 +++++++++++++++ ComposeCalendar/gradlew.bat | 92 ++++++ ComposeCalendar/kotlinx-datetime/.gitignore | 1 + .../kotlinx-datetime/api/kotlinx-datetime.api | 37 +++ .../kotlinx-datetime/build.gradle.kts | 15 + .../kotlinx-datetime/gradle.properties | 5 + .../kotlinxDateTime/Converters.kt | 5 + .../kotlinxDateTime/DateTimeExtensions.kt | 15 + .../kotlinxDateTime/YearMonth.kt | 79 +++++ .../kotlinxDateTime/YearMonthTest.kt | 128 ++++++++ ComposeCalendar/library/.gitignore | 1 + ComposeCalendar/library/api/library.api | 256 +++++++++++++++ ComposeCalendar/library/build.gradle.kts | 30 ++ ComposeCalendar/library/gradle.properties | 5 + .../library/src/main/AndroidManifest.xml | 4 + .../composecalendar/Calendar.kt | 286 +++++++++++++++++ .../composecalendar/WeekCalendar.kt | 264 ++++++++++++++++ .../composecalendar/day/Day.kt | 16 + .../composecalendar/day/DayState.kt | 16 + .../composecalendar/day/DefaultDay.kt | 63 ++++ .../composecalendar/day/WeekDay.kt | 11 + .../debug/RecomposeHighlighter.kt | 97 ++++++ .../header/DefaultMonthHeader.kt | 87 ++++++ .../header/DefaultWeekHeader.kt | 91 ++++++ .../composecalendar/header/MonthState.kt | 76 +++++ .../composecalendar/header/WeekState.kt | 86 +++++ .../composecalendar/month/MonthContent.kt | 162 ++++++++++ .../composecalendar/month/MonthListState.kt | 74 +++++ .../selection/DynamicSelectionHandler.kt | 35 +++ .../selection/SelectionExtensions.kt | 15 + .../selection/SelectionMode.kt | 15 + .../selection/SelectionState.kt | 83 +++++ .../composecalendar/util/DateUtil.kt | 13 + .../util/ListStateExtensions.kt | 19 ++ .../util/YearMonthExtensions.kt | 7 + .../composecalendar/week/DefaultWeekHeader.kt | 61 ++++ .../composecalendar/week/Week.kt | 55 ++++ .../composecalendar/week/WeekDays.kt | 10 + .../composecalendar/week/WeekListState.kt | 70 +++++ .../composecalendar/week/WeekPager.kt | 150 +++++++++ .../composecalendar/week/WeekRow.kt | 35 +++ .../composecalendar/week/WeekUtil.kt | 52 ++++ .../selection/SelectionStateTest.kt | 177 +++++++++++ .../composecalendar/week/WeekTest.kt | 68 ++++ .../composecalendar/week/WeekUtilTest.kt | 166 ++++++++++ ComposeCalendar/sample/.gitignore | 2 + ComposeCalendar/sample/build.gradle.kts | 60 ++++ ComposeCalendar/sample/proguard-rules.pro | 21 ++ .../sample/ScrollBehaviorTest.kt | 50 +++ .../sample/src/main/AndroidManifest.xml | 24 ++ .../sample/CustomComponentsSample.kt | 104 +++++++ .../sample/CustomSelectionSample.kt | 66 ++++ .../sample/KotlinDateTimeSample.kt | 103 ++++++ .../composecalendar/sample/MainActivity.kt | 113 +++++++ .../sample/MinMaxCalendarSample.kt | 268 ++++++++++++++++ .../sample/SelectableCalendarSample.kt | 57 ++++ .../sample/StaticCalendarSample.kt | 17 + .../composecalendar/sample/ViewModelSample.kt | 199 ++++++++++++ .../sample/WeekCalendarSample.kt | 57 ++++ .../drawable-v24/ic_launcher_foreground.xml | 30 ++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++ .../src/main/res/layout/activity_main.xml | 18 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3593 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5339 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2636 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 3388 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4926 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7472 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7909 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 11873 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10652 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 16570 bytes .../src/main/res/values-night/themes.xml | 16 + .../sample/src/main/res/values/colors.xml | 10 + .../sample/src/main/res/values/strings.xml | 3 + .../sample/src/main/res/values/themes.xml | 16 + ComposeCalendar/settings.gradle.kts | 4 + 95 files changed, 6004 insertions(+) create mode 100644 ComposeCalendar/.gitignore create mode 100644 ComposeCalendar/.idea/.gitignore create mode 100644 ComposeCalendar/.idea/codeStyles/Project.xml create mode 100644 ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml create mode 100644 ComposeCalendar/LICENSE create mode 100644 ComposeCalendar/README.md create mode 100644 ComposeCalendar/blob/screenshot_1.jpg create mode 100644 ComposeCalendar/build.gradle.kts create mode 100644 ComposeCalendar/buildSrc/build.gradle.kts create mode 100644 ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt create mode 100644 ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt create mode 100644 ComposeCalendar/detekt-config.yml create mode 100644 ComposeCalendar/gradle.properties create mode 100644 ComposeCalendar/gradle/dependencies.gradle create mode 100644 ComposeCalendar/gradle/wrapper/gradle-wrapper.jar create mode 100644 ComposeCalendar/gradle/wrapper/gradle-wrapper.properties create mode 100644 ComposeCalendar/gradlew create mode 100644 ComposeCalendar/gradlew.bat create mode 100644 ComposeCalendar/kotlinx-datetime/.gitignore create mode 100644 ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api create mode 100644 ComposeCalendar/kotlinx-datetime/build.gradle.kts create mode 100644 ComposeCalendar/kotlinx-datetime/gradle.properties create mode 100644 ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt create mode 100644 ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt create mode 100644 ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt create mode 100644 ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt create mode 100644 ComposeCalendar/library/.gitignore create mode 100644 ComposeCalendar/library/api/library.api create mode 100644 ComposeCalendar/library/build.gradle.kts create mode 100644 ComposeCalendar/library/gradle.properties create mode 100644 ComposeCalendar/library/src/main/AndroidManifest.xml create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt create mode 100644 ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt create mode 100644 ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt create mode 100644 ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt create mode 100644 ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt create mode 100644 ComposeCalendar/sample/.gitignore create mode 100644 ComposeCalendar/sample/build.gradle.kts create mode 100644 ComposeCalendar/sample/proguard-rules.pro create mode 100644 ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt create mode 100644 ComposeCalendar/sample/src/main/AndroidManifest.xml create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt create mode 100644 ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt create mode 100644 ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml create mode 100644 ComposeCalendar/sample/src/main/res/layout/activity_main.xml create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 ComposeCalendar/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 ComposeCalendar/sample/src/main/res/values-night/themes.xml create mode 100644 ComposeCalendar/sample/src/main/res/values/colors.xml create mode 100644 ComposeCalendar/sample/src/main/res/values/strings.xml create mode 100644 ComposeCalendar/sample/src/main/res/values/themes.xml create mode 100644 ComposeCalendar/settings.gradle.kts diff --git a/ComposeCalendar/.gitignore b/ComposeCalendar/.gitignore new file mode 100644 index 000000000..14a4e7e33 --- /dev/null +++ b/ComposeCalendar/.gitignore @@ -0,0 +1,71 @@ +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ diff --git a/ComposeCalendar/.idea/.gitignore b/ComposeCalendar/.idea/.gitignore new file mode 100644 index 000000000..e9dc156a8 --- /dev/null +++ b/ComposeCalendar/.idea/.gitignore @@ -0,0 +1,6 @@ +# Default ignored files +/* +!codeStyles/ +!fileTemplates/ +!file.template.settings.xml +!.gitignore diff --git a/ComposeCalendar/.idea/codeStyles/Project.xml b/ComposeCalendar/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..55e76659b --- /dev/null +++ b/ComposeCalendar/.idea/codeStyles/Project.xml @@ -0,0 +1,133 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml b/ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/ComposeCalendar/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/ComposeCalendar/LICENSE b/ComposeCalendar/LICENSE new file mode 100644 index 000000000..4ad935286 --- /dev/null +++ b/ComposeCalendar/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 Bogusz Pawłowski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ComposeCalendar/README.md b/ComposeCalendar/README.md new file mode 100644 index 000000000..68b23a599 --- /dev/null +++ b/ComposeCalendar/README.md @@ -0,0 +1,250 @@ + + +Compose Calendar is a composable handling all complexity of rendering calendar component and date selection. +Due to flexibility provided by slot API's, you can decide how the calendar will look like, the library will handle proper calendar elements arrangement and it's state. + +![Github Actions](https://github.com/boguszpawlowski/composecalendar/actions/workflows/publish-snapshot.yml/badge.svg?branch=main) +[](https://search.maven.org/search?q=g:io.github.boguszpawlowski.composecalendar) +[](https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/boguszpawlowski/composecalendar/) +[](https://androidweekly.net/issues/issue-501/) +## Setup +Library is available on Maven Central repository. +```kotlin + // module-level build.gradle + dependecies { + implementation "io.github.boguszpawlowski.composecalendar:composecalendar:" + + // separate artifact with utilities for working with kotlinx-datetime + implementation "io.github.boguszpawlowski.composecalendar:kotlinx-datetime:" + } +``` +Snapshots are available on [Sonatype’s snapshots repository](https://s01.oss.sonatype.org/content/repositories/snapshots/io/github/boguszpawlowski/composecalendar/). + +### 1.3.0 Changes +From version 1.3.0 library no longer uses `coreLibraryDesugaring` itself. If you are using it in project supporting min SDK < 26, you need to add it to prevent runtime crashes. +```kotlin + // app-level build.gradle + android { + compileOptions { + coreLibraryDesugaringEnabled true + } + kotlinOptions { + jvmTarget = "1.8" + } + } + dependencies { + coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4" + } +``` + +## Supported features +- Selection (single, multiple or a range of days) +- Chose day as first day of week +- Showing/hiding adjacent months +- Month and week headers +- Customizable month container +- Fully customizable day content +- Horizontal swipe for changing a current month +- Month / Week mode +- Min / Max Month (or Week) + +## Basic Usage + +### Static calendar +To show the basic version of the calendar, without any kind of selection mechanism, you can simply use the `StaticCalendar` composable without passing any parameters: +```kotlin + + @Composable + fun MainScreen() { + StaticCalendar() + } + +``` +This chunk will render the calendar with default components for each day, and also month and week headers. +See the `StaticCalendarSample` file for a full example. For showing a week calendar, you can similarly use `StaticWeekCalendar`: +```kotlin + + @Composable + fun MainScreen() { + StaticWeekCalendar() + } + +``` + + + +> :exclamation: By default, at first the calendar will show current month. If you want to start with some different date, you have to pass an `initialMonth` parameter to the initial state of the calendar. See [Initial State section](#initial-state) + +### Selectable calendar +Calendar with a mechanism for selection. The default implementation uses `DynamicSelectionState` (see [Dynamic Selection section](#dynamic-selection-state)) which allows to change `SelectionMode` in the runtime. +```kotlin + + @Composable + fun MainScreen() { + SelectableCalendar() + } + +``` +By the default, after changing the selection mode, selection is cleared. +See the `SelectableCalendarSample` file for a full example. For showing a week calendar, you can similarly use `SelectableWeekCalendar`: +```kotlin + + @Composable + fun MainScreen() { + SelectableWeekCalendar() + } + +``` +See the `WeekCalendarSample` file for a full example. + +https://user-images.githubusercontent.com/36514058/126049987-685a7b81-1596-48a4-95a1-c342f6c796bf.mp4 + +> :exclamation: If you want to define your own selection behavior, please check out the [Custom Selection section](#custom-selection) and/or `CustomSelectionSample`. + +### Calendar with custom components +For the customization you should pass your own composable functions as day content, moth header etc.: +```kotlin + + @Composable + fun MyDay(dayState: DayState) { + Text(dayState.date.dayOfMonth.toString()) + } + + @Composable + fun MainScreen() { + StaticCalendar( + dayContent = { dayState -> MyDay(dayState) } + ) + } + +``` +The same you can do for every customizable element: +- Day content - responsible for single day content +- Month header - responsible for showing the current month (and by default for changing the current month) +- Week header - responsible for showing the names of week days. +- Month container - wrapping the month content, it defaults to a plain `Box`, but can be any layout. + +The `Calendar` composable accepts a `Modifier` for simple customization of the overall appearance. +See the `CustomComponentsSample` for a full example. + +### Custom selection +As the selection state is represented by an interface, you can provide your own implementation, to suit your +use-case. E.g: +```kotlin + class MonthSelectionState( + initialSelection: YearMonth? = null, + ) : SelectionState { + private var selection by mutableStateOf(initialSelection) + + override fun isDateSelected(date: LocalDate): Boolean = + date.yearMonth == selection + + override fun onDateSelected(date: LocalDate) { + selection = if (date.yearMonth == selection) null else date.yearMonth + } + } +``` +To use the defined selection state, you have to pass it into a generic version of `Calendar` composable. +This chunk is an implementation that will select all days in a clicked day's month. For a full example +please check out `CustomSelectionSample` file. + +### Calendar properties customization +Apart from rendering your own components inside the calendar, you can modify it by passing different properties.: +- `showAdjacentMonths` - whenever to render days from adjacent months. Defaults to `true`. +- `firstDayOfWeek` - you can pass the `DayOfWeek` which you want you week to start with. It defaults to the first day of week of the `Locale.default()`. +- `horizontalScrollEnabled` - a Boolean flag which enables month to be changed by a horizontal swipe. Defaults to `true`. +- `minMonth` - a `YearMonth` object representing the minimum month that can be shown in the calendar. By default there is no minimum month. +- `maxMonth` - a `YearMonth` object representing the maximum month that can be shown in the calendar. By default there is no maximum month. + +> :exclamation: You cannot set `minMonth` to be lower than `maxMonth` and vice versa. If you do so, the calendar state won't change. + +Apart from this, `Calendar` you can pass a `Modifier` object like in any other composable. + +## State +Calendar composable holds its state as an `CalendarState` object, which consists of 2 properties. +- `MonthState` - current value of the presented month. +- `SelectionState` - current value of the selection. + +Both properties are represented by interfaces, so the default implementation can be overwritten if needed. +The calendar state is leveraging Compose saving mechanism, so that the state will survive any configuration change, or the process death. + +### Initial state +Initial state for the static calendar is provided by the `rememberCalendarState()` function. If you need to change the initial conditions, you can pass the params to it: + +```kotlin + + @Composable + fun MainScreen() { + StaticCalendar( + calendarState = rememberCalendarState( + initialMonth = YearMonth.now().plusYears(1), + ) + ) + } + +``` +In case of the selectable calendar, the state has additional parameters, used to calculate the initial selection: + +```kotlin + + @Composable + fun MainScreen() { + SelectableCalendar( + calendarState = rememberSelectableCalendarState( + initialMonth = YearMonth.now().plusYears(1), + initialSelection = listOf(LocalDate.parse("20-01-2020")), + initialSelectionMode = SelectionMode.Period, + ) + ) + } + +``` + +### State hoisting +In case you need to react to the state changes, or change the state from the outside of the composable, +you need to hoist the state out of the `Calendar` composable: + +```kotlin + + @Composable + fun MainScreen() { + val calendarState = rememberCalendarState() + StaticCalendar(calendarState = calendarState) + + // now you can manipulate the state from scope of this composable + calendarState.monthState.currentMonth = YearMonth.of(2020, 5) + } + +``` + +### Dynamic Selection State +By default, the `SelectableCalendar` is using a `DynamicSelectionState` implementation of `SelectionState`. The selection is kept as a list of `LocalDate` objects. For a purpose of flexibility, `DynamicSelectionState` allows for 4 different selection modes, each one varying how the selection is changing after interacting with the calendar. Furthermore, selection mode can be changed in the runtime, for some specific use-cases. +Selection modes are represented by `SelectionMode` enum, with following values: +- `None` - no selection allowed - selection will always be an empty list. +- `Single` - only single day is selectable - selection will contain one or zero days selected. +- `Multiple` - a list of dates can be selected. +- `Period` - selectable period - implemented by `start` and `end` dates. - selection will contain all dates between start and the end date. +This implementation of SelectionState also allows for handling side-effects and vetoing the state change via `confirmSelectionChange` callback. + +## Week Calendar +Apart from the default calendar, there is also a week calendar, which shows a single week at a time. It can be used in the same way as the default calendar, and has the same customization options. + +## KotlinX DateTime +As the core of the library is built on `java.time` library, on Android SDK < 26 it requires to use [core libary desugaring](https://developer.android.com/studio/write/java8-support) to access it's API. +As a result it's features may be unavailable to some project built around different date-time libraries (e.g. kotlinx-datetime). Although the project wont be migrating from `java.time`, as it's the best suited for it, there is a separate `kotlinx-datetime` artifact for those who need to use the library from a codebase based on it. It doesn't consist of a separate version of `ComposeCalendar` features, but offers a small bunch of utilities, that will enable you to create your own wrapper, as briefly presented in `KotlinDateTimeSample`. If the provided functionality, doesn't match your use-case, please submit an issue. + +## License + + Copyright 2024 Bogusz Pawłowski + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ComposeCalendar/blob/screenshot_1.jpg b/ComposeCalendar/blob/screenshot_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bb37214fdcfb3b2b06897ef7c9be0dd03043491 GIT binary patch literal 66663 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 BlVJb= literal 0 HcmV?d00001 diff --git a/ComposeCalendar/build.gradle.kts b/ComposeCalendar/build.gradle.kts new file mode 100644 index 000000000..057fb2192 --- /dev/null +++ b/ComposeCalendar/build.gradle.kts @@ -0,0 +1,115 @@ +import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id(DetektLib.PluginId) version DetektLib.Version + id(GradleVersions.PluginId) version GradleVersions.Version + id(GrGit.PluginId) version GrGit.Version + id(Shipkit.AutoVersion.PluginId) version Shipkit.AutoVersion.Version + id(Shipkit.Changelog.PluginId) version Shipkit.Changelog.Version + id(Shipkit.GithubRelease.PluginId) version Shipkit.GithubRelease.Version + id(Kotlin.CompatibilityPlugin) version Kotlin.CompatibilityPluginVersion +} + +buildscript { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } + dependencies { + classpath(Android.GradlePlugin) + classpath(Kotlin.GradlePlugin) + classpath(Kotlin.DokkaGradlePlugin) + classpath(DetektLib.Plugin) + classpath(GradleVersions.Plugin) + classpath(MavenPublish.GradlePlugin) + classpath(Firebase.GoogleServicesPlugin) + } +} + +allprojects { + repositories { + mavenCentral() + gradlePluginPortal() + google() + mavenLocal() + } + + tasks.withType { + kotlinOptions { + jvmTarget = "17" + freeCompilerArgs = freeCompilerArgs + listOf( + "-progressive", + "-Xopt-in=kotlin.RequiresOptIn", + "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xuse-experimental=kotlin.contracts.ExperimentalContracts", + ) + } + } + + tasks.withType { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } + } +} + +dependencies { + detekt(DetektLib.Formatting) + detekt(DetektLib.Cli) +} + +apiValidation { + ignoredProjects.add("sample") +} + +tasks { + withType { + parallel = true + config.setFrom(rootProject.file("detekt-config.yml")) + setSource(files(projectDir)) + exclude(subprojects.map { "${it.buildDir.relativeTo(rootDir).path}/" }) + exclude("**/.gradle/**") + reports { + xml { + required.set(true) + outputLocation.set(file("build/reports/detekt/detekt-results.xml")) + } + html.required.set(false) + txt.required.set(false) + } + } + + register("check") { + group = "Verification" + description = "Allows to attach Detekt to the root project." + } + + withType { + rejectVersionIf { + isNonStable(candidate.version) && !isNonStable(currentVersion) + } + } + + withType(org.shipkit.changelog.GenerateChangelogTask::class) { + previousRevision = project.ext["shipkit-auto-version.previous-tag"] as String? + githubToken = System.getenv("GITHUB_TOKEN") + repository = "boguszpawlowski/composecalendar" + } + + withType(org.shipkit.github.release.GithubReleaseTask::class) { + dependsOn(named("generateChangelog")) + repository = "boguszpawlowski/composecalendar" + changelog = named("generateChangelog").get().outputs.files.singleFile + githubToken = System.getenv("GITHUB_TOKEN") + newTagRevision = System.getenv("GITHUB_SHA") + } +} + +fun isNonStable(version: String): Boolean { + val regex = "^[0-9,.v-]+(-r)?$".toRegex() + return !regex.matches(version) +} diff --git a/ComposeCalendar/buildSrc/build.gradle.kts b/ComposeCalendar/buildSrc/build.gradle.kts new file mode 100644 index 000000000..d8ab500d1 --- /dev/null +++ b/ComposeCalendar/buildSrc/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + google() +} + +gradlePlugin { + plugins { + register("common-android-plugin") { + id = "common-android-plugin" + implementationClass = "CommonAndroidPlugin" + } + } +} + +dependencies { + implementation("com.android.tools.build:gradle:8.2.0") + implementation(kotlin("gradle-plugin", "1.9.20")) +} diff --git a/ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt b/ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt new file mode 100644 index 000000000..a4c3e2d1d --- /dev/null +++ b/ComposeCalendar/buildSrc/src/main/kotlin/CommonAndroidPlugin.kt @@ -0,0 +1,32 @@ +import com.android.build.gradle.BaseExtension +import org.gradle.api.JavaVersion.VERSION_17 +import org.gradle.api.Plugin +import org.gradle.api.Project + +class CommonAndroidPlugin : Plugin { + + @Suppress("LongMethod") + override fun apply(target: Project) { + val androidExtension = target.extensions.getByName("android") + + (androidExtension as? BaseExtension)?.apply { + compileSdkVersion(AndroidSdk.Compile) + defaultConfig { + minSdk = AndroidSdk.Min + targetSdk = AndroidSdk.Target + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + compileOptions { + sourceCompatibility = VERSION_17 + targetCompatibility = VERSION_17 + } + + buildFeatures.compose = true + + composeOptions { + kotlinCompilerExtensionVersion = Compose.CompilerVersion + } + } + } +} diff --git a/ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt b/ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt new file mode 100644 index 000000000..16c4ae3cf --- /dev/null +++ b/ComposeCalendar/buildSrc/src/main/kotlin/Dependencies.kt @@ -0,0 +1,141 @@ +@file:Suppress("ObjectPropertyNaming", "ClassNaming", "UnderscoresInNumericLiterals") +object MavenPublish { + const val PluginId = "com.vanniktech.maven.publish" + const val GradlePlugin = "com.vanniktech:gradle-maven-publish-plugin:0.27.0" +} + +object AndroidSdk { + const val Min = 21 + const val Compile = 33 + const val Target = Compile +} + +object Kotlin { + const val Version = "1.9.21" + const val CompatibilityPluginVersion = "0.13.2" + + const val GradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$Version" + const val DokkaGradlePlugin = "org.jetbrains.dokka:dokka-gradle-plugin:1.9.10" + + const val AndroidPluginId = "android" + const val JvmPluginId = "jvm" + + const val CompatibilityPlugin = "org.jetbrains.kotlinx.binary-compatibility-validator" + const val CompatibilityPluginId = "binary-compatibility-validator" + + const val DesugarJdkLibs = "com.android.tools:desugar_jdk_libs:2.0.4" + const val DateTime = "org.jetbrains.kotlinx:kotlinx-datetime:0.5.0" +} + +object Android { + const val GradlePlugin = "com.android.tools.build:gradle:8.2.0" + + const val ApplicationPluginId = "com.android.application" + const val LibraryPluginId = "com.android.library" +} + +object GradleVersions { + const val Version = "0.42.0" + + const val PluginId = "com.github.ben-manes.versions" + const val Plugin = "com.github.ben-manes:gradle-versions-plugin:$Version" +} + +object GrGit { + const val Version = "5.0.0" + + const val PluginId = "org.ajoberstar.grgit" +} + +object Shipkit { + object AutoVersion { + const val Version = "1.1.19" + + const val PluginId = "org.shipkit.shipkit-auto-version" + } + + object Changelog { + const val Version = "1.1.15" + + const val PluginId = "org.shipkit.shipkit-changelog" + } + + object GithubRelease { + const val Version = "1.1.15" + + const val PluginId = "org.shipkit.shipkit-github-release" + } +} + +object AndroidX { + const val Version = "1.0.0" + const val LifecycleVersion = "2.2.0" + + const val AppCompat = "androidx.appcompat:appcompat:1.6.1" + const val ComposeActivity = "androidx.activity:activity-compose:1.6.1" +} + +object Material { + const val Core = "com.google.android.material:material:1.5.0" +} + +object DetektLib { + const val Version = "1.23.4" + + const val PluginId = "io.gitlab.arturbosch.detekt" + const val Plugin = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:$Version" + + const val Formatting = "io.gitlab.arturbosch.detekt:detekt-formatting:$Version" + const val Cli = "io.gitlab.arturbosch.detekt:detekt-cli:$Version" +} + +object Timber { + const val Version = "5.0.1" + const val Core = "com.jakewharton.timber:timber:$Version" +} + +object Compose { + const val Version = "1.3.1" + const val CompilerVersion = "1.5.5" + const val AccompanistVersion = "0.25.1" + + const val Runtime = "androidx.compose.runtime:runtime:$Version" + const val Compiler = "androidx.compose.compiler:compiler:$CompilerVersion" + const val Foundation = "androidx.compose.foundation:foundation:$Version" + const val FoundationLayout = "androidx.compose.foundation:foundation-layout:$Version" + const val Material = "androidx.compose.material:material:$Version" + const val Ui = "androidx.compose.ui:ui:$Version" + const val UiTooling = "androidx.compose.ui:ui-tooling:$Version" + const val AccompanistPager = "com.google.accompanist:accompanist-pager:$AccompanistVersion" + const val Navigation = "androidx.navigation:navigation-compose:2.5.0-alpha03" +} + +object Debug { + const val LeakCanary = "com.squareup.leakcanary:leakcanary-android:2.7" +} + +object Hyperion { + private const val Version = "0.9.34" + const val Core = "com.willowtreeapps.hyperion:hyperion-core:$Version" + const val Crash = "com.willowtreeapps.hyperion:hyperion-crash:$Version" + const val GeigerCounter = "com.willowtreeapps.hyperion:hyperion-geiger-counter:$Version" + const val Measurement = "com.willowtreeapps.hyperion:hyperion-measurement:$Version" +} + +object Kotest { + const val Version = "5.4.2" + + const val RunnerJunit5 = "io.kotest:kotest-runner-junit5-jvm:$Version" + + const val Assertions = "io.kotest:kotest-assertions-core-jvm:$Version" +} + +object ComposeTest { + const val Core = "androidx.compose.ui:ui-test-junit4:${Compose.Version}" + const val Manifest = "androidx.compose.ui:ui-test-manifest:${Compose.Version}" +} + +object Firebase { + const val Bom = "com.google.firebase:firebase-bom:30.0.0" + const val GoogleServicesPlugin = "com.google.gms:google-services:4.3.10" +} diff --git a/ComposeCalendar/detekt-config.yml b/ComposeCalendar/detekt-config.yml new file mode 100644 index 000000000..92c2fccc9 --- /dev/null +++ b/ComposeCalendar/detekt-config.yml @@ -0,0 +1,294 @@ +build: + maxIssues: 0 + weights: + +complexity: + active: true + ComplexCondition: + active: true + threshold: 3 + ComplexInterface: + active: true + ComplexMethod: + active: true + threshold: 10 + ignoreSingleWhenExpression: true + LargeClass: + active: true + threshold: 400 + LongMethod: + active: true + threshold: 40 + LongParameterList: + active: true + functionThreshold: 10 + constructorThreshold: 5 + ignoreAnnotated: [ 'Composable' ] + NestedBlockDepth: + active: true + StringLiteralDuplication: + active: false + +empty-blocks: + active: true + EmptyCatchBlock: + active: true + EmptyClassBlock: + active: true + EmptyDefaultConstructor: + active: true + EmptyDoWhileBlock: + active: true + EmptyElseBlock: + active: true + EmptyFinallyBlock: + active: true + EmptyForBlock: + active: true + EmptyFunctionBlock: + active: true + EmptyIfBlock: + active: true + EmptyInitBlock: + active: true + EmptyKtFile: + active: true + EmptySecondaryConstructor: + active: true + EmptyWhenBlock: + active: true + EmptyWhileBlock: + active: true + +exceptions: + active: true + ExceptionRaisedInUnexpectedLocation: + active: true + methodNames: 'toString,hashCode,equals,finalize' + InstanceOfCheckForException: + active: true + NotImplementedDeclaration: + active: true + PrintStackTrace: + active: true + RethrowCaughtException: + active: true + ReturnFromFinally: + active: true + SwallowedException: + active: true + ThrowingExceptionFromFinally: + active: true + ThrowingExceptionsWithoutMessageOrCause: + active: true + ThrowingNewInstanceOfSameException: + active: true + +formatting: + active: true + ChainWrapping: + active: true + CommentSpacing: + active: true + ImportOrdering: + active: true + Indentation: + active: true + indentSize: 2 + continuationIndentSize: 4 + NoBlankLineBeforeRbrace: + active: true + NoConsecutiveBlankLines: + active: true + NoLineBreakBeforeAssignment: + active: true + NoMultipleSpaces: + active: true + NoSemicolons: + active: true + NoTrailingSpaces: + active: true + ParameterListWrapping: + active: true + indentSize: 2 + SpacingAroundColon: + active: true + SpacingAroundComma: + active: true + SpacingAroundCurly: + active: true + SpacingAroundKeyword: + active: true + SpacingAroundOperators: + active: true + SpacingAroundRangeOperator: + active: true + StringTemplate: + active: true + +naming: + active: true + ClassNaming: + active: true + classPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' + ConstructorParameterNaming: + active: true + parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + privateParameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + EnumNaming: + active: true + enumEntryPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' + FunctionNaming: + active: false + functionPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + ignoreAnnotated: [ 'Composable' ] + FunctionParameterNaming: + active: true + parameterPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + MatchingDeclarationName: + active: true + MemberNameEqualsClassName: + active: true + ObjectPropertyNaming: + active: false + constantPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' + propertyPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' + privatePropertyPattern: '^[A-Z][a-z]+(?:[A-Z][a-z]+)*$' + PackageNaming: + active: true + packagePattern: '^[a-z]+(\.[a-z][a-zA-Z]*)*$' + TopLevelPropertyNaming: + active: false + constantPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + propertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + privatePropertyPattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + VariableNaming: + active: true + variablePattern: '^[a-z]+(?:[A-Z][a-z]+)*$' + privateVariablePattern: '^[_a-z]+(?:[A-Z][a-z]+)*$' + +performance: + active: true + ForEachOnRange: + active: true + UnnecessaryTemporaryInstantiation: + active: true + +potential-bugs: + active: true + DuplicateCaseInWhenExpression: + active: true + EqualsAlwaysReturnsTrueOrFalse: + active: false + EqualsWithHashCodeExist: + active: true + ExplicitGarbageCollectionCall: + active: true + InvalidRange: + active: true + IteratorHasNextCallsNextMethod: + active: true + IteratorNotThrowingNoSuchElementException: + active: true + MissingWhenCase: + active: true + RedundantElseInWhen: + active: true + UnconditionalJumpStatementInLoop: + active: true + UnreachableCode: + active: true + UselessPostfixExpression: + active: true + WrongEqualsTypeParameter: + active: true + +style: + active: true + CollapsibleIfStatements: + active: true + DataClassShouldBeImmutable: + active: true + EqualsNullCall: + active: true + EqualsOnSignatureLine: + active: true + ExplicitItLambdaParameter: + active: true + ForbiddenVoid: + active: true + LibraryCodeMustSpecifyReturnType: + active: true + LoopWithTooManyJumpStatements: + active: false + maxJumpCount: 1 + MaxLineLength: + active: true + maxLineLength: 120 + excludePackageStatements: true + excludeImportStatements: true + excludeCommentStatements: true + MayBeConst: + active: true + ModifierOrder: + active: true + NewLineAtEndOfFile: + active: true + NoTabs: + active: true + OptionalAbstractKeyword: + active: true + OptionalWhenBraces: + active: true + PreferToOverPairSyntax: + active: true + ProtectedMemberInFinalClass: + active: true + RedundantVisibilityModifierRule: + active: true + excludes: ['**/library/**', '**/kotlinx-datetime/**'] + SafeCast: + active: true + SerialVersionUIDInSerializableClass: + active: true + SpacingBetweenPackageAndImports: + active: true + TrailingWhitespace: + active: true + UnderscoresInNumericLiterals: + active: false + acceptableDecimalLength: 3 + UnnecessaryAbstractClass: + active: true + UnnecessaryApply: + active: true + UnnecessaryInheritance: + active: true + UnnecessaryLet: + active: true + UnnecessaryParentheses: + active: true + UntilInsteadOfRangeTo: + active: true + UnusedImports: + active: true + UnusedPrivateClass: + active: true + UnusedPrivateMember: + active: false # Due to compose preview + UseArrayLiteralsInAnnotations: + active: true + UseCheckOrError: + active: true + UseIfInsteadOfWhen: + active: false + UseRequire: + active: true + UselessCallOnNotNull: + active: true + UtilityClassWithPublicConstructor: + active: true + VarCouldBeVal: + active: true + WildcardImport: + active: true diff --git a/ComposeCalendar/gradle.properties b/ComposeCalendar/gradle.properties new file mode 100644 index 000000000..269fd8b11 --- /dev/null +++ b/ComposeCalendar/gradle.properties @@ -0,0 +1,27 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.enableJetifier=true +kotlin.code.style=official + +POM_NAME=Compose Calendar +GROUP=io.github.boguszpawlowski.composecalendar +POM_URL=https://github.com/boguszpawlowski/ComposeCalendar +POM_SCM_URL=https://github.com/boguszpawlowski/ComposeCalendar +POM_SCM_CONNECTION=git@github.com:boguszpawlowski/ComposeCalendar.git +POM_SCM_DEV_CONNECTION=git@github.com:boguszpawlowski/ComposeCalendar.git +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo +POM_DEVELOPER_ID=boguszp +POM_DEVELOPER_NAME=Bogusz Pawlowski +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/ComposeCalendar/gradle/dependencies.gradle b/ComposeCalendar/gradle/dependencies.gradle new file mode 100644 index 000000000..e69de29bb diff --git a/ComposeCalendar/gradle/wrapper/gradle-wrapper.jar b/ComposeCalendar/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..d64cd4917707c1f8861d8cb53dd15194d4248596 GIT binary patch literal 43462 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 literal 0 HcmV?d00001 diff --git a/ComposeCalendar/gradle/wrapper/gradle-wrapper.properties b/ComposeCalendar/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..1af9e0930 --- /dev/null +++ b/ComposeCalendar/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/ComposeCalendar/gradlew b/ComposeCalendar/gradlew new file mode 100644 index 000000000..1aa94a426 --- /dev/null +++ b/ComposeCalendar/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/ComposeCalendar/gradlew.bat b/ComposeCalendar/gradlew.bat new file mode 100644 index 000000000..93e3f59f1 --- /dev/null +++ b/ComposeCalendar/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/ComposeCalendar/kotlinx-datetime/.gitignore b/ComposeCalendar/kotlinx-datetime/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api b/ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api new file mode 100644 index 000000000..c5e8ba1a5 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/api/kotlinx-datetime.api @@ -0,0 +1,37 @@ +public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/ConvertersKt { + public static final fun toJavaYearMonth (Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;)Ljava/time/YearMonth; + public static final fun toKotlinYearMonth (Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; +} + +public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensionsKt { + public static final fun getFirstDayOfWeek (Ljava/util/Locale;)Ljava/time/DayOfWeek; + public static final fun now (Lkotlinx/datetime/LocalDate$Companion;)Lkotlinx/datetime/LocalDate; +} + +public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth { + public static final field Companion Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth$Companion; + public synthetic fun (ILjava/time/Month;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()I + public final fun component2 ()Ljava/time/Month; + public final fun copy (ILjava/time/Month;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth;ILjava/time/Month;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun dec ()Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public fun equals (Ljava/lang/Object;)Z + public final fun getMonth ()Ljava/time/Month; + public final fun getYear ()I + public fun hashCode ()I + public final fun inc ()Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun minus (ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun minus (JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun plus (ILkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun plus (JLkotlinx/datetime/DateTimeUnit$MonthBased;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public fun toString ()Ljava/lang/String; +} + +public final class io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth$Companion { + public final fun now ()Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun of (II)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun of (ILjava/time/Month;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; + public final fun parse (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth; +} + diff --git a/ComposeCalendar/kotlinx-datetime/build.gradle.kts b/ComposeCalendar/kotlinx-datetime/build.gradle.kts new file mode 100644 index 000000000..4c35546f9 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + kotlin(Kotlin.JvmPluginId) + id(MavenPublish.PluginId) +} + +kotlin { + explicitApi() +} + +dependencies { + api(Kotlin.DateTime) + + testImplementation(Kotest.Assertions) + testImplementation(Kotest.RunnerJunit5) +} diff --git a/ComposeCalendar/kotlinx-datetime/gradle.properties b/ComposeCalendar/kotlinx-datetime/gradle.properties new file mode 100644 index 000000000..5b55af44f --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/gradle.properties @@ -0,0 +1,5 @@ +POM_ARTIFACT_ID=kotlinx-datetime +POM_DESCRIPTION=A set of utilities supporting usage of kotlin datetime instead of java.time. +POM_PACKAGING=aar +SONATYPE_HOST=S01 +RELEASE_SIGNING_ENABLED=true diff --git a/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt new file mode 100644 index 000000000..04f2cc336 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/Converters.kt @@ -0,0 +1,5 @@ +package io.github.boguszpawlowski.composecalendar.kotlinxDateTime + +public fun YearMonth.toJavaYearMonth(): java.time.YearMonth = java.time.YearMonth.of(year, month) + +public fun java.time.YearMonth.toKotlinYearMonth(): YearMonth = YearMonth.of(year, month) diff --git a/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt new file mode 100644 index 000000000..7887914b4 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/DateTimeExtensions.kt @@ -0,0 +1,15 @@ +package io.github.boguszpawlowski.composecalendar.kotlinxDateTime + +import kotlinx.datetime.Clock +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import java.time.temporal.WeekFields +import java.util.Locale + +public fun LocalDate.Companion.now(): LocalDate = + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date + +public val Locale.firstDayOfWeek: DayOfWeek + get() = WeekFields.of(this).firstDayOfWeek diff --git a/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt new file mode 100644 index 000000000..eb2a8e8b9 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/src/main/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonth.kt @@ -0,0 +1,79 @@ +package io.github.boguszpawlowski.composecalendar.kotlinxDateTime + +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate +import kotlinx.datetime.Month +import java.time.temporal.ChronoField.YEAR + +/** + * Kotlin implementation of `YearMonth` available in java.time, since `KotlinxDateTime` doesn't + * include a substitute for it: https://github.com/Kotlin/kotlinx-datetime/issues/168 + * + * For construction use factory methods 'of' provided in companion object + */ +@Suppress("DataClassPrivateConstructor") +public data class YearMonth private constructor( + val year: Int, + val month: Month, +) { + + /** + * Increment by one month + */ + public operator fun inc(): YearMonth = plus(1, DateTimeUnit.MONTH) + + /** + * Decrement by one month + */ + public operator fun dec(): YearMonth = minus(1, DateTimeUnit.MONTH) + + /** + * Add specified amount of months to current date + */ + public fun plus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth = + plus(value.toLong(), unit) + + /** + * Subtract specified amount of months from current date + */ + public fun minus(value: Int, unit: DateTimeUnit.MonthBased): YearMonth = + plus(-value.toLong(), unit) + + public fun minus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth = + plus(-value, unit) + + public fun plus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth { + val monthsToAdd = value * unit.months + if (monthsToAdd == 0L) { + return this + } + val monthCount = year * 12L + (month.value - 1) + val calcMonths = monthCount + monthsToAdd + + val newYear = YEAR.checkValidIntValue(calcMonths.floorDiv(12)) + val newMonth = calcMonths.floorMod(12) + 1 + return of(newYear, newMonth.toInt()) + } + + override fun toString(): String = toJavaYearMonth().toString() + + public companion object { + public fun of(year: Int, month: Int): YearMonth = YearMonth(year, Month.of(month)) + + public fun of(year: Int, month: Month): YearMonth = YearMonth(year, month) + + public fun parse(value: String): YearMonth = + java.time.YearMonth.parse(value).toKotlinYearMonth() + + public fun now(): YearMonth { + val today = LocalDate.now() + + return of(today.year, today.month.value) + } + } +} + +private fun Long.floorMod(other: Long): Long = when (other) { + 0L -> this + else -> this - floorDiv(other) * other +} diff --git a/ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt b/ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt new file mode 100644 index 000000000..6de0bf6b8 --- /dev/null +++ b/ComposeCalendar/kotlinx-datetime/src/test/java/io/github/boguszpawlowski/composecalendar/kotlinxDateTime/YearMonthTest.kt @@ -0,0 +1,128 @@ +package io.github.boguszpawlowski.composecalendar.kotlinxDateTime + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.data.Headers3 +import io.kotest.data.Row3 +import io.kotest.data.forAll +import io.kotest.data.table +import io.kotest.matchers.shouldBe +import kotlinx.datetime.DateTimeUnit +import java.time.DateTimeException + +internal class YearMonthTest : ShouldSpec({ + + context("Year Month") { + should("overflow a year if adding months") { + val yearMonth = YearMonth.of(2002, 11) + val expectedResult = YearMonth.of(2003, 1) + + val result = yearMonth.plus(2, DateTimeUnit.MONTH) + + result shouldBe expectedResult + } + should("add years if adding months") { + val yearMonth = YearMonth.of(2002, 11) + val expectedResult = YearMonth.of(2202, 11) + + val result = yearMonth.plus(2, DateTimeUnit.CENTURY) + + result shouldBe expectedResult + } + should("subtract years if subtracting months") { + val yearMonth = YearMonth.of(2002, 1) + val expectedResult = YearMonth.of(2001, 11) + + val result = yearMonth.minus(2, DateTimeUnit.MONTH) + + result shouldBe expectedResult + } + should("subtract year if subtracting months") { + val yearMonth = YearMonth.of(2002, 11) + val expectedResult = YearMonth.of(2000, 11) + + val result = yearMonth.minus(2, DateTimeUnit.YEAR) + + result shouldBe expectedResult + } + should("throw an error if months greater than 12") { + shouldThrow { + YearMonth.of(1222, 13) + } + } + should("correctly add months") { + forAll( + table( + Headers3("starting month", "months to add", "expected month"), + Row3(1, 2, 3), + Row3(12, 24, 12), + Row3(11, 2, 1), + Row3(1, 14, 3), + Row3(6, 7, 1), + Row3(10, 20, 6), + ) + ) { startingMonth: Int, monthsToAdd: Int, expectedMonth: Int -> + val yearMonth = YearMonth.of(2000, startingMonth) + + val result = yearMonth.plus(monthsToAdd, DateTimeUnit.MONTH).month.value + + result shouldBe expectedMonth + } + } + should("correctly add years") { + forAll( + table( + Headers3("starting month", "months to add", "expected year"), + Row3(1, 2, 2000), + Row3(12, 24, 2002), + Row3(11, 2, 2001), + Row3(1, 14, 2001), + Row3(6, 7, 2001), + Row3(10, 20, 2002), + ) + ) { startingMonth: Int, monthsToAdd: Int, expectedYear: Int -> + val yearMonth = YearMonth.of(2000, startingMonth) + + val result = yearMonth.plus(monthsToAdd, DateTimeUnit.MONTH).year + + result shouldBe expectedYear + } + } + context("when decrementing") { + should("subtract one month") { + val yearMonth = YearMonth.of(2002, 11) + val expectedResult = YearMonth.of(2002, 10) + + val result = yearMonth.dec() + + result shouldBe expectedResult + } + should("overflow the year when january") { + val yearMonth = YearMonth.of(2002, 1) + val expectedResult = YearMonth.of(2001, 12) + + val result = yearMonth.dec() + + result shouldBe expectedResult + } + } + context("when incrementing") { + should("add one month") { + val yearMonth = YearMonth.of(2002, 11) + val expectedResult = YearMonth.of(2002, 12) + + val result = yearMonth.inc() + + result shouldBe expectedResult + } + should("overflow the year when december") { + val yearMonth = YearMonth.of(2002, 12) + val expectedResult = YearMonth.of(2003, 1) + + val result = yearMonth.inc() + + result shouldBe expectedResult + } + } + } +}) diff --git a/ComposeCalendar/library/.gitignore b/ComposeCalendar/library/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/ComposeCalendar/library/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/ComposeCalendar/library/api/library.api b/ComposeCalendar/library/api/library.api new file mode 100644 index 000000000..9c12ac395 --- /dev/null +++ b/ComposeCalendar/library/api/library.api @@ -0,0 +1,256 @@ +public final class io/github/boguszpawlowski/composecalendar/BuildConfig { + public static final field BUILD_TYPE Ljava/lang/String; + public static final field DEBUG Z + public static final field LIBRARY_PACKAGE_NAME Ljava/lang/String; + public fun ()V +} + +public final class io/github/boguszpawlowski/composecalendar/CalendarKt { + public static final fun Calendar (Lio/github/boguszpawlowski/composecalendar/CalendarState;Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun SelectableCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun StaticCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZZLio/github/boguszpawlowski/composecalendar/CalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;III)V + public static final fun rememberCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState; + public static final fun rememberSelectableCalendarState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/CalendarState; +} + +public final class io/github/boguszpawlowski/composecalendar/CalendarState { + public static final field $stable I + public fun (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V + public final fun getMonthState ()Lio/github/boguszpawlowski/composecalendar/header/MonthState; + public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState; +} + +public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt { + public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$CalendarKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-10 Lkotlin/jvm/functions/Function4; + public static field lambda-11 Lkotlin/jvm/functions/Function4; + public static field lambda-12 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public static field lambda-3 Lkotlin/jvm/functions/Function4; + public static field lambda-4 Lkotlin/jvm/functions/Function3; + public static field lambda-5 Lkotlin/jvm/functions/Function4; + public static field lambda-6 Lkotlin/jvm/functions/Function4; + public static field lambda-7 Lkotlin/jvm/functions/Function4; + public static field lambda-8 Lkotlin/jvm/functions/Function3; + public static field lambda-9 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-10$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-11$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-12$library_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4; +} + +public final class io/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt { + public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/ComposableSingletons$WeekCalendarKt; + public static field lambda-1 Lkotlin/jvm/functions/Function4; + public static field lambda-2 Lkotlin/jvm/functions/Function4; + public static field lambda-3 Lkotlin/jvm/functions/Function4; + public static field lambda-4 Lkotlin/jvm/functions/Function4; + public static field lambda-5 Lkotlin/jvm/functions/Function4; + public static field lambda-6 Lkotlin/jvm/functions/Function4; + public static field lambda-7 Lkotlin/jvm/functions/Function4; + public static field lambda-8 Lkotlin/jvm/functions/Function4; + public static field lambda-9 Lkotlin/jvm/functions/Function4; + public fun ()V + public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-3$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-4$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-5$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-6$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-7$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-8$library_release ()Lkotlin/jvm/functions/Function4; + public final fun getLambda-9$library_release ()Lkotlin/jvm/functions/Function4; +} + +public final class io/github/boguszpawlowski/composecalendar/WeekCalendarKt { + public static final fun SelectableWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun StaticWeekCalendar (Landroidx/compose/ui/Modifier;Ljava/time/DayOfWeek;Ljava/time/LocalDate;ZLio/github/boguszpawlowski/composecalendar/WeekCalendarState;ZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun WeekCalendar (Lio/github/boguszpawlowski/composecalendar/WeekCalendarState;Landroidx/compose/ui/Modifier;Ljava/time/LocalDate;Ljava/time/DayOfWeek;ZZLkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V + public static final fun rememberSelectableWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;Lkotlin/jvm/functions/Function1;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState; + public static final fun rememberWeekCalendarState (Ljava/time/DayOfWeek;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/runtime/Composer;II)Lio/github/boguszpawlowski/composecalendar/WeekCalendarState; +} + +public final class io/github/boguszpawlowski/composecalendar/WeekCalendarState { + public static final field $stable I + public fun (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V + public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState; + public final fun getWeekState ()Lio/github/boguszpawlowski/composecalendar/header/WeekState; +} + +public abstract interface class io/github/boguszpawlowski/composecalendar/day/Day { + public abstract fun getDate ()Ljava/time/LocalDate; + public abstract fun isCurrentDay ()Z + public abstract fun isFromCurrentMonth ()Z +} + +public final class io/github/boguszpawlowski/composecalendar/day/DayState : io/github/boguszpawlowski/composecalendar/day/Day { + public static final field $stable I + public fun (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)V + public final fun component2 ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState; + public final fun copy (Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;)Lio/github/boguszpawlowski/composecalendar/day/DayState; + public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/day/DayState;Lio/github/boguszpawlowski/composecalendar/day/Day;Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/day/DayState; + public fun equals (Ljava/lang/Object;)Z + public fun getDate ()Ljava/time/LocalDate; + public final fun getSelectionState ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionState; + public fun hashCode ()I + public fun isCurrentDay ()Z + public fun isFromCurrentMonth ()Z + public fun toString ()Ljava/lang/String; +} + +public final class io/github/boguszpawlowski/composecalendar/day/DefaultDayKt { + public static final fun DefaultDay-t6yy7ic (Lio/github/boguszpawlowski/composecalendar/day/DayState;Landroidx/compose/ui/Modifier;JJLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt { + public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultMonthHeaderKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt { + public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/header/ComposableSingletons$DefaultWeekHeaderKt; + public static field lambda-1 Lkotlin/jvm/functions/Function2; + public static field lambda-2 Lkotlin/jvm/functions/Function2; + public fun ()V + public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2; +} + +public final class io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeaderKt { + public static final fun DefaultMonthHeader (Lio/github/boguszpawlowski/composecalendar/header/MonthState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeaderKt { + public static final fun DefaultWeekHeader (Lio/github/boguszpawlowski/composecalendar/header/WeekState;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public abstract interface class io/github/boguszpawlowski/composecalendar/header/MonthState { + public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/MonthState$Companion; + public abstract fun getCurrentMonth ()Ljava/time/YearMonth; + public abstract fun getMaxMonth ()Ljava/time/YearMonth; + public abstract fun getMinMonth ()Ljava/time/YearMonth; + public abstract fun setCurrentMonth (Ljava/time/YearMonth;)V + public abstract fun setMaxMonth (Ljava/time/YearMonth;)V + public abstract fun setMinMonth (Ljava/time/YearMonth;)V +} + +public final class io/github/boguszpawlowski/composecalendar/header/MonthState$Companion { + public final fun Saver ()Landroidx/compose/runtime/saveable/Saver; +} + +public final class io/github/boguszpawlowski/composecalendar/header/MonthStateKt { + public static final fun MonthState (Ljava/time/YearMonth;Ljava/time/YearMonth;Ljava/time/YearMonth;)Lio/github/boguszpawlowski/composecalendar/header/MonthState; +} + +public abstract interface class io/github/boguszpawlowski/composecalendar/header/WeekState { + public static final field Companion Lio/github/boguszpawlowski/composecalendar/header/WeekState$Companion; + public abstract fun getCurrentWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week; + public abstract fun getMaxWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week; + public abstract fun getMinWeek ()Lio/github/boguszpawlowski/composecalendar/week/Week; + public abstract fun setCurrentWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V + public abstract fun setMaxWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V + public abstract fun setMinWeek (Lio/github/boguszpawlowski/composecalendar/week/Week;)V +} + +public final class io/github/boguszpawlowski/composecalendar/header/WeekState$Companion { + public final fun Saver ()Landroidx/compose/runtime/saveable/Saver; +} + +public final class io/github/boguszpawlowski/composecalendar/header/WeekStateKt { + public static final fun WeekState (Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)Lio/github/boguszpawlowski/composecalendar/header/WeekState; +} + +public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler { + public static final field $stable I + public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler; + public final fun calculateNewSelection (Ljava/time/LocalDate;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)Ljava/util/List; +} + +public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState { + public static final field $stable I + public fun (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V + public synthetic fun (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getSelection ()Ljava/util/List; + public final fun getSelectionMode ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; + public fun isDateSelected (Ljava/time/LocalDate;)Z + public fun onDateSelected (Ljava/time/LocalDate;)V + public final fun setSelection (Ljava/util/List;)V + public final fun setSelectionMode (Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V +} + +public final class io/github/boguszpawlowski/composecalendar/selection/EmptySelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState { + public static final field $stable I + public static final field INSTANCE Lio/github/boguszpawlowski/composecalendar/selection/EmptySelectionState; + public fun isDateSelected (Ljava/time/LocalDate;)Z + public fun onDateSelected (Ljava/time/LocalDate;)V +} + +public final class io/github/boguszpawlowski/composecalendar/selection/SelectionMode : java/lang/Enum { + public static final field Multiple Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; + public static final field None Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; + public static final field Period Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; + public static final field Single Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; + public static fun values ()[Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode; +} + +public abstract interface class io/github/boguszpawlowski/composecalendar/selection/SelectionState { + public abstract fun isDateSelected (Ljava/time/LocalDate;)Z + public abstract fun onDateSelected (Ljava/time/LocalDate;)V +} + +public final class io/github/boguszpawlowski/composecalendar/selection/SelectionState$DefaultImpls { + public static fun isDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)Z + public static fun onDateSelected (Lio/github/boguszpawlowski/composecalendar/selection/SelectionState;Ljava/time/LocalDate;)V +} + +public final class io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeaderKt { + public static final fun DefaultDaysOfWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V + public static final fun DefaultWeekHeader (Ljava/util/List;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V +} + +public final class io/github/boguszpawlowski/composecalendar/week/Week { + public static final field $stable I + public static final field Companion Lio/github/boguszpawlowski/composecalendar/week/Week$Companion; + public fun (Ljava/util/List;)V + public final fun compareTo (Lio/github/boguszpawlowski/composecalendar/week/Week;)I + public final fun component1 ()Ljava/util/List; + public final fun copy (Ljava/util/List;)Lio/github/boguszpawlowski/composecalendar/week/Week; + public static synthetic fun copy$default (Lio/github/boguszpawlowski/composecalendar/week/Week;Ljava/util/List;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week; + public final fun dec ()Lio/github/boguszpawlowski/composecalendar/week/Week; + public fun equals (Ljava/lang/Object;)Z + public final fun getDays ()Ljava/util/List; + public final fun getEnd ()Ljava/time/LocalDate; + public final fun getStart ()Ljava/time/LocalDate; + public final fun getYearMonth ()Ljava/time/YearMonth; + public fun hashCode ()I + public final fun inc ()Lio/github/boguszpawlowski/composecalendar/week/Week; + public final fun minusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week; + public final fun plusWeeks (J)Lio/github/boguszpawlowski/composecalendar/week/Week; + public fun toString ()Ljava/lang/String; +} + +public final class io/github/boguszpawlowski/composecalendar/week/Week$Companion { + public final fun now (Ljava/time/DayOfWeek;)Lio/github/boguszpawlowski/composecalendar/week/Week; + public static synthetic fun now$default (Lio/github/boguszpawlowski/composecalendar/week/Week$Companion;Ljava/time/DayOfWeek;ILjava/lang/Object;)Lio/github/boguszpawlowski/composecalendar/week/Week; +} + +public final class io/github/boguszpawlowski/composecalendar/week/WeekKt { + public static final fun between (Ljava/time/temporal/ChronoUnit;Lio/github/boguszpawlowski/composecalendar/week/Week;Lio/github/boguszpawlowski/composecalendar/week/Week;)I +} + diff --git a/ComposeCalendar/library/build.gradle.kts b/ComposeCalendar/library/build.gradle.kts new file mode 100644 index 000000000..864e35c15 --- /dev/null +++ b/ComposeCalendar/library/build.gradle.kts @@ -0,0 +1,30 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id(Android.LibraryPluginId) + kotlin(Kotlin.AndroidPluginId) + id("common-android-plugin") + id(MavenPublish.PluginId) +} + +android { + namespace = "io.github.boguszpawlowski.composecalendar" + tasks.withType { + kotlinOptions { + freeCompilerArgs = freeCompilerArgs + "-Xexplicit-api=strict" + "-Xcontext-receivers" + } + } +} + +dependencies { + implementation(Compose.Ui) + implementation(Compose.UiTooling) + implementation(Compose.AccompanistPager) + implementation(Compose.Foundation) + implementation(Compose.FoundationLayout) + implementation(Compose.Material) + implementation(Timber.Core) + + testImplementation(Kotest.Assertions) + testImplementation(Kotest.RunnerJunit5) +} diff --git a/ComposeCalendar/library/gradle.properties b/ComposeCalendar/library/gradle.properties new file mode 100644 index 000000000..9a193f6c3 --- /dev/null +++ b/ComposeCalendar/library/gradle.properties @@ -0,0 +1,5 @@ +POM_ARTIFACT_ID=composecalendar +POM_DESCRIPTION=Library for handling the Calendar view in Jetpack Compose. +POM_PACKAGING=aar +SONATYPE_HOST=S01 +RELEASE_SIGNING_ENABLED=true diff --git a/ComposeCalendar/library/src/main/AndroidManifest.xml b/ComposeCalendar/library/src/main/AndroidManifest.xml new file mode 100644 index 000000000..8bdb7e14b --- /dev/null +++ b/ComposeCalendar/library/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt new file mode 100644 index 000000000..b8f335b00 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt @@ -0,0 +1,286 @@ +@file:Suppress("MatchingDeclarationName") + +package io.github.boguszpawlowski.composecalendar + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import io.github.boguszpawlowski.composecalendar.day.DayState +import io.github.boguszpawlowski.composecalendar.day.DefaultDay +import io.github.boguszpawlowski.composecalendar.header.DefaultMonthHeader +import io.github.boguszpawlowski.composecalendar.header.MonthState +import io.github.boguszpawlowski.composecalendar.month.MonthContent +import io.github.boguszpawlowski.composecalendar.month.MonthPager +import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState +import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek +import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader +import io.github.boguszpawlowski.composecalendar.week.rotateRight +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth +import java.time.temporal.WeekFields +import java.util.Locale + +/** + * State of the calendar composable + * + * @property monthState currently showed month + * @property selectionState handler for the calendar's selection + */ +@Stable +public class CalendarState( + public val monthState: MonthState, + public val selectionState: T, +) + +/** + * [Calendar] implementation using a [DynamicSelectionState] as a selection handler. + * + * * Basic usage: + * ``` + * @Composable + * fun MainScreen() { + * SelectableCalendar() + * } + * ``` + * + * @param modifier + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param today current day, defaults to [LocalDate.now] + * @param showAdjacentMonths whenever to show or hide the days from adjacent months + * @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe + * @param weekDaysScrollEnabled if the week days should be scrollable + * @param calendarState state of the composable + * @param dayContent composable rendering the current day + * @param monthHeader header for showing the current month and controls for changing it + * @param daysOfWeekHeader header for showing captions for each day of week + * @param monthContainer container composable for all the days in current month + */ +@Composable +@SuppressLint("NewApi") +public fun SelectableCalendar( + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + today: LocalDate = LocalDate.now(), + showAdjacentMonths: Boolean = true, + horizontalSwipeEnabled: Boolean = true, + weekDaysScrollEnabled: Boolean = true, + calendarState: CalendarState = rememberSelectableCalendarState(), + dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) }, + monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) }, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit = { DefaultDaysOfWeekHeader(it) }, + monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content -> + Box { content(PaddingValues()) } + }, +) { + Calendar( + modifier = modifier, + firstDayOfWeek = firstDayOfWeek, + today = today, + showAdjacentMonths = showAdjacentMonths, + horizontalSwipeEnabled = horizontalSwipeEnabled, + weekDaysScrollEnabled = weekDaysScrollEnabled, + calendarState = calendarState, + dayContent = dayContent, + monthHeader = monthHeader, + daysOfWeekHeader = daysOfWeekHeader, + monthContainer = monthContainer + ) +} + +/** + * [Calendar] implementation without any mechanism for the selection. + * + * Basic usage: + * ``` + * @Composable + * fun MainScreen() { + * StaticCalendar() + * } + * ``` + * + * @param modifier + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param today current day, defaults to [LocalDate.now] + * @param showAdjacentMonths whenever to show or hide the days from adjacent months + * @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe + * @param weekDaysScrollEnabled if the week days should be scrollable + * @param calendarState state of the composable + * @param dayContent composable rendering the current day + * @param monthHeader header for showing the current month and controls for changing it + * @param daysOfWeekHeader header for showing captions for each day of week + * @param monthContainer container composable for all the days in current month + */ +@Composable +@SuppressLint("NewApi") +public fun StaticCalendar( + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + today: LocalDate = LocalDate.now(), + showAdjacentMonths: Boolean = true, + horizontalSwipeEnabled: Boolean = true, + weekDaysScrollEnabled: Boolean = true, + calendarState: CalendarState = rememberCalendarState(), + dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) }, + monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) }, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit = { DefaultDaysOfWeekHeader(it) }, + monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content -> + Box { content(PaddingValues()) } + }, +) { + Calendar( + modifier = modifier, + firstDayOfWeek = firstDayOfWeek, + today = today, + showAdjacentMonths = showAdjacentMonths, + horizontalSwipeEnabled = horizontalSwipeEnabled, + weekDaysScrollEnabled = weekDaysScrollEnabled, + calendarState = calendarState, + dayContent = dayContent, + monthHeader = monthHeader, + daysOfWeekHeader = daysOfWeekHeader, + monthContainer = monthContainer + ) +} + +/** + * Composable for showing a calendar. The calendar state has to be provided by the call site. If you + * want to use built-in implementation, check out: + * [SelectableCalendar] - calendar composable handling selection that can be changed at runtime + * [StaticCalendar] - calendar without any mechanism for selection + * + * @param modifier + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param today current day, defaults to [LocalDate.now] + * @param showAdjacentMonths whenever to show or hide the days from adjacent months + * @param horizontalSwipeEnabled whenever user is able to change the month by horizontal swipe + * @param calendarState state of the composable + * @param dayContent composable rendering the current day + * @param monthHeader header for showing the current month and controls for changing it + * @param daysOfWeekHeader header for showing captions for each day of week + * @param monthContainer container composable for all the days in current month + */ +@Composable +@SuppressLint("NewApi") +public fun Calendar( + calendarState: CalendarState, + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + today: LocalDate = LocalDate.now(), + showAdjacentMonths: Boolean = true, + horizontalSwipeEnabled: Boolean = true, + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) }, + monthHeader: @Composable ColumnScope.(MonthState) -> Unit = { DefaultMonthHeader(it) }, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit = { DefaultDaysOfWeekHeader(it) }, + monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit = { content -> + Box { content(PaddingValues()) } + }, +) { + val initialMonth = remember { calendarState.monthState.currentMonth } + val daysOfWeek = remember(firstDayOfWeek) { + DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal) + } + + Column( + modifier = modifier, + ) { + monthHeader(calendarState.monthState) + if (horizontalSwipeEnabled) { + MonthPager( + initialMonth = initialMonth, + showAdjacentMonths = showAdjacentMonths, + monthState = calendarState.monthState, + selectionState = calendarState.selectionState, + today = today, + weekDaysScrollEnabled = weekDaysScrollEnabled, + daysOfWeek = daysOfWeek, + dayContent = dayContent, + weekHeader = daysOfWeekHeader, + monthContainer = monthContainer, + ) + } else { + MonthContent( + modifier = Modifier.fillMaxWidth(), + currentMonth = calendarState.monthState.currentMonth, + showAdjacentMonths = showAdjacentMonths, + selectionState = calendarState.selectionState, + today = today, + weekDaysScrollEnabled = weekDaysScrollEnabled, + daysOfWeek = daysOfWeek, + dayContent = dayContent, + weekHeader = daysOfWeekHeader, + monthContainer = monthContainer, + ) + } + } +} + +/** + * Helper function for providing a [CalendarState] implementation with selection mechanism. + * + * @param initialMonth initially rendered month + * @param initialSelection initial selection of the composable + * @param initialSelectionMode initial mode of the selection + * @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change + * @param minMonth first month that can be shown + * @param maxMonth last month that can be shown + */ +@Composable +@SuppressLint("NewApi") +public fun rememberSelectableCalendarState( + initialMonth: YearMonth = YearMonth.now(), + minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange), + maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange), + initialSelection: List = emptyList(), + initialSelectionMode: SelectionMode = SelectionMode.Single, + confirmSelectionChange: (newValue: List) -> Boolean = { true }, + monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) { + MonthState( + initialMonth = initialMonth, + minMonth = minMonth, + maxMonth = maxMonth + ) + }, + selectionState: DynamicSelectionState = rememberSaveable( + saver = DynamicSelectionState.Saver(confirmSelectionChange), + ) { + DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode) + }, +): CalendarState = remember { CalendarState(monthState, selectionState) } + +/** + * Helper function for providing a [CalendarState] implementation without a selection mechanism. + * + * @param initialMonth initially rendered month + * @param minMonth first month that can be shown + * @param maxMonth last month that can be shown + */ +@Composable +@SuppressLint("NewApi") +public fun rememberCalendarState( + initialMonth: YearMonth = YearMonth.now(), + minMonth: YearMonth = initialMonth.minusMonths(DefaultCalendarPagerRange), + maxMonth: YearMonth = initialMonth.plusMonths(DefaultCalendarPagerRange), + monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) { + MonthState( + initialMonth = initialMonth, + minMonth = minMonth, + maxMonth = maxMonth + ) + }, +): CalendarState = remember { CalendarState(monthState, EmptySelectionState) } + +internal const val DefaultCalendarPagerRange = 10_000L diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt new file mode 100644 index 000000000..28da35a51 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/WeekCalendar.kt @@ -0,0 +1,264 @@ +@file:Suppress("MatchingDeclarationName", "LongParameterList") + +package io.github.boguszpawlowski.composecalendar + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import io.github.boguszpawlowski.composecalendar.day.DayState +import io.github.boguszpawlowski.composecalendar.day.DefaultDay +import io.github.boguszpawlowski.composecalendar.header.WeekState +import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState +import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import io.github.boguszpawlowski.composecalendar.week.DaysInAWeek +import io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader +import io.github.boguszpawlowski.composecalendar.week.Week +import io.github.boguszpawlowski.composecalendar.week.WeekContent +import io.github.boguszpawlowski.composecalendar.week.WeekPager +import io.github.boguszpawlowski.composecalendar.week.getWeekDays +import io.github.boguszpawlowski.composecalendar.week.rotateRight +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.temporal.WeekFields +import java.util.Locale +import io.github.boguszpawlowski.composecalendar.header.DefaultWeekHeader as DefaultProperWeekHeader + +/** + * State of the week calendar composable + * + * @property weekState currently showed week + * @property selectionState handler for the calendar's selection + */ +@Stable +public class WeekCalendarState( + public val weekState: WeekState, + public val selectionState: T, +) + +/** + * [WeekCalendar] implementation using a [DynamicSelectionState] as a selection handler. + * + * * Basic usage: + * ``` + * @Composable + * fun MainScreen() { + * SelectableWeekCalendar() + * } + * ``` + * + * @param modifier + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param today current day, defaults to [LocalDate.now] + * @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe + * @param weekDaysScrollEnabled if the week days should be scrollable + * @param calendarState state of the composable + * @param dayContent composable rendering the current day + * @param weekHeader header for showing the current week and controls for changing it + * @param daysOfWeekHeader header for showing captions for each day of week + */ +@Composable +public fun SelectableWeekCalendar( + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + today: LocalDate = LocalDate.now(), + horizontalSwipeEnabled: Boolean = true, + weekDaysScrollEnabled: Boolean = true, + calendarState: WeekCalendarState = rememberSelectableWeekCalendarState(), + dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) }, + weekHeader: @Composable ColumnScope.(WeekState) -> Unit = { + DefaultProperWeekHeader(it) + }, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit = { DefaultDaysOfWeekHeader(it) }, +) { + WeekCalendar( + modifier = modifier, + calendarState = calendarState, + today = today, + horizontalSwipeEnabled = horizontalSwipeEnabled, + firstDayOfWeek = firstDayOfWeek, + weekDaysScrollEnabled = weekDaysScrollEnabled, + dayContent = dayContent, + weekHeader = weekHeader, + daysOfWeekHeader = daysOfWeekHeader, + ) +} + +/** + * [WeekCalendar] implementation without any mechanism for the selection. + * + * * Basic usage: + * ``` + * @Composable + * fun MainScreen() { + * StaticWeekCalendar() + * } + * ``` + * + * @param modifier + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param today current day, defaults to [LocalDate.now] + * @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe + * @param calendarState state of the composable + * @param dayContent composable rendering the current day + * @param weekHeader header for showing the current week and controls for changing it + * @param daysOfWeekHeader header for showing captions for each day of week + */ +@Composable +@SuppressLint("NewApi") +public fun StaticWeekCalendar( + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + today: LocalDate = LocalDate.now(), + horizontalSwipeEnabled: Boolean = true, + calendarState: WeekCalendarState = rememberWeekCalendarState(), + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) }, + weekHeader: @Composable ColumnScope.(WeekState) -> Unit = { + DefaultProperWeekHeader(it) + }, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit = { DefaultDaysOfWeekHeader(it) }, +) { + WeekCalendar( + modifier = modifier, + calendarState = calendarState, + today = today, + horizontalSwipeEnabled = horizontalSwipeEnabled, + firstDayOfWeek = firstDayOfWeek, + weekDaysScrollEnabled = weekDaysScrollEnabled, + dayContent = dayContent, + weekHeader = weekHeader, + daysOfWeekHeader = daysOfWeekHeader, + ) +} + +/** + * Composable for showing a week calendar. The calendar state has to be provided by the call site. If you + * want to use built-in implementation, check out: + * [SelectableWeekCalendar] - calendar composable handling selection that can be changed at runtime + * [StaticWeekCalendar] - calendar without any mechanism for selection + * + * @param modifier + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param today current day, defaults to [LocalDate.now] + * @param horizontalSwipeEnabled whenever user is able to change the week by horizontal swipe + * @param calendarState state of the composable + * @param dayContent composable rendering the current day + * @param weekHeader header for showing the current week and controls for changing it + * @param daysOfWeekHeader header for showing captions for each day of week + */ +@Composable +@SuppressLint("NewApi") +public fun WeekCalendar( + calendarState: WeekCalendarState, + modifier: Modifier = Modifier, + today: LocalDate = LocalDate.now(), + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + horizontalSwipeEnabled: Boolean = true, + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit = { DefaultDay(it) }, + weekHeader: @Composable ColumnScope.(WeekState) -> Unit = { + DefaultProperWeekHeader(it) + }, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit = { DefaultDaysOfWeekHeader(it) }, +) { + val initialWeek = remember { calendarState.weekState.currentWeek } + val daysOfWeek = remember(firstDayOfWeek) { + DayOfWeek.values().rotateRight(DaysInAWeek - firstDayOfWeek.ordinal) + } + + Column( + modifier = modifier, + ) { + weekHeader(calendarState.weekState) + if (horizontalSwipeEnabled) { + WeekPager( + initialWeek = initialWeek, + daysOfWeek = daysOfWeek, + weekState = calendarState.weekState, + selectionState = calendarState.selectionState, + today = today, + weekDaysScrollEnabled = weekDaysScrollEnabled, + dayContent = dayContent, + daysOfWeekHeader = daysOfWeekHeader, + ) + } else { + WeekContent( + daysOfWeek = daysOfWeek, + weekDays = calendarState.weekState.currentWeek.getWeekDays(today), + selectionState = calendarState.selectionState, + weekDaysScrollEnabled = weekDaysScrollEnabled, + dayContent = dayContent, + daysOfWeekHeader = daysOfWeekHeader, + ) + } + } +} + +/** + * Helper function for providing a [WeekCalendarState] implementation with selection mechanism. + * + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param initialWeek initially rendered month + * @param initialSelection initial selection of the composable + * @param initialSelectionMode initial mode of the selection + * @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change + * @param minWeek first week that can be shown + * @param maxWeek last week that can be shown + */ +@Composable +@SuppressLint("NewApi") +public fun rememberSelectableWeekCalendarState( + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + initialWeek: Week = Week.now(firstDayOfWeek), + minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange), + maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange), + initialSelection: List = emptyList(), + initialSelectionMode: SelectionMode = SelectionMode.Single, + confirmSelectionChange: (newValue: List) -> Boolean = { true }, + weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) { + WeekState( + initialWeek = initialWeek, + minWeek = minWeek, + maxWeek = maxWeek, + ) + }, + selectionState: DynamicSelectionState = rememberSaveable( + saver = DynamicSelectionState.Saver(confirmSelectionChange), + ) { + DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode) + }, +): WeekCalendarState = + remember { WeekCalendarState(weekState, selectionState) } + +/** + * Helper function for providing a [WeekCalendarState] implementation without a selection mechanism. + * + * @param firstDayOfWeek first day of a week, defaults to current locale's + * @param initialWeek initially rendered week + * @param minWeek first week that can be shown + * @param maxWeek last week that can be shown + */ +@Composable +@SuppressLint("NewApi") +public fun rememberWeekCalendarState( + firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek, + initialWeek: Week = Week.now(firstDayOfWeek), + minWeek: Week = initialWeek.minusWeeks(DefaultCalendarPagerRange), + maxWeek: Week = initialWeek.plusWeeks(DefaultCalendarPagerRange), + weekState: WeekState = rememberSaveable(saver = WeekState.Saver()) { + WeekState( + initialWeek = initialWeek, + minWeek = minWeek, + maxWeek = maxWeek, + ) + }, +): WeekCalendarState = + remember { WeekCalendarState(weekState, EmptySelectionState) } diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt new file mode 100644 index 000000000..e476ce8b7 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/Day.kt @@ -0,0 +1,16 @@ +package io.github.boguszpawlowski.composecalendar.day + +import java.time.LocalDate + +/** + * Container for basic info about the displayed day + * + * @param date local date of the day + * @param isCurrentDay whenever the day is the today's date + * @param isFromCurrentMonth whenever the day is from currently rendered month + */ +public interface Day { + public val date: LocalDate + public val isCurrentDay: Boolean + public val isFromCurrentMonth: Boolean +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt new file mode 100644 index 000000000..a10a29967 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DayState.kt @@ -0,0 +1,16 @@ +package io.github.boguszpawlowski.composecalendar.day + +import androidx.compose.runtime.Stable +import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionState + +public typealias NonSelectableDayState = DayState + +/** + * Contains information about current selection as well as date of rendered day + */ +@Stable +public data class DayState( + private val day: Day, + val selectionState: T, +) : Day by day diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt new file mode 100644 index 000000000..2afd0d4ba --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/DefaultDay.kt @@ -0,0 +1,63 @@ +package io.github.boguszpawlowski.composecalendar.day + +import android.annotation.SuppressLint +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import java.time.LocalDate + +/** + * Default implementation for day content. It supports different appearance for days from + * current and adjacent month, as well as current day and selected day + * + * @param selectionColor color of the border, when day is selected + * @param currentDayColor color of content for the current date + * @param onClick callback for interacting with day clicks + */ +@Composable +@SuppressLint("NewApi") +public fun DefaultDay( + state: DayState, + modifier: Modifier = Modifier, + selectionColor: Color = MaterialTheme.colors.secondary, + currentDayColor: Color = MaterialTheme.colors.primary, + onClick: (LocalDate) -> Unit = {}, +) { + val date = state.date + val selectionState = state.selectionState + + val isSelected = selectionState.isDateSelected(date) + + Card( + modifier = modifier + .aspectRatio(1f) + .padding(2.dp), + elevation = if (state.isFromCurrentMonth) 4.dp else 0.dp, + border = if (state.isCurrentDay) BorderStroke(1.dp, currentDayColor) else null, + contentColor = if (isSelected) selectionColor else contentColorFor( + backgroundColor = MaterialTheme.colors.surface + ) + ) { + Box( + modifier = Modifier.clickable { + onClick(date) + selectionState.onDateSelected(date) + }, + contentAlignment = Alignment.Center, + ) { + Text(text = date.dayOfMonth.toString()) + } + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt new file mode 100644 index 000000000..7ac0002e9 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/day/WeekDay.kt @@ -0,0 +1,11 @@ +package io.github.boguszpawlowski.composecalendar.day + +import androidx.compose.runtime.Immutable +import java.time.LocalDate + +@Immutable +internal class WeekDay( + override val date: LocalDate, + override val isCurrentDay: Boolean, + override val isFromCurrentMonth: Boolean +) : Day diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt new file mode 100644 index 000000000..61b27449c --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/debug/RecomposeHighlighter.kt @@ -0,0 +1,97 @@ +package io.github.boguszpawlowski.composecalendar.debug + +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlin.math.min + +/** + * A [Modifier] that draws a border around elements that are recomposing. The border increases in + * size and interpolates from red to green as more recompositions occur before a timeout. + */ +@Stable +internal fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier) + +// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations +// Modifier.composed will still remember unique data per call site. +private val recomposeModifier = + Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) { + // The total number of compositions that have occurred. We're not using a State<> here be + // able to read/write the value without invalidating (which would cause infinite + // recomposition). + val totalCompositions = remember { arrayOf(0L) } + totalCompositions[0]++ + + // The value of totalCompositions at the last timeout. + val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) } + + // Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions + // as the key is really just to cause the timer to restart every composition). + LaunchedEffect(totalCompositions[0]) { + delay(3000) + totalCompositionsAtLastTimeout.value = totalCompositions[0] + } + + Modifier.drawWithCache { + onDrawWithContent { + // Draw actual content. + drawContent() + + // Below is to draw the highlight, if necessary. A lot of the logic is copied from + // Modifier.border + val numCompositionsSinceTimeout = + totalCompositions[0] - totalCompositionsAtLastTimeout.value + + val hasValidBorderParams = size.minDimension > 0f + if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) { + return@onDrawWithContent + } + + val (color, strokeWidthPx) = + when (numCompositionsSinceTimeout) { + // We need at least one composition to draw, so draw the smallest border + // color in blue. + 1L -> Color.Blue to 1f + // 2 compositions is _probably_ okay. + 2L -> Color.Green to 2.dp.toPx() + // 3 or more compositions before timeout may indicate an issue. lerp the + // color from yellow to red, and continually increase the border size. + else -> lerp( + Color.Yellow.copy(alpha = 0.8f), + Color.Red.copy(alpha = 0.5f), + min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f) + ) to numCompositionsSinceTimeout.toInt().dp.toPx() + } + + val halfStroke = strokeWidthPx / 2 + val topLeft = Offset(halfStroke, halfStroke) + val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) + + val fillArea = strokeWidthPx * 2 > size.minDimension + val rectTopLeft = if (fillArea) Offset.Zero else topLeft + val size = if (fillArea) size else borderSize + val style = if (fillArea) Fill else Stroke(strokeWidthPx) + + drawRect( + brush = SolidColor(color), + topLeft = rectTopLeft, + size = size, + style = style + ) + } + } + } diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt new file mode 100644 index 000000000..653587cee --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultMonthHeader.kt @@ -0,0 +1,87 @@ +package io.github.boguszpawlowski.composecalendar.header + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import java.time.format.TextStyle.FULL +import java.util.Locale + +/** + * Default implementation of month header, shows current month and year, as well as + * 2 arrows for changing currently showed month + */ +@Composable +@SuppressLint("NewApi") +public fun DefaultMonthHeader( + monthState: MonthState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + DecrementButton(monthState = monthState) + Text( + modifier = Modifier.testTag("MonthLabel"), + text = monthState.currentMonth.month + .getDisplayName(FULL, Locale.getDefault()) + .lowercase() + .replaceFirstChar { it.titlecase() }, + style = MaterialTheme.typography.h4, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h4) + IncrementButton(monthState = monthState) + } +} + +@Composable +private fun DecrementButton( + monthState: MonthState, +) { + IconButton( + modifier = Modifier.testTag("Decrement"), + enabled = monthState.currentMonth > monthState.minMonth, + onClick = { monthState.currentMonth = monthState.currentMonth.minusMonths(1) } + ) { + Image( + imageVector = Icons.Default.KeyboardArrowLeft, + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + contentDescription = "Previous", + ) + } +} + +@Composable +private fun IncrementButton( + monthState: MonthState, +) { + IconButton( + modifier = Modifier.testTag("Increment"), + enabled = monthState.currentMonth < monthState.maxMonth, + onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) } + ) { + Image( + imageVector = Icons.Default.KeyboardArrowRight, + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + contentDescription = "Next", + ) + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt new file mode 100644 index 000000000..cb99137b2 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/DefaultWeekHeader.kt @@ -0,0 +1,91 @@ +package io.github.boguszpawlowski.composecalendar.header + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowLeft +import androidx.compose.material.icons.filled.KeyboardArrowRight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import java.time.format.TextStyle.FULL +import java.util.Locale + +/** + * Default implementation of week header, shows current month and year, as well as + * 2 arrows for changing currently showed month + */ +@Composable +@Suppress("LongMethod") +@SuppressLint("NewApi") +public fun DefaultWeekHeader( + weekState: WeekState, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + DecrementButton(weekState = weekState) + Text( + modifier = Modifier.testTag("WeekLabel"), + text = weekState.currentWeek.yearMonth.month + .getDisplayName(FULL, Locale.getDefault()) + .lowercase() + .replaceFirstChar { it.titlecase() }, + style = MaterialTheme.typography.h4, + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = weekState.currentWeek.yearMonth.year.toString(), + style = MaterialTheme.typography.h4 + ) + IncrementButton(monthState = weekState) + } +} + +@Composable +private fun DecrementButton( + weekState: WeekState, +) { + IconButton( + modifier = Modifier.testTag("Decrement"), + enabled = weekState.currentWeek > weekState.minWeek, + onClick = { weekState.currentWeek = weekState.currentWeek.dec() } + ) { + Image( + imageVector = Icons.Default.KeyboardArrowLeft, + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + contentDescription = "Previous", + ) + } +} + +@Composable +private fun IncrementButton( + monthState: WeekState, +) { + IconButton( + modifier = Modifier.testTag("Increment"), + enabled = monthState.currentWeek < monthState.maxWeek, + onClick = { monthState.currentWeek = monthState.currentWeek.inc() } + ) { + Image( + imageVector = Icons.Default.KeyboardArrowRight, + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + contentDescription = "Next", + ) + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt new file mode 100644 index 000000000..293479896 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/MonthState.kt @@ -0,0 +1,76 @@ +package io.github.boguszpawlowski.composecalendar.header + +import android.annotation.SuppressLint +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import androidx.compose.runtime.setValue +import java.time.YearMonth + +@Suppress("FunctionName") // Factory function +public fun MonthState( + initialMonth: YearMonth, + minMonth: YearMonth, + maxMonth: YearMonth, +): MonthState = MonthStateImpl(initialMonth, minMonth, maxMonth) + +@Stable +public interface MonthState { + public var currentMonth: YearMonth + public var minMonth: YearMonth + public var maxMonth: YearMonth + + public companion object { + public fun Saver(): Saver = mapSaver( + save = { monthState -> + mapOf( + "currentMonth" to monthState.currentMonth.toString(), + "minMonth" to monthState.minMonth.toString(), + "maxMonth" to monthState.maxMonth.toString(), + ) + }, + restore = { restoreMap -> + MonthState( + YearMonth.parse(restoreMap["currentMonth"] as String), + YearMonth.parse(restoreMap["minMonth"] as String), + YearMonth.parse(restoreMap["maxMonth"] as String), + ) + } + ) + } +} + +@Stable +@SuppressLint("NewApi") +private class MonthStateImpl( + initialMonth: YearMonth, + minMonth: YearMonth, + maxMonth: YearMonth, +) : MonthState { + + private var _currentMonth by mutableStateOf(initialMonth) + private var _minMonth by mutableStateOf(minMonth) + private var _maxMonth by mutableStateOf(maxMonth) + + override var currentMonth: YearMonth + get() = _currentMonth + set(value) { + _currentMonth = value + } + + override var minMonth: YearMonth + get() = _minMonth + set(value) { + if (value > _maxMonth) return + _minMonth = value + } + + override var maxMonth: YearMonth + get() = _maxMonth + set(value) { + if (value < _minMonth) return + _maxMonth = value + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt new file mode 100644 index 000000000..f64182a4d --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/header/WeekState.kt @@ -0,0 +1,86 @@ +package io.github.boguszpawlowski.composecalendar.header + +import android.annotation.SuppressLint +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import androidx.compose.runtime.setValue +import io.github.boguszpawlowski.composecalendar.week.Week +import java.time.LocalDate + +@Suppress("FunctionName") // Factory function +public fun WeekState( + initialWeek: Week, + minWeek: Week, + maxWeek: Week, +): WeekState = WeekStateImpl( + initialWeek = initialWeek, + minWeek = minWeek, + maxWeek = maxWeek, +) + +@Stable +@SuppressLint("NewApi") +public interface WeekState { + public var currentWeek: Week + public var minWeek: Week + public var maxWeek: Week + + public companion object { + @Suppress("FunctionName") // Factory function + public fun Saver(): Saver = mapSaver( + save = { weekState -> + mapOf( + CurrentWeekKey to weekState.currentWeek.days[0].toString(), + MinWeekKey to weekState.minWeek.days[0].toString(), + MaxWeekKey to weekState.maxWeek.days[0].toString(), + ) + }, + restore = { restoreMap -> + WeekState( + initialWeek = Week(firstDay = LocalDate.parse(restoreMap[CurrentWeekKey] as String)), + minWeek = Week(firstDay = LocalDate.parse(restoreMap[MinWeekKey] as String)), + maxWeek = Week(firstDay = LocalDate.parse(restoreMap[MaxWeekKey] as String)), + ) + } + ) + + private const val CurrentWeekKey = "CurrentWeek" + private const val MinWeekKey = "MinWeek" + private const val MaxWeekKey = "MaxWeek" + } +} + +@Stable +private class WeekStateImpl( + initialWeek: Week, + minWeek: Week, + maxWeek: Week, +) : WeekState { + + private var _currentWeek by mutableStateOf(initialWeek) + private var _minWeek by mutableStateOf(minWeek) + private var _maxWeek by mutableStateOf(maxWeek) + + override var currentWeek: Week + get() = _currentWeek + set(value) { + _currentWeek = value + } + + override var minWeek: Week + get() = _minWeek + set(value) { + if (value > _maxWeek) return + _minWeek = value + } + + override var maxWeek: Week + get() = _maxWeek + set(value) { + if (value < _minWeek) return + _maxWeek = value + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt new file mode 100644 index 000000000..2d02ed18c --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthContent.kt @@ -0,0 +1,162 @@ +package io.github.boguszpawlowski.composecalendar.month + +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import dev.chrisbanes.snapper.ExperimentalSnapperApi +import dev.chrisbanes.snapper.SnapOffsets +import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults +import dev.chrisbanes.snapper.SnapperLayoutInfo +import dev.chrisbanes.snapper.rememberSnapperFlingBehavior +import io.github.boguszpawlowski.composecalendar.day.DayState +import io.github.boguszpawlowski.composecalendar.header.MonthState +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import io.github.boguszpawlowski.composecalendar.week.WeekRow +import io.github.boguszpawlowski.composecalendar.week.getWeeks +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth +import java.time.temporal.ChronoUnit + +@OptIn(ExperimentalSnapperApi::class) +@Composable +@Suppress("LongMethod", "NewApi") +internal fun MonthPager( + initialMonth: YearMonth, + showAdjacentMonths: Boolean, + selectionState: T, + monthState: MonthState, + daysOfWeek: List, + today: LocalDate, + modifier: Modifier = Modifier, + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit, + weekHeader: @Composable BoxScope.(List) -> Unit, + monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + + val initialFirstVisibleItemIndex = remember(initialMonth, monthState.minMonth) { + ChronoUnit.MONTHS.between(monthState.minMonth, initialMonth).toInt() + } + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = initialFirstVisibleItemIndex, + ) + val flingBehavior = rememberSnapperFlingBehavior( + lazyListState = listState, + snapOffsetForItem = SnapOffsets.Start, + springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec, + decayAnimationSpec = rememberSplineBasedDecay(), + snapIndex = coerceSnapIndex, + ) + + val monthListState = remember { + MonthListState( + coroutineScope = coroutineScope, + monthState = monthState, + listState = listState, + ) + } + + Column(modifier = Modifier.fillMaxWidth()) { + if (weekDaysScrollEnabled.not()) { + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + content = { weekHeader(daysOfWeek) }, + ) + } + val pagerCount = remember(monthState.minMonth, monthState.maxMonth) { + ChronoUnit.MONTHS.between(monthState.minMonth, monthState.maxMonth).toInt() + 1 + } + LazyRow( + modifier = modifier.testTag("MonthPager"), + state = listState, + flingBehavior = flingBehavior, + verticalAlignment = Alignment.Top, + ) { + items( + count = pagerCount, + key = { index -> + monthListState.getMonthForPage(index).let { "${it.year}-${it.monthValue}" } + } + ) { index -> + MonthContent( + modifier = Modifier.fillParentMaxWidth(), + showAdjacentMonths = showAdjacentMonths, + selectionState = selectionState, + currentMonth = monthListState.getMonthForPage(index), + today = today, + weekDaysScrollEnabled = weekDaysScrollEnabled, + daysOfWeek = daysOfWeek, + dayContent = dayContent, + weekHeader = weekHeader, + monthContainer = monthContainer + ) + } + } + } +} + +@Composable +internal fun MonthContent( + showAdjacentMonths: Boolean, + selectionState: T, + currentMonth: YearMonth, + daysOfWeek: List, + today: LocalDate, + modifier: Modifier = Modifier, + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit, + weekHeader: @Composable BoxScope.(List) -> Unit, + monthContainer: @Composable (content: @Composable (PaddingValues) -> Unit) -> Unit, +) { + Column { + if (weekDaysScrollEnabled) { + Box( + modifier = modifier + .wrapContentHeight(), + content = { weekHeader(daysOfWeek) }, + ) + } + monthContainer { paddingValues -> + Column( + modifier = modifier + .padding(paddingValues) + ) { + currentMonth.getWeeks( + includeAdjacentMonths = showAdjacentMonths, + firstDayOfTheWeek = daysOfWeek.first(), + today = today, + ).forEach { week -> + WeekRow( + weekDays = week, + selectionState = selectionState, + dayContent = dayContent, + ) + } + } + } + } +} + +@OptIn(ExperimentalSnapperApi::class) +internal val coerceSnapIndex: (SnapperLayoutInfo, startIndex: Int, targetIndex: Int) -> Int = + { _, startIndex, targetIndex -> + targetIndex + .coerceIn(startIndex - 1, startIndex + 1) + } diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt new file mode 100644 index 000000000..d85fe2448 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/month/MonthListState.kt @@ -0,0 +1,74 @@ +package io.github.boguszpawlowski.composecalendar.month + +import android.annotation.SuppressLint +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow +import io.github.boguszpawlowski.composecalendar.header.MonthState +import io.github.boguszpawlowski.composecalendar.util.throttleOnOffset +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.time.YearMonth +import java.time.temporal.ChronoUnit + +@Stable +@SuppressLint("NewApi") +internal class MonthListState( + private val coroutineScope: CoroutineScope, + private val monthState: MonthState, + private val listState: LazyListState, +) { + + private val currentFirstVisibleMonth by derivedStateOf { + getMonthForPage(listState.firstVisibleItemIndex) + } + + init { + snapshotFlow { monthState.currentMonth }.onEach { month -> + moveToMonth(month) + }.launchIn(coroutineScope) + + with(listState) { + snapshotFlow { currentFirstVisibleMonth } + .throttleOnOffset() + .onEach { newMonth -> + monthState.currentMonth = newMonth + }.launchIn(coroutineScope) + } + } + + fun getMonthForPage(index: Int): YearMonth = + monthState.minMonth.plusMonths(index.toLong()) + + private fun moveToMonth(month: YearMonth) { + if (month == currentFirstVisibleMonth) return + coroutineScope.launch { + listState.animateScrollToItem(ChronoUnit.MONTHS.between(monthState.minMonth, month).toInt()) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MonthListState + + if (monthState != other.monthState) return false + if (listState != other.listState) return false + + return true + } + + override fun hashCode(): Int { + var result = monthState.hashCode() + result = 31 * result + listState.hashCode() + return result + } +} + +private operator fun YearMonth.minus(other: YearMonth) = + ChronoUnit.MONTHS.between(other, this) diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt new file mode 100644 index 000000000..d3f244e36 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionHandler.kt @@ -0,0 +1,35 @@ +package io.github.boguszpawlowski.composecalendar.selection + +import android.annotation.SuppressLint +import io.github.boguszpawlowski.composecalendar.util.addOrRemoveIfExists +import java.time.LocalDate + +/** + * Helper class for calculating new selection, when using a [DynamicSelectionState] approach. + * @param date clicked on date + * @param selection current selection + * @param selectionMode current selection mode + * @returns new selection in a form of a list of local dates. + */ +@SuppressLint("NewApi") +public object DynamicSelectionHandler { + public fun calculateNewSelection( + date: LocalDate, + selection: List, + selectionMode: SelectionMode, + ): List = when (selectionMode) { + SelectionMode.None -> emptyList() + SelectionMode.Single -> if (date == selection.firstOrNull()) { + emptyList() + } else { + listOf(date) + } + SelectionMode.Multiple -> selection.addOrRemoveIfExists(date) + SelectionMode.Period -> when { + date.isBefore(selection.startOrMax()) -> listOf(date) + date.isAfter(selection.startOrMax()) -> selection.fillUpTo(date) + date == selection.startOrMax() -> emptyList() + else -> selection + } + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt new file mode 100644 index 000000000..0fdb7060d --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionExtensions.kt @@ -0,0 +1,15 @@ +package io.github.boguszpawlowski.composecalendar.selection + +import android.annotation.SuppressLint +import java.time.LocalDate + +@SuppressLint("NewApi") +internal fun Collection.startOrMax() = firstOrNull() ?: LocalDate.MAX + +internal fun Collection.endOrNull() = drop(1).lastOrNull() + +@SuppressLint("NewApi") +internal fun Collection.fillUpTo(date: LocalDate) = + (0..date.toEpochDay() - first().toEpochDay()).map { + first().plusDays(it) + } diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt new file mode 100644 index 000000000..7a1aeee3b --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionMode.kt @@ -0,0 +1,15 @@ +package io.github.boguszpawlowski.composecalendar.selection + +/** + * Selection modes for the [DynamicSelectionState] + * None - no selection allowed + * Single - only one date selected + * Multiple - multiple dates can be selected + * Period - period can be selected + */ +public enum class SelectionMode { + None, + Single, + Multiple, + Period, +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt new file mode 100644 index 000000000..79448f5a7 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt @@ -0,0 +1,83 @@ +package io.github.boguszpawlowski.composecalendar.selection + +import android.annotation.SuppressLint +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.setValue +import java.time.LocalDate + +@Stable +public interface SelectionState { + public fun isDateSelected(date: LocalDate): Boolean = false + public fun onDateSelected(date: LocalDate) { } +} + +/** + * Class that enables for dynamically changing selection modes in the runtime. Depending on the mode, selection changes differently. + * Mode can be varied by setting desired [SelectionMode] in the [selectionMode] mutable property. + * @param confirmSelectionChange return false from this callback to veto the selection change + */ +@Stable +@SuppressLint("NewApi") +public class DynamicSelectionState( + private val confirmSelectionChange: (newValue: List) -> Boolean = { true }, + selection: List, + selectionMode: SelectionMode, +) : SelectionState { + + private var _selection by mutableStateOf(selection) + private var _selectionMode by mutableStateOf(selectionMode) + + public var selection: List + get() = _selection + set(value) { + if (value != selection && confirmSelectionChange(value)) { + _selection = value + } + } + + public var selectionMode: SelectionMode + get() = _selectionMode + set(value) { + if (value != selectionMode) { + _selection = emptyList() + _selectionMode = value + } + } + + override fun isDateSelected(date: LocalDate): Boolean = selection.contains(date) + + override fun onDateSelected(date: LocalDate) { + selection = DynamicSelectionHandler.calculateNewSelection(date, selection, selectionMode) + } + + internal companion object { + @Suppress("FunctionName", "UNCHECKED_CAST") // Factory function + fun Saver( + confirmSelectionChange: (newValue: List) -> Boolean, + ): Saver = + listSaver( + save = { raw -> + listOf(raw.selectionMode, raw.selection.map { it.toString() }) + }, + restore = { restored -> + DynamicSelectionState( + confirmSelectionChange = confirmSelectionChange, + selectionMode = restored[0] as SelectionMode, + selection = (restored[1] as? List)?.map { LocalDate.parse(it) }.orEmpty(), + ) + } + ) + } +} + +@Immutable +public object EmptySelectionState : SelectionState { + override fun isDateSelected(date: LocalDate): Boolean = false + + override fun onDateSelected(date: LocalDate): Unit = Unit +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt new file mode 100644 index 000000000..76154cf8d --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/DateUtil.kt @@ -0,0 +1,13 @@ +package io.github.boguszpawlowski.composecalendar.util + +import java.time.DayOfWeek +import java.time.LocalDate + +internal fun Collection.addOrRemoveIfExists(date: LocalDate) = + if (contains(date)) { + this - date + } else { + this + date + } + +internal infix fun DayOfWeek.daysUntil(other: DayOfWeek) = (7 + (value - other.value)) % 7 diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt new file mode 100644 index 000000000..07c576d95 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/ListStateExtensions.kt @@ -0,0 +1,19 @@ +package io.github.boguszpawlowski.composecalendar.util + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map + +context(LazyListState) internal fun Flow.throttleOnOffset() = + combine( + snapshotFlow { firstVisibleItemScrollOffset } + ) { newMonth, offset -> + newMonth to (offset <= MinimalOffsetForEmit) + }.filter { (_, shouldUpdate) -> + shouldUpdate + }.map { (newValue, _) -> newValue } + +private const val MinimalOffsetForEmit = 10 diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt new file mode 100644 index 000000000..f3479b044 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/util/YearMonthExtensions.kt @@ -0,0 +1,7 @@ +package io.github.boguszpawlowski.composecalendar.util + +import java.time.YearMonth + +internal operator fun YearMonth.dec() = this.minusMonths(1) + +internal operator fun YearMonth.inc() = this.plusMonths(1) diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt new file mode 100644 index 000000000..b5d7cdb8f --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/DefaultWeekHeader.kt @@ -0,0 +1,61 @@ +package io.github.boguszpawlowski.composecalendar.week + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import java.time.DayOfWeek +import java.time.format.TextStyle.SHORT +import java.util.Locale +import kotlin.DeprecationLevel.WARNING + +@Composable +@Deprecated( + level = WARNING, + replaceWith = ReplaceWith( + "DefaultDaysOfWeekHeader(daysOfWeek, modifier)", + "io.github.boguszpawlowski.composecalendar.week.DefaultDaysOfWeekHeader", + ), + message = "Replace with DefaultDaysOfWeekHeader, DefaultWeekHeader will be removed in future versions" +) +@SuppressLint("NewApi") +public fun DefaultWeekHeader( + daysOfWeek: List, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + daysOfWeek.forEach { dayOfWeek -> + Text( + textAlign = TextAlign.Center, + text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()), + modifier = modifier + .weight(1f) + .wrapContentHeight() + ) + } + } +} + +@Composable +@SuppressLint("NewApi") +public fun DefaultDaysOfWeekHeader( + daysOfWeek: List, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + daysOfWeek.forEach { dayOfWeek -> + Text( + textAlign = TextAlign.Center, + text = dayOfWeek.getDisplayName(SHORT, Locale.getDefault()), + modifier = modifier + .weight(1f) + .wrapContentHeight() + ) + } + } +} + +internal fun Array.rotateRight(n: Int): List = takeLast(n) + dropLast(n) diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt new file mode 100644 index 000000000..815cedc26 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/Week.kt @@ -0,0 +1,55 @@ +package io.github.boguszpawlowski.composecalendar.week + +import android.annotation.SuppressLint +import io.github.boguszpawlowski.composecalendar.selection.fillUpTo +import io.github.boguszpawlowski.composecalendar.util.daysUntil +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth +import java.time.temporal.ChronoUnit +import java.time.temporal.WeekFields +import java.util.Locale + +@SuppressLint("NewApi") +public data class Week( + val days: List, +) { + + init { + require(days.size == DaysInAWeek) + } + + internal constructor(firstDay: LocalDate) : this( + listOf(firstDay).fillUpTo(firstDay.plusDays((DaysInAWeek - 1).toLong())) + ) + + public val start: LocalDate get() = days.first() + + public val end: LocalDate get() = days.last() + + public val yearMonth: YearMonth = YearMonth.of(start.year, start.month) + + public operator fun inc(): Week = plusWeeks(1) + + public operator fun dec(): Week = plusWeeks(-1) + + public operator fun compareTo(other: Week): Int = start.compareTo(other.start) + + public fun minusWeeks(value: Long): Week = plusWeeks(-value) + + public fun plusWeeks(value: Long): Week = copy(days = days.map { it.plusWeeks(value) }) + + public companion object { + public fun now(firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek): Week { + val today = LocalDate.now() + val offset = today.dayOfWeek.daysUntil(firstDayOfWeek) + val firstDay = today.minusDays(offset.toLong()) + + return Week(firstDay) + } + } +} + +@SuppressLint("NewApi") +public fun ChronoUnit.between(first: Week, other: Week): Int = + between(first.start, other.start).toInt() diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt new file mode 100644 index 000000000..374e175d5 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekDays.kt @@ -0,0 +1,10 @@ +package io.github.boguszpawlowski.composecalendar.week + +import androidx.compose.runtime.Immutable +import io.github.boguszpawlowski.composecalendar.day.Day + +@Immutable +internal data class WeekDays( + val isFirstWeekOfTheMonth: Boolean = false, + val days: List, +) diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt new file mode 100644 index 000000000..3dbc66da2 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekListState.kt @@ -0,0 +1,70 @@ +package io.github.boguszpawlowski.composecalendar.week + +import android.annotation.SuppressLint +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow +import io.github.boguszpawlowski.composecalendar.header.WeekState +import io.github.boguszpawlowski.composecalendar.util.throttleOnOffset +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import java.time.temporal.ChronoUnit + +@Stable +@SuppressLint("NewApi") +internal class WeekListState( + private val coroutineScope: CoroutineScope, + private val weekState: WeekState, + private val listState: LazyListState, +) { + + private val currentlyFirstVisibleMonth by derivedStateOf { + getWeekForPage(listState.firstVisibleItemIndex) + } + + init { + snapshotFlow { weekState.currentWeek }.onEach { week -> + moveToWeek(week) + }.launchIn(coroutineScope) + + with(listState) { + snapshotFlow { currentlyFirstVisibleMonth } + .throttleOnOffset() + .onEach { newMonth -> + weekState.currentWeek = newMonth + }.launchIn(coroutineScope) + } + } + + fun getWeekForPage(index: Int): Week = + weekState.minWeek.plusWeeks(index.toLong()) + + private fun moveToWeek(week: Week) { + if (week == currentlyFirstVisibleMonth) return + coroutineScope.launch { + listState.animateScrollToItem(ChronoUnit.WEEKS.between(weekState.minWeek, week)) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as WeekListState + + if (weekState != other.weekState) return false + if (listState != other.listState) return false + + return true + } + + override fun hashCode(): Int { + var result = weekState.hashCode() + result = 31 * result + listState.hashCode() + return result + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt new file mode 100644 index 000000000..5354d0e63 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekPager.kt @@ -0,0 +1,150 @@ +package io.github.boguszpawlowski.composecalendar.week + +import android.annotation.SuppressLint +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import dev.chrisbanes.snapper.ExperimentalSnapperApi +import dev.chrisbanes.snapper.SnapOffsets +import dev.chrisbanes.snapper.SnapperFlingBehaviorDefaults +import dev.chrisbanes.snapper.rememberSnapperFlingBehavior +import io.github.boguszpawlowski.composecalendar.day.DayState +import io.github.boguszpawlowski.composecalendar.day.WeekDay +import io.github.boguszpawlowski.composecalendar.header.WeekState +import io.github.boguszpawlowski.composecalendar.month.coerceSnapIndex +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.temporal.ChronoUnit + +@OptIn(ExperimentalSnapperApi::class) +@Composable +@Suppress("LongMethod") +@SuppressLint("NewApi") +internal fun WeekPager( + initialWeek: Week, + selectionState: T, + weekState: WeekState, + daysOfWeek: List, + today: LocalDate, + modifier: Modifier = Modifier, + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + + val initialFirstVisibleItemIndex = remember(initialWeek, weekState.minWeek) { + ChronoUnit.WEEKS.between(weekState.minWeek, initialWeek) + } + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = initialFirstVisibleItemIndex, + ) + val flingBehavior = rememberSnapperFlingBehavior( + lazyListState = listState, + snapOffsetForItem = SnapOffsets.Start, + springAnimationSpec = SnapperFlingBehaviorDefaults.SpringAnimationSpec, + decayAnimationSpec = rememberSplineBasedDecay(), + snapIndex = coerceSnapIndex, + ) + + val weekListState = remember { + WeekListState( + coroutineScope = coroutineScope, + weekState = weekState, + listState = listState, + ) + } + Column( + modifier = modifier, + ) { + if (weekDaysScrollEnabled.not()) { + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + content = { daysOfWeekHeader(daysOfWeek) }, + ) + } + val pagerCount = remember(weekState.minWeek, weekState.maxWeek) { + ChronoUnit.WEEKS.between(weekState.minWeek, weekState.maxWeek) + 1 + } + LazyRow( + modifier = modifier.testTag("WeekPager"), + state = listState, + flingBehavior = flingBehavior, + verticalAlignment = Alignment.Top, + ) { + items( + count = pagerCount, + key = { index -> weekListState.getWeekForPage(index).start.let { "${it.month}-${it.dayOfMonth}" } }, + ) { index -> + WeekContent( + modifier = Modifier.fillParentMaxWidth(), + daysOfWeek = daysOfWeek, + weekDays = weekListState.getWeekForPage(index).getWeekDays(today = today), + selectionState = selectionState, + weekDaysScrollEnabled = weekDaysScrollEnabled, + dayContent = dayContent, + daysOfWeekHeader = daysOfWeekHeader, + ) + } + } + } +} + +@Composable +internal fun WeekContent( + selectionState: T, + weekDays: WeekDays, + daysOfWeek: List, + modifier: Modifier = Modifier, + weekDaysScrollEnabled: Boolean = true, + dayContent: @Composable BoxScope.(DayState) -> Unit, + daysOfWeekHeader: @Composable BoxScope.(List) -> Unit, +) { + Column( + modifier = modifier, + ) { + if (weekDaysScrollEnabled) { + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + content = { daysOfWeekHeader(daysOfWeek) }, + ) + } + WeekRow( + weekDays = weekDays, + modifier = Modifier.fillMaxWidth(), + selectionState = selectionState, + dayContent = dayContent, + ) + } +} + +internal fun Week.getWeekDays(today: LocalDate): WeekDays { + val weekDays = days.map { + WeekDay( + date = it, + isCurrentDay = it == today, + isFromCurrentMonth = true, + ) + } + + return WeekDays( + isFirstWeekOfTheMonth = false, + days = weekDays, + ) +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt new file mode 100644 index 000000000..61881e178 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekRow.kt @@ -0,0 +1,35 @@ +package io.github.boguszpawlowski.composecalendar.week + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.github.boguszpawlowski.composecalendar.day.DayState +import io.github.boguszpawlowski.composecalendar.selection.SelectionState + +@Composable +internal fun WeekRow( + weekDays: WeekDays, + selectionState: T, + modifier: Modifier = Modifier, + dayContent: @Composable BoxScope.(DayState) -> Unit +) { + Row( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = if (weekDays.isFirstWeekOfTheMonth) Arrangement.End else Arrangement.Start + ) { + weekDays.days.forEachIndexed { index, day -> + Box( + modifier = Modifier.fillMaxWidth(1f / (7 - index)) + ) { + dayContent(DayState(day, selectionState)) + } + } + } +} diff --git a/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt new file mode 100644 index 000000000..e926e14e8 --- /dev/null +++ b/ComposeCalendar/library/src/main/java/io/github/boguszpawlowski/composecalendar/week/WeekUtil.kt @@ -0,0 +1,52 @@ +package io.github.boguszpawlowski.composecalendar.week + +import android.annotation.SuppressLint +import io.github.boguszpawlowski.composecalendar.day.WeekDay +import io.github.boguszpawlowski.composecalendar.util.daysUntil +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth + +internal const val DaysInAWeek = 7 + +@SuppressLint("NewApi") +internal fun YearMonth.getWeeks( + includeAdjacentMonths: Boolean, + firstDayOfTheWeek: DayOfWeek, + today: LocalDate, +): List { + val daysLength = lengthOfMonth() + + val starOffset = atDay(1).dayOfWeek daysUntil firstDayOfTheWeek + val endOffset = + DaysInAWeek - (atDay(daysLength).dayOfWeek daysUntil firstDayOfTheWeek) - 1 + + return (1 - starOffset..daysLength + endOffset).chunked(DaysInAWeek).mapIndexed { index, days -> + WeekDays( + isFirstWeekOfTheMonth = index == 0, + days = days.mapNotNull { dayOfMonth -> + val (date, isFromCurrentMonth) = when (dayOfMonth) { + in Int.MIN_VALUE..0 -> if (includeAdjacentMonths) { + val previousMonth = this.minusMonths(1) + previousMonth.atDay(previousMonth.lengthOfMonth() + dayOfMonth) to false + } else { + return@mapNotNull null + } + in 1..daysLength -> atDay(dayOfMonth) to true + else -> if (includeAdjacentMonths) { + val previousMonth = this.plusMonths(1) + previousMonth.atDay(dayOfMonth - daysLength) to false + } else { + return@mapNotNull null + } + } + + WeekDay( + date = date, + isFromCurrentMonth = isFromCurrentMonth, + isCurrentDay = date.equals(today), + ) + } + ) + } +} diff --git a/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt new file mode 100644 index 000000000..75ca49c09 --- /dev/null +++ b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt @@ -0,0 +1,177 @@ +@file:Suppress("UnderscoresInNumericLiterals") + +package io.github.boguszpawlowski.composecalendar.selection + +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.shouldBe +import java.time.LocalDate +import java.time.Month.APRIL + +internal class SelectionStateTest : ShouldSpec({ + + val yesterday = LocalDate.of(2020, APRIL, 9) + val today = LocalDate.of(2020, APRIL, 10) + val tomorrow = LocalDate.of(2020, APRIL, 11) + + context("Selection state with SelectionMode.None") { + should("not change selection after new value arrives") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None) + + state.onDateSelected(LocalDate.now()) + + state.selection shouldBe emptyList() + } + + should("be able to change if mode has been changed") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None) + + state.selectionMode = SelectionMode.Single + state.onDateSelected(today) + + state.selection shouldBe listOf(today) + } + } + + context("Selection state with SelectionMode.Single") { + should("change state to single after day is selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single) + + state.onDateSelected(today) + + state.selection shouldBe listOf(today) + } + + should("change state to none when same day is selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single) + + state.onDateSelected(today) + state.onDateSelected(today) + + state.selection shouldBe emptyList() + } + + should("change to other day when selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single) + + state.onDateSelected(today) + state.onDateSelected(tomorrow) + + state.selection shouldBe listOf(tomorrow) + } + + should("not be mutable after selection mode is changed to None") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single) + + state.selectionMode = SelectionMode.None + state.onDateSelected(today) + + state.selection shouldBe emptyList() + } + } + + context("Selection state with SelectionMode.Multiple") { + should("allow for multiple days selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple) + + state.onDateSelected(today) + state.onDateSelected(tomorrow) + + state.selection shouldContainExactly listOf( + today, + tomorrow + ) + } + + should("switch selection off once day is selected second time") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple) + + state.onDateSelected(today) + state.onDateSelected(tomorrow) + state.onDateSelected(today) + + state.selection shouldContainExactly listOf(tomorrow) + } + } + + context("Selection state with SelectionMode.Period") { + should("allow for period of days selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period) + + state.onDateSelected(today) + state.onDateSelected(tomorrow) + + state.selection.first() shouldBe today + state.selection.last() shouldBe tomorrow + } + + should("switch selection off once start day is selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period) + + state.onDateSelected(today) + state.onDateSelected(tomorrow) + state.onDateSelected(today) + + state.selection shouldBe emptyList() + } + should("change end date once the date selected is between start and the end") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period) + + state.onDateSelected(yesterday) + state.onDateSelected(tomorrow) + state.onDateSelected(today) + + state.selection.first() shouldBe yesterday + state.selection.last() shouldBe today + } + should("change start day once day before start is selected") { + val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period) + + state.onDateSelected(today) + state.onDateSelected(tomorrow) + state.onDateSelected(yesterday) + + state.selection.first() shouldBe yesterday + state.selection.endOrNull() shouldBe null + } + } + + context("Selection State interface default values") { + val myInterfaceWithNoMethods = object : SelectionState {} + + should("Default isDateSelected to false") { + myInterfaceWithNoMethods.isDateSelected(today) shouldBe false + } + + should("Have a default implementation that doesn't throw an exception for onDateSelected") { + shouldNotThrowAny { + myInterfaceWithNoMethods.onDateSelected(today) + } + } + } + context("Selection State with confirm state change callback") { + var nextVetoResult = false + val initialSelection = LocalDate.of(1999, 10, 12) + val newSelection = initialSelection.plusDays(1) + + val selectionState = DynamicSelectionState( + confirmSelectionChange = { nextVetoResult }, + selection = listOf(initialSelection), + selectionMode = Multiple, + ) + should("Not change the selection when change is vetoed") { + selectionState.onDateSelected(newSelection) + + selectionState.selection shouldBe listOf(initialSelection) + } + should("Change the selection when change is not vetoed") { + nextVetoResult = true + + selectionState.onDateSelected(newSelection) + + selectionState.selection shouldBe listOf(initialSelection, newSelection) + } + } +}) diff --git a/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt new file mode 100644 index 000000000..c59c4fcf6 --- /dev/null +++ b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekTest.kt @@ -0,0 +1,68 @@ +package io.github.boguszpawlowski.composecalendar.week + +import io.kotest.assertions.asClue +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.shouldBe +import java.time.LocalDate + +internal class WeekTest : ShouldSpec({ + + val today = LocalDate.of(2000, 10, 2) + + context("Secondary constructor") { + val week = Week(firstDay = today) + + should("properly create an instance of week") { + week.asClue { + it.days.first() shouldBe today + it.days.last() shouldBe today.plusDays(6) + it.days.size shouldBe 7 + } + } + } + + context("Incrementing") { + val week = Week(firstDay = today).inc() + + should("shift the days by 7") { + week.asClue { + it.days.first() shouldBe today.plusWeeks(1) + it.days.last() shouldBe today.plusDays(6).plusWeeks(1) + it.days.size shouldBe 7 + } + } + } + + context("Decrementing") { + val week = Week(firstDay = today).dec() + + should("shift the days by -7") { + week.asClue { + it.days.first() shouldBe today.plusWeeks(-1) + it.days.last() shouldBe today.plusDays(6).plusWeeks(-1) + it.days.size shouldBe 7 + } + } + } + + context("Adding a week") { + val week = Week(firstDay = today).plusWeeks(7) + + should("add correct number of days") { + week.asClue { + it.days.first() shouldBe today.plusWeeks(7) + it.days.last() shouldBe today.plusDays(6).plusWeeks(7) + it.days.size shouldBe 7 + } + } + } + + context("Constructing a week with invalid number of days") { + should("throw") { + shouldThrow { + Week(List(18) { LocalDate.now() }) + } + } + } +}) diff --git a/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt new file mode 100644 index 000000000..17ce731f0 --- /dev/null +++ b/ComposeCalendar/library/src/test/java/io/github/boguszpawlowski/composecalendar/week/WeekUtilTest.kt @@ -0,0 +1,166 @@ +@file:Suppress("UnderscoresInNumericLiterals") + +package io.github.boguszpawlowski.composecalendar.week + +import io.kotest.core.spec.style.ShouldSpec +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import java.time.DayOfWeek.MONDAY +import java.time.DayOfWeek.SATURDAY +import java.time.DayOfWeek.SUNDAY +import java.time.LocalDate +import java.time.YearMonth + +internal class WeekUtilTest : ShouldSpec({ + + val today = LocalDate.of(2020, 9, 15) + val month = YearMonth.of(2020, 9) + val previousMonth = YearMonth.of(2020, 8) + val nextMonth = YearMonth.of(2020, 10) + + context("Extracting weeks without adjacent months") { + val includeAdjacentMonths = false + should("return days only from current month") { + val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today) + val days = weeks.flatMap { it.days } + + days.map { it.date }.forEach { + it.isFromMonth(month) shouldBe true + it.isFromMonth(previousMonth) shouldBe false + it.isFromMonth(nextMonth) shouldBe false + } + } + + should("return all days from the month") { + val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today) + val days = weeks.flatMap { it.days }.map { it.date } + + val daysLength = month.lengthOfMonth() + val daysFromCurrentMonth = (1..daysLength).map { month.atDay(it) } + + days shouldContainExactly daysFromCurrentMonth + } + + should("return days properly split to weeks") { + val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today) + + weeks[0].days shouldHaveSize 6 + weeks[1].days shouldHaveSize 7 + weeks[2].days shouldHaveSize 7 + weeks[3].days shouldHaveSize 7 + weeks[4].days shouldHaveSize 3 + + weeks shouldHaveSize 5 + } + + should("return days properly split to weeks when first day is Saturday") { + val weeks = month.getWeeks(includeAdjacentMonths, SATURDAY, today) + + weeks[0].days shouldHaveSize 4 + weeks[1].days shouldHaveSize 7 + weeks[2].days shouldHaveSize 7 + weeks[3].days shouldHaveSize 7 + weeks[4].days shouldHaveSize 5 + + weeks shouldHaveSize 5 + } + + should("return days properly split to weeks when first day is Sunday ") { + val weeks = month.getWeeks(includeAdjacentMonths, SUNDAY, today) + + weeks[0].days shouldHaveSize 5 + weeks[1].days shouldHaveSize 7 + weeks[2].days shouldHaveSize 7 + weeks[3].days shouldHaveSize 7 + weeks[4].days shouldHaveSize 4 + + weeks shouldHaveSize 5 + } + } + + context("Extracting weeks with adjacent months") { + val includeAdjacentMonths = true + should("return days from current and previous months") { + val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today) + val days = weeks.flatMap { it.days } + + days.map { it.date }.forEachIndexed { index, day -> + when (index) { + 0 -> { + day.isFromMonth(month) shouldBe false + day.isFromMonth(previousMonth) shouldBe true + day.isFromMonth(nextMonth) shouldBe false + } + in 1..30 -> { + day.isFromMonth(month) shouldBe true + day.isFromMonth(previousMonth) shouldBe false + day.isFromMonth(nextMonth) shouldBe false + } + else -> { + day.isFromMonth(month) shouldBe false + day.isFromMonth(previousMonth) shouldBe false + day.isFromMonth(nextMonth) shouldBe true + } + } + } + } + + should("return all days from the month") { + val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today) + val days = weeks.flatMap { it.days }.map { it.date } + + val daysLength = month.lengthOfMonth() + val endOffset = 6 - month.atEndOfMonth().dayOfWeek.ordinal + val startOffset = month.atDay(1).dayOfWeek.ordinal - 1 + val daysFromCurrentMonth = (1..daysLength).map { month.atDay(it) } + val daysFromNextMonth = (1..endOffset).map { nextMonth.atDay(it) } + val daysFromPreviousMonth = + (previousMonth.lengthOfMonth() - startOffset..previousMonth.lengthOfMonth()).map { + previousMonth.atDay(it) + } + + days shouldContainAll daysFromCurrentMonth + daysFromNextMonth + daysFromPreviousMonth + } + + should("return days properly split to weeks") { + val weeks = month.getWeeks(includeAdjacentMonths, MONDAY, today) + + weeks[0].days shouldHaveSize 7 + weeks[1].days shouldHaveSize 7 + weeks[2].days shouldHaveSize 7 + weeks[3].days shouldHaveSize 7 + weeks[4].days shouldHaveSize 7 + + weeks shouldHaveSize 5 + } + + should("return days properly split to weeks when first day is Sunday") { + val weeks = month.getWeeks(includeAdjacentMonths, SUNDAY, today) + + weeks[0].days shouldHaveSize 7 + weeks[1].days shouldHaveSize 7 + weeks[2].days shouldHaveSize 7 + weeks[3].days shouldHaveSize 7 + weeks[4].days shouldHaveSize 7 + + weeks shouldHaveSize 5 + } + + should("return days properly split to weeks when first day is Saturday") { + val weeks = month.getWeeks(includeAdjacentMonths, SATURDAY, today) + + weeks[0].days shouldHaveSize 7 + weeks[1].days shouldHaveSize 7 + weeks[2].days shouldHaveSize 7 + weeks[3].days shouldHaveSize 7 + weeks[4].days shouldHaveSize 7 + + weeks shouldHaveSize 5 + } + } +}) + +private fun LocalDate.isFromMonth(yearMonth: YearMonth) = + month == yearMonth.month && year == yearMonth.year diff --git a/ComposeCalendar/sample/.gitignore b/ComposeCalendar/sample/.gitignore new file mode 100644 index 000000000..a2def884f --- /dev/null +++ b/ComposeCalendar/sample/.gitignore @@ -0,0 +1,2 @@ +/build +google-services.json diff --git a/ComposeCalendar/sample/build.gradle.kts b/ComposeCalendar/sample/build.gradle.kts new file mode 100644 index 000000000..7734761e4 --- /dev/null +++ b/ComposeCalendar/sample/build.gradle.kts @@ -0,0 +1,60 @@ +import org.ajoberstar.grgit.Grgit + +plugins { + id(Android.ApplicationPluginId) + kotlin(Kotlin.AndroidPluginId) + id("common-android-plugin") +} + +val commitsCount = Grgit.open(mapOf("dir" to rootDir)).log().size + +android { + namespace = "io.github.boguszpawlowski.composecalendar.sample" + defaultConfig { + versionCode = commitsCount + versionName = "0.0.1" + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + packaging { + resources { + excludes.addAll(listOf("META-INF/AL2.0", "META-INF/LGPL2.1")) + } + } + +// compileOptions { +// isCoreLibraryDesugaringEnabled = true +// } +} + +dependencies { +// coreLibraryDesugaring(Kotlin.DesugarJdkLibs) + +// implementation(project(":library")) + implementation(project(":kotlinx-datetime")) + + implementation("io.github.boguszpawlowski.composecalendar:composecalendar:1.3.0") + + implementation(Material.Core) + + implementation(AndroidX.AppCompat) + implementation(AndroidX.ComposeActivity) + + implementation(Compose.Runtime) + implementation(Compose.Navigation) + implementation(Compose.Ui) + implementation(Compose.UiTooling) + implementation(Compose.Foundation) + implementation(Compose.FoundationLayout) + implementation(Compose.Material) + implementation(Timber.Core) + + debugImplementation(Debug.LeakCanary) + debugImplementation(Hyperion.Core) + debugImplementation(Hyperion.Crash) + debugImplementation(Hyperion.GeigerCounter) + debugImplementation(Hyperion.Measurement) + debugImplementation(ComposeTest.Manifest) + + androidTestImplementation(ComposeTest.Core) +} diff --git a/ComposeCalendar/sample/proguard-rules.pro b/ComposeCalendar/sample/proguard-rules.pro new file mode 100644 index 000000000..2f9dc5a47 --- /dev/null +++ b/ComposeCalendar/sample/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt b/ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt new file mode 100644 index 000000000..b468e3fea --- /dev/null +++ b/ComposeCalendar/sample/src/androidTest/java/io/github/boguszpawlowski/composecalendar/sample/ScrollBehaviorTest.kt @@ -0,0 +1,50 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeLeft +import androidx.compose.ui.test.swipeRight +import io.github.boguszpawlowski.composecalendar.StaticCalendar +import io.github.boguszpawlowski.composecalendar.rememberCalendarState +import org.junit.Rule +import org.junit.Test +import java.time.YearMonth + +internal class ScrollBehaviorTest { + + private val initialMonth = YearMonth.of(2012, 5) + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun monthChangeTest() { + composeTestRule.setContent { + StaticCalendar( + calendarState = rememberCalendarState(initialMonth = initialMonth) + ) + } + + composeTestRule.onNodeWithTag("Decrement").performClick() + composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("April") + composeTestRule.onNodeWithTag("Increment").performClick() + composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("May") + } + + @Test + fun scrollMonthChangeTest() { + composeTestRule.setContent { + StaticCalendar( + calendarState = rememberCalendarState(initialMonth = initialMonth) + ) + } + + composeTestRule.onNodeWithTag("MonthPager").performTouchInput { swipeLeft() } + composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("June") + composeTestRule.onNodeWithTag("MonthPager").performTouchInput { swipeRight() } + composeTestRule.onNodeWithTag("MonthLabel", true).assertTextEquals("May") + } +} diff --git a/ComposeCalendar/sample/src/main/AndroidManifest.xml b/ComposeCalendar/sample/src/main/AndroidManifest.xml new file mode 100644 index 000000000..67df51341 --- /dev/null +++ b/ComposeCalendar/sample/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt new file mode 100644 index 000000000..4a77f4907 --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomComponentsSample.kt @@ -0,0 +1,104 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.StaticCalendar +import io.github.boguszpawlowski.composecalendar.day.NonSelectableDayState +import io.github.boguszpawlowski.composecalendar.header.MonthState +import java.time.DayOfWeek +import java.time.DayOfWeek.SUNDAY +import java.time.format.TextStyle.NARROW +import java.util.Locale + +@Composable +fun CustomComponentsSample() { + StaticCalendar( + modifier = Modifier.animateContentSize(), + showAdjacentMonths = false, + firstDayOfWeek = SUNDAY, + monthContainer = { MonthContainer(it) }, + dayContent = { DayContent(dayState = it) }, + daysOfWeekHeader = { WeekHeader(daysOfWeek = it) }, + monthHeader = { MonthHeader(monthState = it) }, + ) +} + +@Composable +private fun DayContent(dayState: NonSelectableDayState) { + Text( + text = dayState.date.dayOfMonth.toString(), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.h6, + ) +} + +@Composable +private fun WeekHeader(daysOfWeek: List) { + Row { + daysOfWeek.forEach { dayOfWeek -> + Text( + textAlign = TextAlign.Center, + text = dayOfWeek.getDisplayName(NARROW, Locale.ROOT), + modifier = Modifier + .weight(1f) + .wrapContentHeight() + ) + } + } +} + +@Composable +private fun MonthHeader(monthState: MonthState) { + Row { + Text(monthState.currentMonth.year.toString(), style = MaterialTheme.typography.h3) + Text(monthState.currentMonth.month.name, style = MaterialTheme.typography.h3) + IconButton(onClick = { monthState.currentMonth = monthState.currentMonth.plusMonths(1) }) { + Image( + imageVector = Icons.Default.Star, + colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface), + contentDescription = "Next", + ) + } + } +} + +@Composable +private fun MonthContainer(content: @Composable (PaddingValues) -> Unit) { + Card( + elevation = 0.dp, + shape = RoundedCornerShape(10.dp), + border = BorderStroke(1.dp, Color.LightGray), + content = { content(PaddingValues(4.dp)) }, + ) +} + +@Preview +@Composable +private fun CustomComponentsPreview() { + MaterialTheme { + Surface { + CustomComponentsSample() + } + } +} diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt new file mode 100644 index 000000000..47bbe09a9 --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/CustomSelectionSample.kt @@ -0,0 +1,66 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.github.boguszpawlowski.composecalendar.Calendar +import io.github.boguszpawlowski.composecalendar.CalendarState +import io.github.boguszpawlowski.composecalendar.header.MonthState +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import java.time.LocalDate +import java.time.YearMonth + +@Composable +fun CustomSelectionSample() { + Calendar(calendarState = rememberMonthSelectionState()) +} + +private class MonthSelectionState( + initialSelection: YearMonth? = null, +) : SelectionState { + + private var selection by mutableStateOf(initialSelection) + + override fun isDateSelected(date: LocalDate): Boolean = + date.yearMonth == selection + + override fun onDateSelected(date: LocalDate) { + selection = if (date.yearMonth == selection) null else date.yearMonth + } + + companion object { + @Suppress("FunctionName") // Factory function + fun Saver(): Saver = Saver( + save = { it.selection?.toString() }, + restore = { restored -> + val selection = (restored as? String)?.let { YearMonth.parse(it) } + MonthSelectionState(selection) + } + ) + } +} + +@Composable +private fun rememberMonthSelectionState( + initialMonth: YearMonth = YearMonth.now(), + minMonth: YearMonth = initialMonth.minusMonths(10000), + maxMonth: YearMonth = initialMonth.plusMonths(10000), + initialSelection: YearMonth? = null, + monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) { + MonthState( + initialMonth = initialMonth, + minMonth = minMonth, + maxMonth = maxMonth + ) + }, + selectionState: MonthSelectionState = rememberSaveable(saver = MonthSelectionState.Saver()) { + MonthSelectionState(initialSelection = initialSelection) + } +): CalendarState = remember { CalendarState(monthState, selectionState) } + +private val LocalDate.yearMonth: YearMonth + get() = YearMonth.of(year, month) diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt new file mode 100644 index 000000000..bdbc3bef1 --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt @@ -0,0 +1,103 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.SelectableCalendar +import io.github.boguszpawlowski.composecalendar.kotlinxDateTime.now +import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState +import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple +import io.github.boguszpawlowski.composecalendar.selection.SelectionState +import kotlinx.datetime.LocalDate +import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toKotlinLocalDate + +@Composable +fun KotlinXDateTimeSample() { + var selection by remember { mutableStateOf(emptyList()) } + + Column { + DateTimeCalendar( + today = LocalDate.now(), + onSelectionChanged = { selection = it }, + dayContent = { DayContent(dayState = it) }, + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Selection: ${selection.joinToString { it.toString() }}", + style = MaterialTheme.typography.h6, + ) + } +} + +@Composable +fun BoxScope.DayContent( + dayState: KotlinDayState, +) { + val isSelected = dayState.selectionState.isDateSelected(dayState.date) + + Text( + text = dayState.date.dayOfMonth.toString(), + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center) + .aspectRatio(1f) + .clickable { + dayState.selectionState.onDateSelected(dayState.date) + }, + color = if (isSelected) Color.Red else Color.Unspecified, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.h6, + ) +} + +@Composable +fun DateTimeCalendar( + today: LocalDate, + onSelectionChanged: (List) -> Unit, + dayContent: @Composable BoxScope.(KotlinDayState) -> Unit, +) { + SelectableCalendar( + calendarState = rememberSelectableCalendarState( + confirmSelectionChange = { selection -> onSelectionChanged(selection.map { it.toKotlinLocalDate() }); true }, + initialSelectionMode = Multiple, + ), + today = today.toJavaLocalDate(), + showAdjacentMonths = false, + dayContent = { dayState -> + dayContent( + KotlinDayState( + date = dayState.date.toKotlinLocalDate(), + isCurrentDay = dayState.isCurrentDay, + selectionState = dayState.selectionState, + ) + ) + } + ) +} + +data class KotlinDayState( + val date: LocalDate, + val isCurrentDay: Boolean, + val selectionState: T, +) + +private fun SelectionState.isDateSelected(date: LocalDate) = isDateSelected(date.toJavaLocalDate()) +private fun SelectionState.onDateSelected(date: LocalDate) = onDateSelected(date.toJavaLocalDate()) diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt new file mode 100644 index 000000000..2a883777b --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MainActivity.kt @@ -0,0 +1,113 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import io.github.boguszpawlowski.composecalendar.BuildConfig +import timber.log.Timber +import timber.log.Timber.DebugTree + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (BuildConfig.DEBUG) { + Timber.plant(DebugTree()) + } + setContent { + MainScreen() + } + } +} + +@Composable +fun MainScreen() { + MaterialTheme( + colors = if (isSystemInDarkTheme()) darkColors() else lightColors() + ) { + Surface( + modifier = Modifier.fillMaxSize(), + ) { + val navController = rememberNavController() + + NavHost(navController = navController, startDestination = "main") { + composable("main") { MainMenu(navController = navController) } + composable("static") { StaticCalendarSample() } + composable("week") { WeekCalendarSample() } + composable("selection") { SelectableCalendarSample() } + composable("components") { CustomComponentsSample() } + composable("custom_selection") { CustomSelectionSample() } + composable("viewmodel") { ViewModelSample() } + composable("kotlinx_datetime") { KotlinXDateTimeSample() } + composable("min_max_month") { MinMaxCalendarSample() } + } + } + } +} + +@Composable +fun MainMenu(navController: NavController) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Button(onClick = { navController.navigate("static") }) { + Text(text = "Static Calendar") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("selection") }) { + Text(text = "Selectable Calendar") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("week") }) { + Text(text = "Week Calendar") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("components") }) { + Text(text = "Custom Components") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("custom_selection") }) { + Text(text = "Custom Selection") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("viewmodel") }) { + Text(text = "ViewModel") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("kotlinx_datetime") }) { + Text(text = "Kotlinx DateTime") + } + Spacer(modifier = Modifier.height(16.dp)) + + Button(onClick = { navController.navigate("min_max_month") }) { + Text(text = "Min Max Month") + } + } +} diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt new file mode 100644 index 000000000..03128ea4d --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/MinMaxCalendarSample.kt @@ -0,0 +1,268 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.CalendarState +import io.github.boguszpawlowski.composecalendar.StaticCalendar +import io.github.boguszpawlowski.composecalendar.StaticWeekCalendar +import io.github.boguszpawlowski.composecalendar.WeekCalendarState +import io.github.boguszpawlowski.composecalendar.rememberCalendarState +import io.github.boguszpawlowski.composecalendar.rememberWeekCalendarState +import io.github.boguszpawlowski.composecalendar.selection.EmptySelectionState +import io.github.boguszpawlowski.composecalendar.week.Week +import java.time.YearMonth +import java.time.format.DateTimeFormatter + +@Composable +fun MinMaxCalendarSample() { + val calendarState = rememberCalendarState( + initialMonth = YearMonth.now(), + minMonth = YearMonth.now(), + maxMonth = YearMonth.now() + ) + + val weekCalendarState = rememberWeekCalendarState( + initialWeek = Week.now(), + minWeek = Week.now(), + maxWeek = Week.now(), + ) + + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + StaticCalendar(calendarState = calendarState) + + Spacer(modifier = Modifier.height(20.dp)) + + MinMaxControls(calendarState = calendarState) + + Spacer(modifier = Modifier.height(40.dp)) + + StaticWeekCalendar(calendarState = weekCalendarState) + + Spacer(modifier = Modifier.height(20.dp)) + + WeekMinMaxControls(calendarState = weekCalendarState) + } +} + +@Composable +private fun MinMaxControls( + calendarState: CalendarState, +) { + val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM") } + Text( + text = "Calendar Min Month", + style = MaterialTheme.typography.h5, + ) + MinControls( + calendarState = calendarState, + dateFormatter = dateFormatter + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = "Calendar Max Month", + style = MaterialTheme.typography.h5, + ) + MaxControls( + calendarState = calendarState, + dateFormatter = dateFormatter + ) +} + +@Composable +private fun MinControls( + calendarState: CalendarState, + dateFormatter: DateTimeFormatter, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .clickable { + calendarState.monthState.minMonth = calendarState.monthState.minMonth.minusMonths(1L) + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "-", + textAlign = TextAlign.Center + ) + val minYearMonthText = remember(calendarState.monthState.minMonth) { + dateFormatter.format(calendarState.monthState.minMonth) + } + Text(text = minYearMonthText) + Text( + modifier = Modifier + .clickable { + calendarState.monthState.minMonth = calendarState.monthState.minMonth.plusMonths(1L) + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "+", + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun MaxControls( + calendarState: CalendarState, + dateFormatter: DateTimeFormatter, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .clickable { + calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.minusMonths(1L) + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "-", + textAlign = TextAlign.Center + ) + val maxYearMonthText = remember(calendarState.monthState.maxMonth) { + dateFormatter.format(calendarState.monthState.maxMonth) + } + Text(text = maxYearMonthText) + Text( + modifier = Modifier + .clickable { + calendarState.monthState.maxMonth = calendarState.monthState.maxMonth.plusMonths(1L) + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "+", + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun WeekMinMaxControls( + calendarState: WeekCalendarState, +) { + val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd") } + Text( + text = "Calendar Min Week", + style = MaterialTheme.typography.h5, + ) + WeekMinControls( + calendarState = calendarState, + dateFormatter = dateFormatter + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = "Calendar Max Week", + style = MaterialTheme.typography.h5, + ) + WeekMaxControls( + calendarState = calendarState, + dateFormatter = dateFormatter + ) +} + +@Composable +private fun WeekMinControls( + calendarState: WeekCalendarState, + dateFormatter: DateTimeFormatter, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .clickable { + calendarState.weekState.minWeek = --calendarState.weekState.minWeek + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "-", + textAlign = TextAlign.Center + ) + val minYearMonthText = remember(calendarState.weekState.minWeek) { + val start = dateFormatter.format(calendarState.weekState.minWeek.start) + val end = dateFormatter.format(calendarState.weekState.minWeek.end) + "$start - $end" + } + Text(text = minYearMonthText) + Text( + modifier = Modifier + .clickable { + calendarState.weekState.minWeek = ++calendarState.weekState.minWeek + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "+", + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun WeekMaxControls( + calendarState: WeekCalendarState, + dateFormatter: DateTimeFormatter, +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier + .clickable { + calendarState.weekState.maxWeek = --calendarState.weekState.maxWeek + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "-", + textAlign = TextAlign.Center + ) + val maxYearMonthText = remember(calendarState.weekState.maxWeek) { + val start = dateFormatter.format(calendarState.weekState.maxWeek.start) + val end = dateFormatter.format(calendarState.weekState.maxWeek.end) + "$start - $end" + } + Text(text = maxYearMonthText) + Text( + modifier = Modifier + .clickable { + calendarState.weekState.maxWeek = ++calendarState.weekState.maxWeek + } + .size(25.dp) + .border(1.dp, Color.Black), + text = "+", + textAlign = TextAlign.Center + ) + } +} diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt new file mode 100644 index 000000000..254707dbb --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/SelectableCalendarSample.kt @@ -0,0 +1,57 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.SelectableCalendar +import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState +import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode + +@Composable +fun SelectableCalendarSample() { + val calendarState = rememberSelectableCalendarState() + + Column( + Modifier.verticalScroll(rememberScrollState()) + ) { + SelectableCalendar(calendarState = calendarState) + + SelectionControls(selectionState = calendarState.selectionState) + } +} + +@Composable +private fun SelectionControls( + selectionState: DynamicSelectionState, +) { + Text( + text = "Calendar Selection Mode", + style = MaterialTheme.typography.h5, + ) + SelectionMode.values().forEach { selectionMode -> + Row(modifier = Modifier.fillMaxWidth()) { + RadioButton( + selected = selectionState.selectionMode == selectionMode, + onClick = { selectionState.selectionMode = selectionMode } + ) + Text(text = selectionMode.name) + Spacer(modifier = Modifier.height(4.dp)) + } + } + + Text( + text = "Selection: ${selectionState.selection.joinToString { it.toString() }}", + style = MaterialTheme.typography.h6, + ) +} diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt new file mode 100644 index 000000000..1fe884242 --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/StaticCalendarSample.kt @@ -0,0 +1,17 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.StaticCalendar + +@Composable +fun StaticCalendarSample() { + StaticCalendar( + modifier = Modifier + .padding(8.dp) + .animateContentSize(), + ) +} diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt new file mode 100644 index 000000000..99dfcd17d --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt @@ -0,0 +1,199 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.github.boguszpawlowski.composecalendar.SelectableCalendar +import io.github.boguszpawlowski.composecalendar.day.DayState +import io.github.boguszpawlowski.composecalendar.header.MonthState +import io.github.boguszpawlowski.composecalendar.rememberSelectableCalendarState +import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Period +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.LocalDate +import java.time.YearMonth +import kotlin.random.Random +import kotlin.random.nextLong + +/** + * In this sample, calendar composable is wired with an ViewModel. It's purpose is to show how to use + * the composable in real world use-case, by an example implementation of a calendar + * which can display planned recipes along with their prices. The recipes will also be re-fetched from + * fake API every time month changes. + */ +@Composable +fun ViewModelSample() { + val viewModel = remember { RecipeViewModel() } + val recipes by viewModel.recipesFlow.collectAsState() + val selectedPrice by viewModel.selectedRecipesPriceFlow.collectAsState(0) + val monthState = rememberMonthStateDefault() + LaunchedEffect(monthState) { + snapshotFlow { monthState.currentMonth } + .onEach { viewModel.onMonthChanged(it) } + .launchIn(this) + } + val state = rememberSelectableCalendarState( + confirmSelectionChange = { viewModel.onSelectionChanged(it); true }, + monthState = monthState, + initialSelectionMode = Period, + ) + Column( + Modifier.verticalScroll(rememberScrollState()) + ) { + SelectableCalendar( + calendarState = state, + dayContent = { dayState -> + RecipeDay( + state = dayState, + plannedRecipe = recipes.firstOrNull { it.date == dayState.date }, + ) + } + ) + Spacer(modifier = Modifier.height(20.dp)) + Text( + text = "Selected recipes price: $selectedPrice", + style = MaterialTheme.typography.h6, + ) + Spacer(modifier = Modifier.height(20.dp)) + } +} + +@Composable +private fun rememberMonthStateDefault() = rememberSaveable(saver = MonthState.Saver()) { + MonthState( + initialMonth = YearMonth.now(), + minMonth = YearMonth.now().minusMonths(10000), + maxMonth = YearMonth.now().plusMonths(10000), + ) +} + +/** + * Custom implementation of DayContent, which shows a dot if there is an recipe planned for this day. + */ +@Composable +fun RecipeDay( + state: DayState, + plannedRecipe: PlannedRecipe?, + modifier: Modifier = Modifier, +) { + val date = state.date + val selectionState = state.selectionState + + val isSelected = selectionState.isDateSelected(date) + + Card( + modifier = modifier + .aspectRatio(1f) + .padding(2.dp), + elevation = if (state.isFromCurrentMonth) 4.dp else 0.dp, + border = if (state.isCurrentDay) BorderStroke(1.dp, MaterialTheme.colors.primary) else null, + contentColor = if (isSelected) MaterialTheme.colors.secondary else contentColorFor( + backgroundColor = MaterialTheme.colors.surface + ) + ) { + Column( + modifier = Modifier.clickable { + selectionState.onDateSelected(date) + }, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text(text = date.dayOfMonth.toString()) + if (plannedRecipe != null) { + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background(MaterialTheme.colors.primary) + ) + Text( + text = plannedRecipe.price.toString(), + fontSize = 8.sp, + ) + } + } + } +} + +data class PlannedRecipe( + val date: LocalDate, + val price: Double, +) + +/** + * ViewModel exposing list of our recipes + */ +class RecipeViewModel : ViewModel() { + + private val selectionFlow = MutableStateFlow(emptyList()) + val recipesFlow = MutableStateFlow(getRecipes()) + val selectedRecipesPriceFlow = recipesFlow.combine(selectionFlow) { recipes, selection -> + recipes.filter { it.date in selection }.sumOf { it.price } + } + + fun onSelectionChanged(selection: List) { + selectionFlow.value = selection + } + + fun onMonthChanged(newMonth: YearMonth) = viewModelScope.launch { + recipesFlow.value = getRecipesFromApi(startingDay = newMonth.atDay(15)) + } + + private fun getRecipes(startingDay: LocalDate = LocalDate.now()) = listOf( + PlannedRecipe(startingDay.plusDays(Random.nextLong(1L..3L)), 20.0), + PlannedRecipe(startingDay.plusDays(Random.nextLong(4L..7L)), 20.0), + PlannedRecipe(startingDay.plusDays(Random.nextLong(8L..11L)), 10.0), + PlannedRecipe(startingDay.plusDays(Random.nextLong(-5L..-1L)), 25.0), + ) + + /** + * Simulated API call for new recipes + */ + private suspend fun getRecipesFromApi(startingDay: LocalDate) = withContext(Dispatchers.Default) { + delay(100) + getRecipes(startingDay) + } +} + +@Preview +@Composable +fun ViewModelSamplePreview() { + MaterialTheme { + ViewModelSample() + } +} diff --git a/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt new file mode 100644 index 000000000..9420be0cd --- /dev/null +++ b/ComposeCalendar/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/WeekCalendarSample.kt @@ -0,0 +1,57 @@ +package io.github.boguszpawlowski.composecalendar.sample + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.github.boguszpawlowski.composecalendar.SelectableWeekCalendar +import io.github.boguszpawlowski.composecalendar.rememberSelectableWeekCalendarState +import io.github.boguszpawlowski.composecalendar.selection.DynamicSelectionState +import io.github.boguszpawlowski.composecalendar.selection.SelectionMode + +@Composable +fun WeekCalendarSample() { + val calendarState = rememberSelectableWeekCalendarState() + + Column( + Modifier.verticalScroll(rememberScrollState()) + ) { + SelectableWeekCalendar(calendarState = calendarState) + + SelectionControls(selectionState = calendarState.selectionState) + } +} + +@Composable +private fun SelectionControls( + selectionState: DynamicSelectionState, +) { + Text( + text = "Calendar Selection Mode", + style = MaterialTheme.typography.h5, + ) + SelectionMode.values().forEach { selectionMode -> + Row(modifier = Modifier.fillMaxWidth()) { + RadioButton( + selected = selectionState.selectionMode == selectionMode, + onClick = { selectionState.selectionMode = selectionMode } + ) + Text(text = selectionMode.name) + Spacer(modifier = Modifier.height(4.dp)) + } + } + + Text( + text = "Selection: ${selectionState.selection.joinToString { it.toString() }}", + style = MaterialTheme.typography.h6, + ) +} diff --git a/ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..7706ab9e6 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml b/ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ComposeCalendar/sample/src/main/res/layout/activity_main.xml b/ComposeCalendar/sample/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..5f7f67a68 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6b78462d6 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6b78462d6 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a571e60098c92c2baca8a5df62f2929cbff01b52 GIT binary patch literal 3593 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 P00000NkvXXu0mjfP=x?Y literal 0 HcmV?d00001 diff --git a/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..61da551c5594a1f9d26193983d2cd69189014603 GIT binary patch literal 5339 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 literal 0 HcmV?d00001 diff --git a/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ComposeCalendar/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..b216f2d313cc673d8b8c4da591c174ebed52795c GIT binary patch literal 11873 zcmV-nE}qeeP)>j(mnvHsDN`- z)Hpc!RY~GsN8h7-e0h){1pPyutMv!xY8((UfI!|$uSc$h*USS<3D;)>jA&v@d9D7< zHT4Fjd$j16?%uwChG$oUbXRr5R1Xal{*3>Jzr)wyYfFQK2UQ7FC4)xfYKnLmrg}CT zknXNCFx_kFjC)(1$K4CqX>!La*yN7qWum)8&xqa=WfSER0aGsfzxV7lce(d?1>-gF zT6j&oHvWy`fRfqbDIfBK#+iKbXJl;cI`!U`>C-Z|ZJwUFC3f0BTOUu$+zK-?w}I2c zzrg0fKA2AaJ?-8WL7Gm4*T8GxHSyZ?Z`|7&Lw??be;eC?ZBfFcU=N%Wj6KBvZxnGY zW*HlYn%(vHHM_eZiRe8Mh?L<^HSumhuE(R}*~|XjpKX@0A;&bsKgTTHKNn@1?*FMI ziC%~AA@9X&;I$@Z1myD9r^@@g@42>+Hj%br8^zmsYn%e-Q zJ01asY3^x8Y3?9WsvAD%7~OWuCO_vGrn==C-gf&mAk`CW|2+V+?`;R8+vIh(-2}>= zUIVX%*Tie%-@w1c|4r5gk!Tx9TaD8^OlXWGW|a;qty1|t3YvTjXbn@{9SzdluNiU^ z!ztArCo!8S#{egkOmsn+hyeP9f?z06_+GpQUdx07sE`aesB*~9*{p4%w$iqfK44!8 zx@6^ymlHUykB{k(yz9H$@Q(YNJZRid*#?}2DRtuI2~Z)RxHe|9HgoMKeZf9q-;^Mg zAvod#XmH1E(8!GSL2i$a!N?3>9-M6U>6U8ZD-xi55?LlU+9$4W>w}EbJq8yy4$6lF zagKOwV4UiyM_@UH!0>}S;_kZa;@nfE0!YlwjYwaY?fU3w-iL$qnZ!)}#A7{Wd{oLq z9Gw0ct2>ZE+$|R0d_r(sA0CAfch(7>EJXweg?*xZBOuXODX-tVaV&}&Bjuwgt3!S^ zyzOpF2JWTUAm-#7|# z`yNb>^X^rtA>vKwyn8#kxj#Pszl~4MgXR5QS#vXYfKb`o-v`^DgwbbNu4D1fF4*v2 z5Sg%JU@pUT@V$5qycS+lLHd@3W9^c8=*iT0FZD|4&iEj1N&3F__74yKyMc6Q=hKKR z$AAAMpVmJF%jMw_*#9h+KFe|)Y{$+g;owgu-cE+=;Ct~JcrC^1TSOL)`I7WK56myD z?Odq>Yd(!MxVpO0pgUeEgVWcLPsL6O&#*La7?|cISZ3+|;Q8i!p>Z7KX9f6f5WwIcT{gIli9H^Jc;nVYHw=1SpQ z7lFssgJ0*VG=uy(1H>&jX6yg$47#zlJ~&4T=gRmUVS`&PV?_nyY>`k2P{sF+&IOs1 zepgq5)&=WH3bl*R)7IZ)QRxyI=d~uIkcu^ap zN`MroZ&;vr(*<;6Y-7lreO2M{5L@M}qJPWPMLh0N0;IrwBXiX68gXU8HfwS2Dr}{i z51I{9R_GRtdz1hvZr}KLNH56=dLNnJzhWTDGkaBuS&S>Grbh{o0``q}Wzn|DWDcv# z-Ia-4*G*UJ;#`*!AO-Imy0R-PK;!HpNBLSIZY8sdW|Un!l65_!uB(KiFeN~W**8|G z54v#<&%fI;;~QGhD34WY7W-5+xaGE8l5$ifKnmP9TwuJu3N+8#?87-N_q3i5ob@g{ z=@58wiwm5U09B5@@d34Nfjz^p{BlO8uZPm*N2~1c(`A;i0VI1*(V9sHAmT0=YhAe}LpS8KjTfWEvwOeZ#pNb=wC9g*co?D^%u3 z?j2;-$LZES9XwtIMH=}D8!CymJqe}Nb{-FpgQV{%N`8;e!NaWQkeizeS-IKp=d*Z0 z*THsRd$3)yv`5yyxj#GxA+P?1oZKARC+r*cQI_@y?As@tQ@d-sVAdZlCOFs5Wod=@ z%xhHIx^2=~pR%<;)9-G9lP@m8$DAxW;CJ3XhFSNvS6U0S`2O$kB&vH$Qx_Hth}coORr_6AxujsJMnz>RD@nll zJnIb|_y-@K!;HJzDjh%${~m;w*>7ndurJuBip(&vY7ysF@8WXk{inGz&belidG)f` z^FmcKxape2Quhi62n)}TJx>x@p|dZp(0jBh3qS)?S3}CXe?->jFA~dPpDKKbf&hdd zX$4tdC39YrTb-6+kBpCfbmQy{_|s6Oy&bu{)=I`_1i;g**P?(L&ugwM0HLem;lVy& zUld`DOSG^UXAj-CPaTGHFH=g-OxRcbt~vV%abM*L5L%o~{{_Pb7EogfEa~7^BtVlh zHo?6Q|D$cjwqqZ#FAB3rO6C|#U)2v;Zo#=1?#7t=>h3(QuEA~B6lsHJd92oszO!Bw zP-7P3MLyX=1{o)CXxdtO-7zF{`7wP1)ufC-m`KF`8~@&L@|wYEYeXm9OVc;wR1Y}# zEKZcRW83kXinPj(b4=Y>u+6PD)QZ|~AY%-^5JfZyY@ z;PdDdZIdK@o0qvm3R~qoy*wCm|ueH}s?oID#m1a>0T9L-7zgcs8c71)cM1bdal$rYTd~bX3S8@iZfsP_S{QnG z*)Pa~BBT^>#2 zAY?+KIEckR-!2*1bV|miOw$ZMg>zw8SZ12;Ph$ywKdCYb+m3x0o9?G@0O6eD+>Z`- zebCxew+)ShB&ic(rs^xr6V@8jGPh(=fMob;rSbsC=AXTg{3gB9f>Th5Z|;EgKYJ7l zATsCZeasTPvb%VWGp0;zm0(qxy{KBh2-_cLWc~sZ?goAus350!;UXb!qGGE2xxkZ` z{=XyED3SJ25l&yj4d03P0zXZ>`-pw5=o4sBwhs>EEWEQ52K;5S8<~&@AQk8S7z5QZ zy6${zTIN;^R&$Ih@GNEA0>Fhhd8{HUim%q%h-@J*xKe+>h?=jE(6`p^=@bJPhz_Bo@5Pw$X6Mu`BiRp=Vs11I+;(f>zz1B9!ne8IW23c8yJ zKZp3i_|wkxIpY2mg@ET{b`~7UhyaV2jW8)}HP|QafJ;x(1YHZq2FFO=0QHTu&+cqJ zSf8>{(rPphP`3>e`^Xz0{M{eVVg(IsNajW8xo0Ny+B=KWzFDCAhXtI=h_CR1vYofj zfzC-Q&^T^M^fQ(2sfB_eI`B9OOm2C|7oaHHEQtVO=Bb97w^=XaRL^(v1PC*YM;~7Z za$9I|#NpvJJ!mz&{7`Y3+_U$u;Kva6eDG+T;N+OR3*HKFXOG@LgIOt?zz~bRLdhkr0(BK)4P>voPD&ZRhsWmKdN;3kQEg()j<$ z3m_~$7h2cz^xaFCeSU2rcu=ONS5hlbQ2;%C{}M)Ba4rN7$|`;{y!a^0I^z50By6A% z8QgR&_cUJj!jh-0$M#V#9UxYT*lM(PTcew9neqS#|L@SVc)_>VV1{!nEebUEo9BZ^ z3% zE51hhef9?uNC(0AFi+4X!SjUh)v)hQi0szw!z&mSomf-}y3HYsrS^#9cjn^Aw&Cw^ossr>Jb~*@xHg zkiP%n@`hEC!vB#h{nq00VA&mT5W1 zC>fwu=9;z1bHhfQ z36vnnrYq0WK|j=1B;zm#Sdg%ZS|Y4yl(ndSLXr=txs0+vCR&Y@0H7{b-(wb5udDm$ zepBymeqUa<_25C_Ut*?5hlcVLBB*tFudt1(``Lt zqdY#eoohH0ndmU1f6Y<>VtIa@hJ8A=pPUwufdJ{>b}jQ83-RAyQk`?T)lX-C1e+_{ zDLgu%OF%!&mI1T|biH9cW&|WohA+o@jkO-hED&Kd(K)OM< z*@OCwz2p0o9xx^FfQ6y}!h;bqKRi)ReizW5pVjxV6BLMO6L^4I$GKgGD zKeay19R{7Zf6;NYjv=zZ77?pR1`q~IjT_e|Kerxrb#*ubBs7pN3ZQZ68zJ+}e{}0X zI=zNhAKubuY2H&vAGqsat&sTt2@zi7)yKEezxQK);SM|Q-Qjb=-<77!xBr9DaURrN z=||WxfV}g-Ves(kcX4@%5aC?ocZeAuSb#^|wWBOZ7(j~x>8AQ>^~iI}!NHDRWew1v zTdQGioIlJAT0`UoGtaNduVB>Le40gsg=1@@_QHY?f0%W_8)k(R*6dIprgeD=ns z1UyvHb{s^-xG%IoeUltPd&Bf?m`pX+?NVRT09q6WwHVS1GqI)`-jhbs6IunHlUQ69 zW{~1ci>->PB;-pn#HGG}4(K0T0CSG71_Sb}{>R)r9pu#ePjgOx%`2=!^QrnAo)6kb zEMfW?PZ)h_IcOZUfIhsASyFLDV3x%egHfGY0GdRm=UreX0ay3TBG5cz#p&$ALee_7 zC{IC5=dC#fTZ2i616apyfdL_oq770`i}Q)kwy46G_+S|UinJF4$hI&%3?K^8rNWko zKOd3&tsFJWAycFcp!3{V7a9jOB@NfYA z%m7-E2auHTZ~$3>X|M~md?J7Zz=ImV0~G2g7#@swC_qUBpm=YrWiA#T-58=+glI)R zh;WYagw|dM=G-K6{|#k;W1)(40I8@{Yhci>5yn9pXBPUF2SBvJ*H+PqD-9m?0}P-O zUIZX3!SGOkjuL>*@&H*%2ah;Fr+I*Upzj%L!SJBPLCcdLAnD;j8I%N&I6OpsW9?}{ zTEELH3b`+}_2YlVxv#I+rZK%ERZ4)wdw#-l>iR~=uZaF zUsi(Q>2t(_0JMMrw3-7*faT%g(c%FjF<0NS*2TjUR5CmiAOem}91oB%cre~Eh_VOE zfHx-s22`&c1XNYbKu zbY~b-6bBDl9JD;*011Hy-4zeenA03ULg1kQ5tn6l!4+na0KFhUl3JcZ0EIaUhKB>l zfdeQ(44_irp^A3^y=yCT^~s01=k8f}8b@a~_cf%Af5hEbb!Ng^_u4(%fj4pGbz`Ca zb!R$hMZv=ZH1{M2kWhFiK*tuqPv;mw0^z}UhX-hO0f3~12VE8gD1Ive$Vo6f2upr| z>?DRqmx#EoTVLjfYNhyXfgBemNS&$iI=hyx@99tu!2 z0q7zDD3JgpAv_eIM2FnI2@cR>_ssw5cWa}IbKX>~X+5FtE1w&y+ovU-4b$HEwB4_x z(|pVQOLs@!@P+|F_F(kaLZ(GvbZ8L_J7Nn9Pp^mXkJ^Fp5o=CIZ3^qy;yfKkEdk>b zocf7`Eu%6ygRAXFW1N;=~4GSXz zU`VhN3=DRFffrDYFfb%fgF>A06v}Hk3<~2kID9#bjdX|QiMzlw$^!;RtboChsFg4z ziq|R_5-l!g7#hPAi*kXXaV{`C-W_Z&@1*NQ!{S{zB@iXLGf+qp$^S=?8?Y^-q?x+>kuz;fKM73l{)%HwOloih)?&!PU*;_$LM?F(MP zyI|p&^q+PH$aU0c=q+d8CZx?B4@~@mOa$0t22PXmz%Kpl4u=&O*@JTrgwpVvi z*` zVQP?Psg`Fzk(P%OTAUeS-V~al7nT>YJo&6o5te6AIA?tZhp(WPXL-_ZU>fa7txwUG z#~Fsi6k&Oo^+An53v^`{U7a45;8vvN878tky!G+SL2IYsI|Ym9JJo4U=em}x?kj&V z-JJ&0Z8}&F979sRY)MmkSq~b=bt26(3u(+_cz7YTJca}&X=0v&>pVIqtYF4@FBo%{ z#6YF2^N7bhh0=5)y!U-hxG(4hEtV?gDVVAc40obdXJEu~sbZdj>pTWAj_~uPEigH0 zU5POdRRWEDK4Gax??23QnorQcmFG6~TGx{~crFMKl32TT`=)qvSr?5H3l1CHaFOUs z=*r@xdV{}R=!79S=&nQn34kXbK<5aYCl*K)Fc-H-C<5sGV!`lWpp4+;14sZoB7iP$ zg~`dJO{Kv@q?hQJgKbdrHa&}TTf1rPujz@b+?_ziTVVhXO<_&X1uCpx`Bf;mHrs3c>K8 z4C5SO0RnVU44|UmNpPgr2ix4mbtGn9U23&%+=kXZmr?Ls^vX0xXuJB|+iH_e{fmo> zC9O`E^_Q(U|8ociT(B1m55_wP(98>KIe<K8 zyE2S(5(B6xaERL?@aQHvaqB)ietJ|(t+_t6KCS9CEsNB>#FU;|A&%6}U46$p>S0|; zn!DTp!fbB%-)rbZQE;S$2ZbkuQGm|p0VEYXB7m&n$1o2LpbJX`!&3+#f$)d`x=H}L zL;xzn@*q6a`XoE$;yAUp8SH^`S>Dzse=LMs{IzPeCC^<+KpjC{*=^Tsd4Ay>ZouLs z_7PCeLjelm0kRSV4+V&r|8WGMxlw);AffP}#X)coAX?ij5FQFpJOZ?h0JJ_2pn~uu zIb~~;zuV1kVgi}N??}SlmX+?PmY4M@l#$ix(5xk{8MK(7F+wML*}LNQ$;$H^3lSom zENSa`bWbf30i-3R+Y(RJDL~;x03@KEXAl7h7YGMMuM`XqJu3(Sy2b!1;I=40NshUA zuUOALv)?x!N(1Lk<&}ArWQA~zpnlDk4Lgu$wQhlvR+ETc?f`LnXRA1fq^Rf7J-vul z5n?HZmH^AcXIt9A44`O#df1aJm4s+{@&P0O9tu#xat4r}2p|zWWRCix>pE%)o$SB& z!?|N~Sf9;lRTVircq>HD5mIST6OX{}rvB%=;C@$E7Rt)x@vY6cCWR9!>8?5gG>ZpF zhB8zNP=se5Kr&PkA~?7;K>-p74?Sp#0`v<^x$GwbhlfWmiLLqgjElrMV{_M-&81wd zPoaQXg)@JhYjtg|r+Lo$K34OKLnN=S{ig1W42~qb>R5i744#q0W!}Akg#Gf z5kN7k1j8c&=sE{bzXI^+lGkh6nmljYr;9XgVg#%`4M=r}1 zkB8(15MK&{lUiCCDg`LihXCYCwq3RHgM}T5@fP_~PB0#t)S_mL1;NbzXy1pHz zUSR+wvbcw2%jyTrb6ZW(wWO}AMT3s?elIx$&ZW6B+;nSFqgnkfXcoJ!pXf~&v{Kza z;VQK}0pi^mT7r_cC$N4Q0m51yErIY9256Z~m4pZm0yJ10ASvO&c*ii22gskE&e0e5 zx-KsN)cddnbhQ0`BhC?(O(^PY3Czfw(ex1H`*C zoVen)Cn!K+>k0uRZ6%=&0d;&N0VsAuK7fQ2gHeDk?}Wjzs|3S?GD=(lRw*1ndWlZB z-jkzo$_l=59djJ#hRsp)igaDYxw3jHwW&|VTS0pE+&eQAtNV=zMDhkGUrbcQA|aNa zViloTh?@u?A!Vo>K&$fsB(#!nusA>h;lX$(4g2t1lW)}Xf5EQ-vDI-Q$ZDy`{U zRiNuC$_iCwOW+M_HmunmeJoLLt%H`yCYPPT;{L8|$NL9m{@QP|bbs)Cc!EAl^7;X{ zJi#E`9`w%GfZkcAbBn<+XerDK^Mi>Yp3pC7G0_s}cb+Mj*HTUwIO!8W3d$hV7N$h4 zg`eXB>B(UFVRrPC45|oT_ViX8PQ)rli7DEVQ;Z}05a$LCS9ZhjcoH|pI&q3aEeE4` zrUXvL2`e}yiYaL&)xcyISbTj4%(@)|-CH1;^;^FgJWX%t6sxoc&-GLQ1-6ph+IVx0}#d4ytT60SqLNUXseVpoy10dE>E#`?l5p9Tov`5YR!ak`o(E0Usf z+D>B~)WVcsMOvJ)0|L@dXFFfq1E#+$zSF2(GXtCpHYbf0A?_(H9>NvPruEykRC|NSjnmJ?sGvT^&9F#0Ub`(~&A0uy7_!nhC*B6pY=>IqKKzrv!( zKp0Pc#zVlxg@=JtMWDQ3LL^g^7fhsD0~4dyz@+H4uq0s{I4AFcsj)sVDRwQ9H%y8{ z`Otf_P?M?F!Q=!^Q&5R0Uzn1_32T_wr5vG^gi|lBC-Q@-mzXYdns(VgPggcjO~1O4 z(=~kF0JBpzWxEh~ChxSr*P>^qK{yBXo7Km#qA8o3YKjO?zUoC5pf%$&v(}nwCR2~O z+%igDNn#=o!RJnoB(V>E=^8#u`(8tmo#AmOT4xs#H)cbNzz`)LH<9|mfojM6=h3rx5=kydl(Yu z40cy{!H{@oS_q~W>p*wYMZ){G;vMrX4)#lM;)KC65ym_ii;dZ~IE}%>XI#zLoK#n2 zcnWTH(A$A(aP)U;)UK6&pFMMuaWMC2@xPX zlMv74k)@JwFagMx0^}lbz^uow^I)ou0WSjJUXo?8`V2@yv7 zE$X$d_bqwuUcGvCjqcm0h3JsMr0YbfZgkO6UI6jyMEWGi#h3?cdC>9*g+~_wit(Z+ zf>D5Es3aUrEDzo_F(ko7VtD%IEfRjxII#fKJjX_mG1kJduF;f^c?&iN)fFvhmNYX{ zWgTeAI@FDHuy?nBiGSiG@MrN!3Q<`AgzA689W0VJ5r90X+Y(wy$N{v50c0mrB_UcK z5kLjuNhlf~+@8=&UQVksyEuSz?$u_t{+wP1=47%}>)g^@T3G^w z3!Agjx6zK>w;rc$f$*r- zRqd`)Q>7CNnCmLiLSb3PM0Hp?*^WWfvtGMq2HiGKzMw@c0lify)h%0I0O1O`;ol@X zi?$V142Id32%t!NnJNhp91bAY;>%EzoU+mS;Jy}#cf#tnX=sdNsM?}#4_edAjcuLE z81qPKiK?@;2;9hPOCaio`!g69bzV7QZJ(o-Z*YL{h*^44Rsm~N9sn7!`_AwfTxsih zcz|%B5CM{N>A7>pn+}Tx`Qn)2*s%{{TQ;V(KSy|q zT5QDCP(1ytl}f!D->NpM(-X~blcC*4ciS>03WHkymLYMsR$c(n?Cd79L{gMw;93u! zMTh_y@Bj%c21Cmu0*Kx8M?Oqgewu^7$3VI38q=62`rnvRmsLl#CypH*LvAcK3M*u z;3+CDs>ODRTNbcJy_*mGc8r?uxZ{0J{QLpq1hhaSGkkOS7|B4uH_?>#y`l&aPI74_ z8F&se9%hLrf)xTt0(f-U$zVDpvl^Q0o`XlM;7Mibd**!j#&y)mCI;V*EyC)wWMft9 zbB}kVwMI4A+C@|P39CV4qh6Tq;~=&etvR{RhN-75f_&c&j$H}taEDL4dy@tvNxqmC z18WLV3ELA05UwQ^0;m*ta65;@IG;$YlY?=NZoED8KW7KC{&IV(?m7NU^I<)vGH`m) zF{q*PEwegJ*%;OMQmu}p)~EsV@9ofJS8rGc7s=FdP`eJ(HtoH3;vNzs-KSr$c4Y){0F$KOY>eN6Od%>}g&Eh7L;yuQln4*HVcj^pPdW(>xw-@z%r@~_eU4i~k8RWL z_gFc0?>B~h%osT8w9lNoYR|@^fzs+o7aP@K*+ok_h;>!J!)%SWNVOW()9<`=sC)OV zQxp0evwW*VCJ#^Wz+-CJmxbgM2b45ljZNKIoPCjtgcP6zA9^Ms1xO4Y9qu6SPsG~f zlK1Bji$m{4*CFwh#_5I7Ywzs0UDuCKXlr5YLHc4KvN&}}A4y*sI4#*2)cKNQ9ii5! z8Z*^(Ss~QdG(IAqN-@{gn@F?854|RR<2-6>&z(PA(L8DS9w%6zSSEzShyX<_RIU+q zb*{Pi^MF*(Pqz2>!|c1i(62u-x?Qrc6a>pD3a|6n!Q@153Xpz`!zZ0+yIdUvCe|*8 z#5TD!K#t?S!vgD)d+nd|{yYDPS324b+uC$cx5?Ocww^;>l`3a(I%)#$RH%s@+&69twDR~x`*&V;!krzF3hsU|*4v!~_ zbI%zO@1A3EX-kgd_1(E+l2*frBoF$xzK?Q-!RH;p;NHy8uHez)y7+7{vt*hEiwK=g$s;azI!U@u7 z+_mkH9_B+9_I01K&3Mba(4l`UO&fmN>7{9eJ6K)Z3iGdTfk}V+!{pQen3}#BrrzBG z(=xXftEm~AVf>YKU>5HMrZJu{Cc+J7gnPr>3qCOX1WCmY*u3n&ZGM`b&rhM6PG;NG zruJXdxJ%oi%+mCs)`ql^S{u@4Y&+{ibJi!N#gP+8s%+W5KFdtLW_v-MDNJO7#4M8t zD5Abi^g55}ILpvV%fWPw&f3Ypb@Q8as@JyZvAy@rPSH4Eo}qcj;=b1L1^;QETKJUc zxz6cD&$Ul4e5!R~!GD^EE${ch*`klWX)~I*u;f=K0jie$!X<9PQpwA006m`<{e}F6La+= zCd8M<-#v%`fZtK;j*4l}+;#zxjj6@lrQXeft0k7uxxrm_q5=Z^mah{O(wnZ5c5%MLzTW;;&e^OY}{C ztn=uo)88w2r^)?25qlV}=l{KscK|wyNki?gG439O9Ob7R3OhtCXdyc=$QtU~O_t|@bak=wm@0{To0s)&_Zz1!!m}mZOs<$X= zET`&U*9Oz92!>_Pu;{solz-KYaP!x*ake?!GkD4CRh8LAD2}#rNlS*SKyLViG_!I( z1FgP^KFw-}(ir1Q^VGs4;=q_V1Jxr{Y@h7ZOUgLY>X6yAh(($%rQIVRuhH1JK0$?? zDVETM)0ZlvrEy$>Gl;7A<~rVKXEWL?rYzPOP*rZLr_Z&ew{A=BKHnDMjVTFVF^T05 zU+CA~s#slbJC%8kQg|J*jjotd*)yq{R%x`cJiWs(;{koDvs7e3|GgMLTcTSprt+cm z$Qu#|^U0zRF3Xu6(D^SzXUTeo>HfKDw`H-FhLu}LGujq%FRt(A!YEt+U=FLE5s9qV z>mp~3l~Dx;l{3-Ie?rVQH$N1%ki^ZM|53Ck`L%B0?e@o={qdjI3V%>D&t^oczm8Ow zejO?rJKz^}X-5yo|6PdRX6q_tv7?yoMmo8|?m|$Qq^Nyr%K6TK23~y>ycU&{~1j>eq z9Ks%pHs*?t6Gd*W_95ED&{lfYk0tA+@CF-c-D;(j`1uXsgS?!tf;aT*MYD)0Dcg)Gf>o-L(^(hCWMLVT>W-XzfyVgh> z71+re>L}QeGnM}kB`otCsaJmRKk4<_w^M8;WaOECJ*n=8y?`>B2}f;VMFhk6VTV}F z$RjM})O8LL!|{8oejqzB&>a}!wu!+hrd+eiD7$8DjL&U+!Je^Jzq?LEg${eYDq|QL z1cP#raZbKu;)z6ve3C72s_MjP6+JEle_rU`Wr}l{tcn7ljGAj_Hh>74myG*8M9H)! zZdZK%rT_66EW3W^I_aEy6;S&}VV#AW#L!?t-UrkQFq0@ZN>m`p17ur$|QOx<5RQ~W_&MB%xL7dV@g%DwdXyX%4G$lRh{;Nr9t zXkn+r-AhRXfMZ=raH6O6B{$vg@}Q5MZw1ULmMOu}q&QP(9qUcP#>2fRU)Clyw1paI z;b-gpL*S}U1qo6-M95i>4r_+5;u}{(sTRquUcNw&N4&nsjLd0-^euj30NJHNi65Wi1e>h&2Vob#rZ8%B4Aeqp*24#Hf89%mFnR07bX9*k5qv~pZ$~Bv&049y9 zecv-?UEvhXde2-OdzUO`Q9CXpD;ZJsGhCA7@GKov^@intitK?(UT5M)C#&{ryxeX4 zUG;gd!oiv*MQUV`S5H*aV2bpE0`mYTNN zgDMeX-veiiXwoY~UWG0`&aa&D|E-GUp$ED-C4N6t%df@k1u~1EZ5>R$gMg z=(pN3C{Ez2Z9sKMRA}7j43qs&>j$QdOw}T>g6pP_qZS_j(ZvAA_D>_BPOA--@uS~b z=pU(6nD!b3KEnK1rbu$nwI|EUJF@CDsQAj_?tYilT9AEOa6@dd`jp<>PH|)_{D1T1 z#xesVvv=9?oLBWj>48m)xM?dqR(Dq!X`gXApDjBv#MmW2zcy<%Mb@55tR%Se3Bge| zWcR855UnnG{zkp8tFQq%nxW~u`ww?(v{ft(z4*Iive7bUr*DSw|%YaE904Z zg{vWQQ+U$&HgW2LK2BY7H1;RccF z%W9%LoluENSHos%bNi&CP*L;$Of)~u>^PJkv62)NY(@PqL>F#&UHh)yiYL*2GKWlO zi#XLn8Jz{X@e_{OO*d|vkRTlj=vY!*MrfDMdw^E(d`W#?^tay?5$#7KQ4GXqAHJxD zkGGy^_mlEqFk+8n&P?>9@Auzddl11CrKDsPo&w zf5lM3T*L6I04aY%Fj6}Qq1@d3k+Rj5LwL(G=yHx1L)_3MHuYohe!n9O#fm1KPzL0c zP(R9Sn#H*vZTRySJ_6xPy$gcoXnQKCL!xctL0jfQFcr3c z&jo+~#;V}%_`1Ev&n6Kn*ni?)Ut~xUs+%t@m)1RFihj9Tg$?~3DzEos{O{RPZ%7C| zvnY!&hlyzTUewaT{-%q|-j_wJ7-bR!(|LB7$8T6$T{dj2k;%U?r-c%Pz_EK^Y<}Cp z#r@z~tFT>~FpH&c#UarjzyIuW-cwB(pVAB&Ryo)P4|V#p3GCRvE@P{mI@c9dp0A2f zu9f3>M0d1gKF`{Ef|L3p->P+SdH0sLQixnu?DWcSYT|dOG?p@tS3O=ILVFyU|4hE% zIdc2i;EP{l1|3Wkms>A_rXd6gk!%wqn|tFp*r2#5Bzkdbh3Zm=+J+mHdH7DKCwhiN zte__}3pWXjFOwOarn|7@%KWx_HB;}siOlK zR+XE$-me7BjT+tXWB#X?S ztn}K*Jab4!Fok!*gBuuWhy6fxvydq!Q*X#*?)FF5^_fqn_LgWt2D$9I`82goeu%fR z!TH0;Eb>%lXf_` zR$b6ml)W@-+X_AUEi~dIWL)sQ#GA+d=eE+5%o6?G)mXJAR%w%sTb}|t{|l6+9=^w~ zUJnu4inQ1qkn99qb6*ymN*S6=iw3*Y}^?WbKD_OG| z$U}o#TJq-T5oqv|w5|P5279l0{tDaAbIB(}#}dN8I7cAq7uMe==s2&tW#~n9-ZCC;pWNW|TxL(LE8LTc@mZqI*7oX+y_&V%h1c$=-sfXe#J!67BW5eU`y4&jAAMd5&L){8I49A(cAs9mNf{t|Aqj+^!f9Z7CX5G|@Hv z;WU8=na%*rCo@YEN9^*M5DUlO6T9EX{B8WbN-{0)gt&w3fuJ9Lw5Pyvn11FsuE+nU z+*5i8XhE3gPgoCdgL4|_u29lmsQechRfT!}}Y2jra)p)QFcRw;DZ^>vWZYnI1@1wjCI}G}uwScRd=*TQ-P=?$Rwwb1XprSCVL^0hk^hkHfJ0>D zQ0gjJgL=P|rLl;NbA#A(24TmNbTIKjY$S)qSS}-6}dcmw#4oQ|ptbv>Au9q5g zDFnzOXP0r07KBNB`U{BbVziFi*=#f+bu>3s?G)TU)r7SIH7*GnFvJsKn37mX_iJr{a48G=gc^#ZLRq2v zl~wTd_xzOf9JaQ=Xm7F!n-$ulkRi^#_|e0Ce4yO@Yg4qw?ILp4`kp;pnGXA&N4GaQ z(M285>ovF zJzq~ruP6+0RIUx^^(C9UpnhMC*@%%=;Ogf*lUY>(B|bMq)8oev4HHl%B*BhxpD`Xp zx~2hLH55uO=v713XC+hcS@B@p$|1j{3c*P^judPe4;GpdI&*svs?O5L3qCdkS>lcD z(;G`%_ck8zBv+#606~epIF+sO>#+`;x$12QoA`(`X<)|7HGw?^oiNBuprzob?<>iQ znh+Uv$ZU7I*0FCgUQkO0A2($QIrfb$M# zR@IX<1W~~X=O?#*OT(_Gf#Cggs%(~Zb(A;k){Q&*cPpN#RYR9e$r2l>pTM=0JsfNr zNG+W`qu4)pI3SCK$+VkjHI2EL>fxGJDopv6>dea=DLa6p_;<`ZB&laQQ`!<=3O_<( zQj0?;$>Tv}ek|E=;7c;4RYFIdPM81QN)5p0=IOfcXmsCd8hiJU^4K=X_?E3Av7pAne0?v_c67v2D~<5Kd}?Z1`066k_+- z4N+7Liguy53`HfvN0gSJYrZOVyuL))gEfz#H#(vBsM$|k0zr#}j00RKWO~s(hvM!; zH9z9x`#S`A=}C2b{K_1%hR(hu4Vm}y1=8N?J8Qio&e_+oOvTj-%RofhxM!s zGlkP=IUUnz1yZWi7YGpztUX4IrD|Bh3nROBb8S{5Y@2rr70a;=tD$ z@;Z^PFvVtS?akp(2jjH7-&;JK$)2)^M@S0DLl z=w`n;hbp=8BQl!%L`wZZXwNXdktbGKC~r!~>^rpv}IRweYExXtAchM>lx+nxaBwkWXA(U;~`Ou1@j8YMUPfHzD8`gp*Q`yepy^l z1U=YX4&hF5r1*xB7hBANP9V-20ADw-3nLx}C~2XLwCfmdJmzIVCNd!SKd;`h3)cT( zoxCLInUMKeUziLWt)|eSj}Vztp~4oyt^l~$5Ky{8)GVkbj0S>-SOH}kY7RL_z@&V3 zj6DtJ;D9#+V2))scw7uj8lgEw029y#*VI#j9>lZ;Ly@rm#o+p1BedEb^mQY1-7ARA zfcW51RSS4N2zI#|t~3`Q>lG!&0+Xa_pl6k&6Y-=){Qe>_XwOxziTDO24Jre;h{CtQ zLpdGNwKDf=x-xlFGz+Kli2&~vbs)9SVG+DbW#AvA;El9sqzJ}@3iI-zQliN3m>up{ zxv_Zs{BBN#ZKc0bX?e@^%A)if!BB-3gDcul0W>o36D-~sx1+;kk>VtvjMhu!;o~x& z(QY)T{NIM4Wizk~Gv1QJ;C?wVn9|Ok88`_4q~~}_>=R4uBY@UAP6hn}vxu*O<%K~T zowv(aAux%JAIwaiH%Kv@XKBFjXVa@8oLsm-668wy!MVgm4##`bhoG`2fEwx!U@wB1 zWKhmTLz-(wh4?V{=s4zb{~>fd(1VcbiPyr@FuzmRi$+kX6MpJ$ZnTv{HU~Z;q^UWg zu1-=@csP1IhR^Zb1&Np&7^sZwj0eaY3%cB<-iS(Y{@!G1Iz0q*pceUaF<*zYNVqH2yb#@SY4(TJ{3tg z&!a{!lI*p^IJ73X27ko2NEZRKn1y`6)6+2>!kF~~-_e$V!=3y&j_bBxzQf_+HrxmDBIAP{E+Xg{TWMTfYN_Q?@&+bYwcSWj473Y9Hhgp(DXpS$Fpev=QRPDyATA+Z8 zo-kT(r zjwl`?IM9jC5Z9hj9p^LI_IP6Cols~?Z~P#bpQWSr4&SzW1jM>w##sgTM`kuykUl>i zQtd`)^ECC^w)N@V;g1D%2w|$V8^@R^h`nVBA2NrAL@_6{0url*;=Dj+3n61(K@1s6 zwIQGH(mef)zgRIA8X$bwz9n2IZ2*Omz@xcELA+ z#*RBlpFQdJKW`)Lc#TDnMqLC#0^ARy%vMD#%>oTwAEM+Em423QI7{1w<}IIkTbGOf z3{x)f9W}S~buIjyvgJTtDSfkN<)abtJ2p}s_qXCz@kxi*rI#@W%VScVD1BFiuGV2u zvS2Dg_kdvLz!M?*i6~&jqEgeROjpa43$}-@_~7=6qY7e7ZD5%~O+ zGL|;n>BAQmQD^e4+rMov9YKN{@Hg)J`GtOWW2&tSR3Btp(G=wyGZdY_2SiH%0hlfn zH1wVQ^ijnX{9GgchYyx^RO(RV6h*CIZZFZ&G~F0KJVw8Btx~egXtkN&^aEu^)s^nB(z8O&=lk zA?I+{7{n-9X9Dt*A_gPekY(VMzn4umS2Cvo{yZQFGNm0;L$np2vMgMA6RI4bbJimv zm@ZXc=Z0j@5h6+X^%0LhL8Xn_|G`cgBRpHeAwH2-_lto~Hb4y=Irq02YuKE;(`+SK zCryo3!D9%Pj08K1@3+Bkp@MEyxgtgxK@vmiA!v{t1T$H+G9EmMYuH#~%~6F6&1*t@ z9Pt{;4>OGzq2;~tqUl|6`1w$J8i`?7CMm81hPJ3aO-*_d>Y?|IQKM7_27c9c(;ew; z4v>FiGy7=Z)54l_W@-f=hL_O*g7=A{d>%_3gBLXf`2`~a zLs0&QOf5Jux3(FuyYD&|2c`cMk~f~vf_D5t%p`aqe!A89%}?oa$n=2?0oUhx~bjsg`VO}G2FACuxVVfj$l3!l)w@&LFBTK5rNdoDlQc;Fi{BvKSl^bQZqqwWvr zUuA^5Plu@&mEqPa9}cIF#_jN{>zdCw3k&rYO#Wp-2LMGVo!{L^ee?Qk}IfM&H>n z>)zXizgwd04%7W3t{H%LbLeg-<=pwt?Mt5S3%?<$m6}dk;i5&^tVKhxo)XN?6yyZ^ zT+J4o>TXI%QfEblHX;ZmxLV@US4R{#dnEM#_=2J+u$E`D+&h;1K&zfcvpKWJ8`&Z-3#M%}S1FXZ78wxP#q?G{jAyIJ zJCpe<_`G5JzWRC%q-uE^vDu__Fl>80r3~Dit-6*T!*w7^B`b^`-%e$;`T?5GSgI@X zARyxlVBj;39Og3-TGBQMq~Pc-O_5d74@HP8XdYj-hiH>I!^Hm_UUnosKrhfY9#+1E zP1woPpDbCkcgBIwlvK-5?(2_}lNzEw$i6^Si4h-EMrDY>qtZjxtz-M}H|o2BsoG(4 zcXaIcxvNEE1;cCA`Qhe|Z&taQH`+4!NZxg|>3ls^TVTad{$+IERDbL@)sUT9PTqQL zfFPL#^IENm{+R9SFQb1vG}#*Nazr%yX;$`1!yi+wT{X zcN8VGJJt8@%UfL^UDX6ixgMND5~gIn_gocOO{9rfP5cZn*+^-(-E!v- zs_Lu$7zlPEin3y=A7|;KqAyb>yXSp{V z0(`|SZ5Id{t8V8^NtAzuOlKWMp+;k+I_+9Gfv$0D=t|@KecX$49_UMi_#(V({0~QU z@ufPiJyNx+EWw1P%0V?UA--(JuoQk0`JrvJC_?Iq7iGMb8s~$~DI7K5VdMvz^)Rz^ zVqH;k$mISv(6!mX;WM-Jr>4h~tG7!{AtdQUm>qTSV&a+8>l@@sA1Fqt zKBQ&y*L**fzM#Vh21NAlHwS%L*cp|+oWD4KG~tw9B>3{%W^MPvslj=7{=weC3&KL( zUDsKfuKcMPT$L38+2zg77Kf_{S1cUsS}S|C7U4|(N=dR(vbk(&k@t`zK>Up8@88uQ zT|XWeoSc>(xJVZ2@@@vW+4mXTIFdU1_Jb`qayPIN_oAD7_*}L^@cg1)_owT@-j^4I z+0YS)Gl95jV^q%duP>Qs8V)pWTHkFu@($8dKF$uY$SksL7oF?e8=P@^`7Ypi|CCP! zu0=?pF%p%MbR-urP(3kH-h25byJDtU7Qc0@l}ZCBZEzzKWe29_?GNo!p<7SHnj&g% zw;Zx}%@j7qS+Qb zNQ2d2uxsw~Z;7Dxb~?GSB>u_AW;Vj#&aI2C5toylWYAw7#^Jm^y3T)=#1o_^|KRkk zOx&q*6Ehs=UA$W8W9O#G(1?TIyvF{-D%g5t%zfPYnEj6{F80{y@R`eD`?71z(bO?| z-?*r2bdk0ZM|AU=cf3{bc`yaa5%xui+751TzwZE)6{(Dl_=O2uPr^#4sU`u-9mD)b2?jxVyVsk)p-j-5rV+cZc8GGY5%N`)qq>0%lm8H1uS zrdQ3<#fnm=+YqTy#qn+McW{6Nihq7Z%e?^;q5A?s$#eedqJriK_0fw%PWwIn2(QJCG|R zma%s1hZS$wg$RPFr;`@@oHqFnTgJs^f|N}7y)BROi2PG7Z`I^f3&-^cBK>#d0vX|3BeajwXf_ z)j5U~=eY+eVY^!~Xi7h8=*EXHwV9nP};_?~c{#{?CH^oz@I@oeyA*pCWq zw2e#6in8t6VUg~3Fa&usGc3uUi`HwI8+pFV13Xc|MXc`&C~b;JS1rj~QNxgMew1nB z4D7_d;*5Jbetta2!F8;T+(Ah#V>?ty2MFS6m6!<7mjssNi9{{Jd6I@mONNHezENXl zm{#X~@>eZ-wi)$l+aKLnZ2t9gmg+|&I7jf48W7C)9)&jHBVmI}LsCPnYKEx&wW^VE zk_3I6Gz;n!XV3;6E?$whGo9~QBJ*mamzN?lAAM2Z4##_ND)HcXvtF(%>8NKz?UEE7 z?rLi929wAH*}Huek?7#OH9uDR4r4^!8 z!+gxw8yooRJ9R2gT&#u1ip(KfX%ZPD1Itr{km7v6<~ij(mB;Bl>MGf)sg^~Y0&dEE z#jWUQy1G&(W2h^+1%V_jB8^WDOj>ccmDoPAwDo4W>ZW)X17o$#|!LpDQEjR{+@%F;CNwQpbc zB&8N0M*~3Y(j31o2D+X~GVwA~fpbLt){>Oy*EQ|ti6O=2AeMa0bkTZp=5}8qH9C+Q z)!f4wQMt#uQe08ZqjVMvz>g*=u!sV=m|~a>$aBCW%zE4~9)Vkv!7nZN>}OGF7M&&U z$9Ixf(P|^!>m1XHitm*4XvJ}eeQ`7@bP=-I+erOa?-J-(`Zm$} zF<@@r4$ienzdE>v(!MbukitTUz5knc2hpuUPVoh~^3=n&#$4MsQ>|%MXh%Wyw3;Lc;%mI@i9@)W#Xg-2d^JJUX z&~w&rf_aYhCEa*bztc-(zwJ3V?3Zdid|1Z^p{R#y0mB@CKH^fF0JdLmoAQ!CBD!aA zH(hG-<9ec^3IF^y>>_1~G;E-+nJ_m*CrhTt#>(o-<`u^eA;|X61@utYA?h#B8<`&9 zlOihJ2^g-wYZsEa3g!N2YrnuitM(`ixg2I^P2DLf^5|iizv$Ndw|5~I+5+os3<|WQ zNe`R0z-@R^Gpv|v8kDp{=x=PpkL+5!`Ip{bk#dPaVEL;dW&5qXS|7ZG*Zh}2%bO^sQ zRZp&#l~(^~BpJ^=RO5lj(Vs_7TB}3bJ}{CZatr-DylRxD)fKHJ*}4Y$@8uzmlTdSNLC-=#x*qinNNdsti|E&#<_>gdGl#&xN0zplKnw zc{7i+`iFZT@HicD(p39DwfCUBR%9fzNdNE&BEEMS-5-UA4vVkY zK8b37zeRds)B-+MadU0|0jB$KV1lk`XDa7dZYcpm%r4=?U?K``7nh!}!PiG*Dl}S1@NdjmWipaWmOme@#>Sqa> zU7c~ErR-P1Z_^JhP0W3JSpY4-V#yp;zVTmiSl|faj&}H;tS?d((}FQ+=wzv}{tTo~ zSB@lFKq)|wC+#;&@HJ$`?)Wnk;~;gax{mFb%n8?lxcUD)j&Mg-E5XXH!BSd8e!WDn zRVvQZ_B(VxbNp^And`q1mup(`;z`zVtlpmYvPp%I@`{uYGwJ&v2v3MCC=Se`n2DN* z=F=rA@$IJLJtn^aqADzbm+5v*pT%TYiU7(2eU&3^G_pt`^)j$_GsaUlAHP@ok4c0S z4j4Tz+VcwVA%HES+4{n@USMIhH7XMB316QN8I3_)jbmt(^cAD34uk>VjP3WBEa2%T5 z?e9T7(kD6id^PQe`Vwc8v-d_83T?Ebb0P6OE_p43-*cEc)U|!Ci6Jy-lH-dV5mpRS z;JH1zTW>Q32jb&{`XG0CTTicx0NcQK=>U;^K9CS=QsVcujRm0U_;VWtV(sC+*(5p- z_BHjg2L$M%nt%(4>r;C}7^Vn1fr4%v`BM@;n&3TgCQySCP`X|z>FX;H)vH2R_WPX{ zz+or$2Q}q62=ZbZ5>p)J+V6bXRDmYRi;iO<>DC)f=-DtvFI{(X;CA-TJoKon7MDn) zHGDYZGq#X-8J#32uaN?fMh?b<6J*3HIkb{ z!q>07-hB&0EF`ZFU&K4g=Ti(~4w)=IjksgKvRFFjRph))2}uY^3`q*9I|@j3%19UJ zi`y8!_<_t{+0z$Snh!C}Z4V=j{eUp|yO0_oKJl%vgG5z?EotRu-$%uzt9v%iiISs$ z%fS*sEj$p7d-EVzQ@UWCc^iWwkQ~x!9{XkY`Tu&-xT|lt`FHHZfO67xd=Szap|3U92aA!?O1 zheL&W8p?FKNvPt*EV- zty)SrPzD8-1<(p*Zck)|O7$wXrB~>8Z&8V|lEaYOSVlF#K`>cm6m~n30zXefVzM2V;gS5NNcITZli$)d{hZ z$u*se_D@8bWq#j5)Rm%qLe+MoaQUeDG^+lj=a`Z!j5vhLHk>Ipj|%CHxM}Q!t=`6% z5J%#^e+C9N6c)i}655NIiKfND`I}f$3xAF8USJfVFP7vVa%|eW?8BYQKFiJc)(_+Dd_GUGu1kc?Sw?w4 zte+9lcOQw`0C`bE1Xk*z36A7i|In_Z$4yQ1p9 zXIkrsPieLFTyy+rrZocx7%OM!g(sDZnsUHWD~r41(iI;^sBc88loByuk3@=S+&gzm zzG~*qH%60Hc+wdvNW9um7M6@NORc6DdzQV0!1I@SOei|YB35Rx{M9s=MC3HB`2&g_ zW=(KtatzVmP=Dp|r>(1X-T`ewl3HbE>2FV)s6OU0>%SoybQqI=WGlOAn)Jdh+h+e} z*iMnlg=R5Zy(a{8%tVm!cM|=KI_M3IrqJx4H$1PP4-*DXNg)VOht<7&ck6;0$JX=juH0!J$fGM`N)ijC;R(Z?3t%tvk<5f1l_Hx z+%aFtq-B`n&ZG_dB+By2)C73oGKsFSY>$;4UZ2dFjIVF=71H)VOQUYB*i3KI3$i&pNg|u#aTrTTm@L z1+3toJ-o7oq;h%>I(*L>^RYqP%|OiGAh+*+;(fe?H zJy0=(cL~&mOmaQ5N&C=kU&8D|-D9wF1*kLaK$g0;R}+@+G_v(U8;Pxlwm2aR+9C)x zm^Ay8q2u)3-E+{^*JQdR63{2lWpRW2AdP@7Msf&^&7BTDBGi|6WR>T6+Jca)w$FaZ z-iO&`R)@<|7anx2$tEW!8fN{r`W2Nn_IuzCWC{~LeHJ8|W(EVEm(D(~RXyqusl&*# zC)A(G&I|7ZM*oatC1+X|l15Qb61IUw{x)1opM9lxmT$T16>cf|j@@zE9Ze{y?}!7O z#SF0FI=*y29>u*%L8dMm%pdJ^Foat#jnhdjzooCGK#xwb=x&4ZF=#Tor`qLb*Z1Ow zo{~>;Ku#&NRa{@@^g3~!M6auYOT2e*|Irx&W5)YM{N_b+1igeVA`3IRRo9lVzX;h%`N94c2r_U10SXKEC^2_G3AKv)G{udqY~DTUCV!wU*5NmISYb z0S2_=#5n0cZ4=8>yKD>6#~N|5GXtCmM?$(s!Gn&}XqJ~{oJNdt0Ljmf3i2Pb>0s!X zsyIXQhg{JdTuYjY8~ZF;PybYS-Prtl61p(Y#=mMR)!BdpI1rWfOob zT~&5Eck1aXD}_AcB3_g@bWh9a@PS5sB<6bH=`CNzF~-kDDK2(;sM}Jz<2NQMgiwL* z<9`hdC_o$HSpX$dy55hz)UQ<`x*xzK>08M6_I6@VR??%sW45*wR_eg6Ne$`mk?X<- zFEwI7U!X6QGR&eL=GOzvGP(}L z|8Ruo|C!D$+MHdVroGT(8_ozbCr}y3?^mu2e#ZX!JPtK+`?+zps*rl|mwfCy-sjq{ ze2!D8ytcauy1>x8LmY=Ei?^$xA*mCFzZ&|$4t*Sy2J@@@{fU!65nP5L&*>LQR982N zXN2d)l>QBTtQlCJDz`W{LQH{YOhMZ#O}fn2mzBL?kc9fbk^SLymYyqQ9fd8?JhXq@ zpFJ>a&=}rvu){j>^seKL0ZIfH-j7SSXDOz2ZafXvQV>mfI;ac&Bs^Co?pO*;j<1`+ z_LI43#ida`P8=8isC!@B7L-m9#3a?(t<%Tl{PsOLEDZf0_z9oSaPmXnT{EF`dysL1 zQ$Zjlve}vA5r*ZBkvafbA=ZrH4`(}cC9zkwgJS0~0g3mP$?=+uD%N~w5u4%@raSvH zq3gQs|LDF9p=|67qD1d3N{kmj1ibP8SI;dK*;e!?eD}ASrSGEIl^s+?fSP>y-(jq& zomz1OD)ebvnRDUAN>#neL!G;4gHE|_;Zv35igN z19B?4=HLC@ubJK;Y811$q~D80>Knz|K<|3`OR0)&QNRql(f9$5)M>IhEx?a3!}nV< z8mU7lL+K2b)0_u$!>y~HnxoUtz!=C!ou3SmG`W=v(4cl$)-i-gi1O0ja9 zo6iixEu8IqUtbJkC3>+91;;L(2BcGm^YuL=_eYouo-gxrV>UyAwdBnAG}B&1734l$ zj(WsYD1Vg92SW2!Yrlsvc2|F>0s{b@_GX0-a2oF*zb1CNL@|2%O(A5aIu<)yYMpSqM#GIzb_SwrnvR zuSMKg`ABd;y2XMkIZ8v$9d9SA33qVrUaSYMWPW(Ulb*0naHX_6;pUh<=U_E@@M|j_ zQITFFy8hQxBzOfBO?iyH1U57fudPACUln(ujfFGsPN_}O205}b@%q|CLNGmE+5YGW zSHDW=v zt5_0tgTUHT1BC_#zsyOTtlKS;8y`L!jcx8l9$>(e#7EDiv0BAPE?o-VlrYQF^Ju2|jij})B5B*~ePB&; z54u5O;J}mzVfb&DaQrH{V4S6ER3_rG8QRB_v{whTo@Y+u5lBXbQP{wBqW5>5&z4`E zaBZdEXc`G*ks@c{KN+>M% zl+68+IY>@AQxhY>l#aGn7SIv}MNP)48|=;De8Hi!T*uAg;~gN!$VxJfU$Yf9)i(m2 zFM{8ZyX3!ifRl$JB=K{?N5*9fJm_O*klY7~B_`*L)FS-8=Fj|J!Nqh9(Nh=6(L^9m ze2a8J(V45Jvo7)Nv`&6ZpDMN{BpP~PA*c>EC&btNe*9SHe23}wcY-R=e)x1^u_(uz zsp+iL%|Zy|y`ilEtii=5pUV<~&nReCSS7GXFnsO87$O}99#7A;Z|MCp%@8wCqu=ot zrxhRNXukfpkmq$R)~`e*_pfjxlvR8SY=}AnOBCY9Y%JT!MxilQ2RLB3F;?ihM4;Q! z6LG<=;@hcjISBJ{o^9euKuC2wFk{Cy+T&33$Boupg%sqEc80ve2n0KAKBZWftft2w z2;P<~>e&l}YBJHF8qbQ#EQC+s6NWt56@nz~KK`C$l6SNDF zo7M%P>+w#o>*cy}rjNpZZ7zXz>T!L0S{gL{65bsn(ieu*QXp}KA3R2|L6%ER`!wi8 zLfT|%eawyrrMuKI)pKQ%1m!SvL@aMEr-YqUI7Q^^@q-yY5+w=fX0o-6^^!m1?fRCp zKxS?W1#8_c@xQ7^1kgTfn{Lw6xJA_=|BdV3pnhU*H~lRiCO?V2y~##RZW-!N6}Oaw z-ipXIyGl#*EL0Q!2BS6YBZ=$r*AJ&)o8W{dL#act4l1EL4ggTC25m79aMDu z6>d1CchA|i9IiW7gI1!L_X;-*ujM7JDe>v0AWPXTexJgMv-VOC<7kno=;jC3bjz?~ zOr8|@9t4Y)QgaoN>6EBsIh{<9TlWAoW0>HFML>uPVHcSvD0Y`A{}TO0m6phk;toA7r;<(k&G+hcSZ01(~pv zI0y{|x!xf~Hi_nc%wQJDFJd2tP`N+Q#j5Dfyct8?i+LD4n6d2&4i$GMh@d{&ISH9M zNkjFC;rf8KQKj>|V-F8=TyKYQSe;(xf*iL6D7Ig2*xOz#DDNx$2`MZC6bw59J4Z-R z?=2EwA(LvZo!vNrM0eV3hys$G^jT~f)I0hDwvn41FA%rloty1->~1E@G}esSWZlMW$BQ{H?03Lg3g&cKB8D=AEWi zQW71pnIs5>6pM2#CTD6fp9J@_WGKZ2BUs3pQ3&=0P+w{QpX;K-JchE-`qbSo>F*J* z5NYPerqO-!iUI2YFbfK7&}fGi%=PFn zbCt58p^})8o5FZT?Se@#{}Y{N#G^KdBMnUwXi@<4Zs~yXZ)0YIK`4r$?*Xp*s59ad zL}rQPJ8h6Zy4}BXE4&d@O9XFhKQ18{Y9bxcPi6eXxA|`#-)FLTuOY!`6pZThSrVUK z{Y7>^2HlVw=6(FgAS6Nj6GOX#3nx$JG{u-rE|d*ghQ$qIUzY6ArDyniO3au)MRFc3SR`E&`4Z*N#d@#XT?GDB>dJIQp^`At0Vwn<4?obElYPV zZPA3#*L=-(Y8bIw$@5lZIwT7w8uA1OrE-NAF6&ezQEa1W3YvFv^n{cU;oISX{p z$oJX$Q&CTSg78AEU~*xSI`R})nj`*;HWlTm6on(YbSNq4(UDUKb|J0_=x71^UGvhR z>cE_gzSM03I^=(q$U&U{s0$bnH-eW?#O}bF>5q#3HLtCL=iYl_7j+*-{81nKp`3L5 zn8JB@Re)30t18s|F0yJKqv}tIR?wFB+OYd)oF-`1tFevAl2>VPu=t>p2t+YS&_e^b zZz6O7>5L*Ynx!`yAc8FTw${Y*7-avqZ88OTAk%GBNy1Bf5<2VCCM^^fKXv8Wm8x)B z{;<$uC;i=M-Y}aVG@P|;gyai#DR!C2wT|~bE&N}Ub3mE}8}!r6 zX{@ z9v+8j=Ua0hB;p%F>cSnfgG*K&O<1Rvq;L7q%Y_me-nu8pUir>!KT0DJ`?tp#%JN)& zf7gJy3dlsRm5hFpo5>g`l%m0w!a|#6U($-75RDSjO2jZhN^V@W3fwU^?hjA-Q^KVk zb>aR?FW%kY0RL=+CL&fb>J3KRWfVlPHGJ@g*}2ms?*aZUR!FHB%e}TgZ(N#8O*Z1w z7Ea-e#2;07Wgfk@S#M8u{@H#LllZUWz@}6D z4O*3@(TJnaITPN$t{yb1>Evo}ti|iHjhsM$83qmE|rmtSPOwY9Y;py5YYv#5P`darC>}fjMe7WO!95 z$K9S1-#asy*PF20G2 zJ8@9hfW*%VRS3xqyh;;BqF$%r(XSStaHef)ea=odBNI==GqiMV% zmN++CeB`UdkI3i?(Wb*@G=hQ;~k-EO;Ssu6pN8f-v zVTgkHUuu7({KI&2Cadt|s^Egy2-}q@a6mFLr4#Rq9*$Ukyd=>GhLR3pNM9+Se6*kn zsc(n!lfp)$9#E{WCPrau1E*H^{Jh6&ONe50W*@%7gt^nGgB&{D*j_gryi1^{IhXl? z(i*c%-rOIghCp3*?UKttk2h=z0(Ap^993%~HY9l1u-8 z5E_NXJ#7OHJiUJj4dDJyoNXA^`(gDho)tD1cM6 z8bo-sc$cOhrc-wHF`Lg+soHZ_#QCN+>)zfTd6rVxhKO6wQ=+m1ktP=v1r%H0UXffU z3xLxt=%AASmv)pmm4k6o;ZEN-l12fq$6gxHBX=B=Id^SJj;q09{BiWfqaegRYnbYU~~^v9gfy~qW>Xh z94f8&|7eg6s%g;h-WEc`4I@M=hVBS5?Fh#Ej0wb>A_lH92j5#oq%nHdN&i5@T&`l= zO?Y=bO^ElYNfLIMGz%|??OzWTjK`_)U4O`d%yR-mJ8zDyAAd#I$3#MYXyOoSFpF02ST5rV3U=JFA76iOs^j;RW6%=VN+RzPwmkdN zS<28GtoWfvr6&0IJGC);uit8KpAs7u%J9hT;+27ROM%z3vFRF$m-HP4yQq?wJC)$} z0eom5{EFiBDZwNjQPc2J1<^f{85)uJICR0E+%oMLGy@Jbo*_Sedj0A)q^08ew*|&+ zb3)*?!4A6aT$LVZ5t5fxYyO4v@Z@d^bt=mLEEmEP9j^@-I-}p>)6hoKNrb>&Gei46 zy`zOQws=Gu0$AGl)4-Y`s0Qah+M$KTeKmq45Ae8JFiC`th}dj3wVhL@8May*A>>_I zG)W@}TZA0XBKGR@%XrV*pV_m;-^Y!ys2{cTgOFCS7 zfpdI(YGncGbU0T3;O2T4y|JU<6^jq`86f%sT+;SxWz=WFaWvw@x_(b_(tyv)z?#S~ zTzr`jMlep|V=&0nCo(`3grWpL%C47)smL(W%0+Qx2$a@|az7k7O~+Vo;!rc0&||H) z7?;-cef1Z;GH@OGqiL%ze@J8opIf6N9;^FO+Gq461mIv3_Y_cpsP6`_8*j0Nbc^%?D?8nu7PVUj`T#Htas$=|XLa>zLZM(jW z$4kT%c*R+KCuTRaqB$UP_2?J0)S8o%o98HgL7V;ivY;tNJEjt z{7=xpqSUk{a({w8E!?!tX@y|3YiTGO3;Lv>v5cZT@g37z!IYQ3VPzuf3S7AAPm^a# z`<|h%t*@sGSieVA9A#FUeIl(}fM;);Vn(2|1mEe|bl1R^0xNH{@Txj;<^I?CNiLy% z0T8*2N>gbwWU7dff&Z%(Rb)J$(O@9-(JXTqa{Cd&(Efro@1W^Ioj9=6qa-x zV{;1X&PQ%msPcRvnMuRV1i8|1N9)RDDO>!g&Q-H80_W|I}Z)-B*_ewVmyf)h)k@_Bw&wZwRjGYGF#v^2AuK=;EO z0Z1`80$pFZ@->{Ao3j!^$&UUN19l2HaH0;kUN~<@#Mx#Rf_XHW0Qo{$@)FtIK z`-TK+7UUr~C$&VE+i|Z5p=Fl4XfSwx87@^kga&}&+Q|Y z%a32lzLlEEbwWCiHMiA@9#v_{2usI3SFXcXnpe03v3tle?!f7~sA>ezA&L$gv*I-> z0zlt+3{H%7-HO3+*Rh4P$q~f0(xqNt66#KE_e(yoyEUS_2^;WsI z0VA-1Zi4kmqamn+I*{=d#ETAG!gG9qW$d|oJKw?<((4pKP6EN@Ehw1Spg?9n@cx4q zXx3c$NrlP$Ux@@c9haesM_R0kz*m%J5Pf{W4p}@mbz;Q+;C!53v%6jq`;?_>r~pK8*sSb)SKpE zj!xaKqUQI)5n9<6kaMj+OCJ;4!0Rb^77a%MUEMOaZ>jL$;(oV+V7hqrd8yz`$qXr@ zO}BS%1fAm4Zt@9xW+Lj8;#8B$PFTO2BxAK+RJOz&m3b6FTRmR2{85n6>^bd2(7 zwc>*XvK-$;!WLXqNoxRATzNQ^Vc0RdBK4NzHwc`n?p?E27l-xbdly)USn9PcWIE}) z4!hRZ>S&)nN8BNpzQ2*rBwuhy!b<61GN6h}9)h_Ml=ppKE#z(z~Hc@=5- zvWjAu<)OUm#lg^^_8TEw`m_s-!BN~gzeM}a) zjF>FwH(RPVfrmYKLQc-Qx3XO#S=21=1_9@3N=uJ(KJJZ~oK3$YJD!;RfMJETXdYG=YOK?3Qvys-Tyn zG-uE$#@7*`lOkTZlQt?MDf%oU&nWs(-@`caOp4 z`LmJJfX-15k!(}6KOox0_+4gN9=At3q8D$-8mQUM6Sp0{^cWJi%omyX*z1z>@>oer zIbyx;#JA%%=@kgOcy?=69`E;y|0c&9yiwHbq+3BZL;W=Iw=B6sOujQisL)8dH>rnP z-QD~c@gT}`ic6&50jUI5mRzbAH$H@shffJ~*9oDTH>1r;e8+cobB#p3s7560#F=xJF^R1@7vL=NEFr;b>bocxNMt^!P^Dt83dGZXG)w6* z&z4j;v(CAhVV_qzFVz#;Vu!cRk7*eAZ&P?SfEBJ72VLjqoz{>a+JD~u;u)`fZ`!WY z*_>ga<=>3g*&mJzdV{Zf*Hh7W7Bee_H1wfQOaE7Tf*dVijLbTlIkMMigDM|9F9m1T zV|v`#_)tkWD0qYt^hHFS!c&K?JJSQb!(@dLotS8~=OKjn%Fkq(*Zw>8o2feXIAC^=kA^yn zwpCL9qh$=UJzWs}_)^UrW=^+3u{~m(*<#}8=%j=DI?q*H$L)3}_JBC&kI%H$?r<<% zHKsobKXyc>>rwgyx%aEk0pSVyTA(2u(ApNNBYw+13~RoSHG@zkSxc0~Wf~&WMuyR&}_9F|k)9kO{)0ZW|509D6jrHD3J=KFIa9!2QuE+)m zu%bCh{#@k2HPO!If4`Dht68Gc#3_$4F+9{hL^r>6TBVKXSC})uw+@S259UiWgc!(iwJ9+4 z;?c2;RtztE5E?Z${vp&0DC8q;Csw2$3R3yGSdA7dm5*_-ae>_VKzJ<;RtXaKab2sC^@S#8URnXUaa)E43AuQ<@a=7R8 zvcHT>((`0(${jg#F~4V>o;O|f{R(`;Y-=fpY@9<}VDl$YGao#rg82Px=Q}*%tdgw> zTKmI_3tS2K@@|ddFlPt%{>D{tXnAKNUnVTJkS6eVi2TOnO0}@V+2Vp;4Bp;D%C!3! zQ6-vz^7i`=Sd-K#mq=tD=gW=aDuT}X_FmB1cr=|PK^q|C6^9?r_KTdmvIrMi{om|C*WFLb5_hhor--}Z1t>l~Dn+4ROFkf;CZMXIwNGqqy+n)7w)mK9NE!3$g)ShF)3~co>B|{AzrF`(R9^u(&P6+K#Utex?$6 zzHY{)xKx`dnWVJbz{*1T&80s&ToPz~{vbi_-Xo>MOWs^=r}atsbm_|q5Iqz0`H8m^NRpxWG)nx$~$KA$oB}T+Q^7x#1i9|0;r)0Ep z`=-o|x~h!EejO4_&3WT+>@-(Jr54aC9yU)blRqp(Ui{lAAxZqT^^a10lH83)1d3si zq+_v9+m}4daONBQNu$EgxHb{9NPF#eOiK^tJDQ|5RtXAP&Mzg1y9?iSvb#>+V+=(p z@vi39=mz;Bu~aOLQ{N(X3mVByN5Mor^Xk(=2-};jCSP%WKjX$db^6vMr$!g9w|ttG zNnJoCP~_*^qqyf>;o>$wwB}3d%(`vfbLS@yd0)aRUGB{|ja4N2H!Caf*!s;&5M(b| z=*Y>TT=663px!178Iyr8B8zC7Ubp)5w8(@mM#~$1((?>Gjp;phc|=d^zTAGHKWTYN zvKW)fO%bGEEfSFX9!@+>FQNH+fbMrOKCL(ePhx8-MQ?vTHWAzBkNNrsvLL@mXq4aWychS&o?VRf#rE6kC+$$+&hc{5Ne&rE zKG|$k`5GkOiPLU(lSo^{Q#V7u0_lhrk<7lbL3+cBEOOd#XAriVQ@+3@qb}HTuxDN^ zv)x~#Gl4^0lq>p%{FmcY(?u8ya3Ob@ZAm+CMJb$UAy`5y=AFaNgH_Z;QYHA=<Los^P4615`ATU{7m+Ws9*b#7eE9VF@ST`9htx%yTH(kV3I7kb02<`cmiAxi=ap zua~WEG}`!eGE}=q%y=89y43C4XRnVW=FdjNVxz7JFGwdm?bP{NF+*)u%aau!f4++P z?!4AP)CnETRq)m?R_BW^@s)du_o-^z|EMGsq5o{*a}_fvqV6DE*%tI>di|fTDWCX| z`_+7q7?x4@{q~2^*!9RR2biZSye6`b`sB(H^Zb6ovX9b@#D5(biRodW_yZvZ)tyqf z1amz!T**d2(NMWf>>o;VtSd2*^y1uA|H)@U3}I_*ncL-%gRjGvda-)jXDud|L2+jT zQbA#bKL@)*dt31@{%~_fx&6_tQ7;VV^JqRCA#iQppUi)0bkRz3Ay2#eWQvmCG#RY{ zYm$~BtG|)0h0`_~!?xoc!vOPSL?>-ebef z!i7>Tf;{u=k~zl)n!=Y5Fz!w)sV$;dzmme`^|TmmsbL%Zcu> zZ)H4KiklB{_n7KziFNl1|IClB zP%IL<_pAOBU`}y5T-Ikjvj@Y-r)eiG6>!pjOyTDVwH&{rSD75)Q2KZ-JFsaleEw3; z`cP1`%VM!O=86iIRCBvT6WU2sy9m$9AKyGQVhJnk;S--&}4|e zN literal 0 HcmV?d00001 diff --git a/ComposeCalendar/sample/src/main/res/values-night/themes.xml b/ComposeCalendar/sample/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..699a78df2 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ComposeCalendar/sample/src/main/res/values/colors.xml b/ComposeCalendar/sample/src/main/res/values/colors.xml new file mode 100644 index 000000000..ca1931bca --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + diff --git a/ComposeCalendar/sample/src/main/res/values/strings.xml b/ComposeCalendar/sample/src/main/res/values/strings.xml new file mode 100644 index 000000000..5a8769f51 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Compose Calendar + diff --git a/ComposeCalendar/sample/src/main/res/values/themes.xml b/ComposeCalendar/sample/src/main/res/values/themes.xml new file mode 100644 index 000000000..7e3bdf6b0 --- /dev/null +++ b/ComposeCalendar/sample/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ComposeCalendar/settings.gradle.kts b/ComposeCalendar/settings.gradle.kts new file mode 100644 index 000000000..6f5d6459f --- /dev/null +++ b/ComposeCalendar/settings.gradle.kts @@ -0,0 +1,4 @@ +rootProject.name = "composecalendar" +include(":sample") +include(":library") +include(":kotlinx-datetime")